Expose HTTP API with JSON content with Jenkins

This page explains how to expose Json objects over HTTP API in your Jenkins plugins, using GET and POST verbs. This page also shows how to test it with JenkinsRules from jenkins-test-harness.

Pre-requisite: a POJO (Plain Old Java Object)

This object represents the structured data that is exchanged between the HTTP server (Jenkins) and the client (curl for example). We are using a very simple java Object for example purpose, but in production code you will have more complex objects to manipulate.

Note that if you use Stapler (JSONObject) for marshalling and unmarshalling JSON, you need an empty constructor.

public static class MyJsonObject {
    private String message;

    //empty constructor required for JSON parsing.
    public MyJsonObject() {}

    public MyJsonObject(String message) {
        this.message = message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Handling GET with Jenkins

This part shows how to expose an HTTP GET that return a structured JSON response.

/* (1) */
import static java.util.Objects.requireNonNull;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import hudson.Extension;
import hudson.model.RootAction;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.json.JsonBody;
import org.kohsuke.stapler.json.JsonHttpResponse;
import org.kohsuke.stapler.verb.GET;
import org.kohsuke.stapler.verb.POST;

@Extension /* (2) */
public class JsonAPI implements RootAction /* (3) */ {

    @CheckForNull
    @Override
    public String getIconFileName() {
        return null; /* (4) */
    }

    @CheckForNull
    @Override
    public String getDisplayName() {
        return null; /* (5) */
    }

    @Override
    public String getUrlName() {
        return "custom-api"; /* (6) */
    }

    @GET
    @WebMethod(name = "get-example")/* (7) */
    public /* (8) */ JsonHttpResponse getExample() {
        JSONObject response = JSONObject.fromObject(new MyJsonObject("I am Jenkins"));
        return new JsonHttpResponse(response, 200); /* (9) */
    }

    @GET
    @WebMethod(name = "get-example-param")
    public JsonHttpResponse getWithParameters(
        @QueryParameter(required = true) String paramValue /* (10) */) {

        requireNonNull(paramValue);
        MyJsonObject myJsonObject = new MyJsonObject("I am Jenkins " + paramValue);
        JSONObject response = JSONObject.fromObject(myJsonObject);
        return new JsonHttpResponse(response, 200);
    }

    @GET
    @WebMethod(name = "get-error500")
    public JsonHttpResponse getError500() { /* (11) */
        MyJsonObject myJsonObject = new MyJsonObject("You got an error 500");
        JSONObject jsonResponse = JSONObject.fromObject(myJsonObject);
        JsonHttpResponse error500 = new JsonHttpResponse(jsonResponse, 500);
        throw error500;
    }
}
  1. Non-exhaustive list of imports to use.

  2. Must be an extension to be discovered as a service by Jenkins.

  3. This class is an extension of RootAction. It is a way to expose HTTP paths that is more frequently used to expose the Web UI, but it can also be used without HTML rendering.

  4. Since there is no HTML/Jelly rendering, no icons are needed.

  5. Since there is no HTML/Jelly rendering, no display name is needed.

  6. getUrlName() is the root of the JSON API. Each WebMethod in the class is prefixed by this.

  7. @GET indicates the HTTP method that is expected, and @WebMethod indicates that it is accepting an HTTP request. By default, the JAVA name of the WebMethod is used, but a different name can be specified by using name =

  8. JsonHttpResponse is a subclass of HttpResponse that indicates that the response content will be JSON

  9. An HTTP status can be set in the response.

  10. @QueryParameter indicates that this parameter is injected from HTTP query parameter. By default, the JAVA parameter name is used (in this case paramValue), but a different name can be specified by using value = annotation attribute.

  11. The method getError500 is added in the example to show how to set an error response as JSON.

Testing with CURL

A mvn hpi:run in the plugin should be enough to run it locally. Assuming that Jenkins is up at http://localhost:8080/jenkins/ , you should have at this point:

curl -XGET \
    -w "\n STATUS:%{http_code}"  \
    http://localhost:8080/jenkins/custom-api/get-example-param?paramValue=hello

{"message":"I am Jenkins hello"}
 STATUS:200

Example of test with JenkinsRule

import static org.hamcrest.MatcherAssert.assertThat;

import jenkins.model.Jenkins;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.JenkinsRule.JSONWebResponse;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
import org.jvnet.hudson.test.junit.jupiter.WithJenkins;

@WithJenkins
class JsonAPITest {

    private static final String GET_API_URL = "custom-api/get-example-param?paramValue=hello";

    @Test
    void testGetJSON(JenkinsRule j) throws Exception {
        JenkinsRule.WebClient webClient = j.createWebClient();

        JSONWebResponse response = webClient.getJSON(GET_API_URL);
        assertThat(response.getContentAsString(), Matchers.containsString("I am Jenkins hello"));
        assertThat(response.getStatusCode(), Matchers.equalTo(200));
    }

    @Test
    void testAdvancedGetJSON(JenkinsRule j) throws Exception {
        //Given a Jenkins setup with a user "admin"
        MockAuthorizationStrategy auth = new MockAuthorizationStrategy()
            .grant(Jenkins.ADMINISTER).everywhere().to("admin");

        j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
        j.jenkins.setAuthorizationStrategy(auth);

        //We need to setup the WebClient, we use it to call the HTTP API
        JenkinsRule.WebClient webClient = j.createWebClient();

        //By default if the status code is not ok, WebClient throw an exception
        //Since we want to assert the error status code, we need to set to false.
        webClient.setThrowExceptionOnFailingStatusCode(false);

        // - simple call without authentication should be forbidden
        JSONWebResponse response = webClient.getJSON(GET_API_URL);
        assertThat(response.getStatusCode(), Matchers.equalTo(403));

        // - same call but authenticated using withBasicApiToken() should be fine
        response = webClient.withBasicApiToken("admin").getJSON(GET_API_URL);
        assertThat(response.getStatusCode(), Matchers.equalTo(200));
    }
}

Handling POST with Jenkins

This section shows how to expose an HTTP endpoint that takes a structured JSON Object as input, and does a response with a JSON structured Object. For this example the same Object is used as input and output, but you can also use different JSON structure for the response.

Starting from the class JsonAPI provided for GET example, add:

@POST
@WebMethod(name = "create")
public JsonHttpResponse create(@JsonBody MyJsonObject body) {
    //Do any logic required for creation
    //For the example purpose we just uppercase the message parsed from the request.
    JSONObject response = new JSONObject();
    response.put("message", body.message.toUpperCase());
    return new JsonHttpResponse(response, 200);
}

Testing with CURL

A mvn hpi:run in the plugin should be enough to run it locally. Assuming that Jenkins is up at http://localhost:8080/jenkins/ , you should have at this point:

Write a file my.json containing the JSON body:

{"message":"A nice message to send"}

Then, if you need a user and a token:

  • go on Jenkins UI

  • login as a user, for example 'myuser'

  • on the top right click on user name

  • go on configure (for this user)

  • in the section "API Token" create a new token.

For additional documentation on the token, please visit:

And then send the POST request:

curl -XPOST \
    -H "Content-Type: application/json" \
    --user myuser:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    http://localhost:8080/jenkins/custom-api/create \
    --data "@my.json"

{"message":"A NICE MESSAGE TO SEND"}
 STATUS:200

Example of test with JenkinsRule

Starting from the class JsonAPITest provided for the GET example, add:

@Test
void testPostJSON(JenkinsRule j) throws Exception {

    //Given a Jenkins setup with a user "admin"
    MockAuthorizationStrategy auth = new MockAuthorizationStrategy()
        .grant(Jenkins.ADMINISTER).everywhere().to("admin");

    j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
    j.jenkins.setAuthorizationStrategy(auth);

    //We need to setup the WebClient, we use it to call the HTTP API
    JenkinsRule.WebClient webClient = j.createWebClient();

    // Testing an authenticated POST that should answer 200 OK and return same json
    MyJsonObject objectToSend = new MyJsonObject("Jenkins is the way !");
    JenkinsRule.JSONWebResponse response = webClient
        .withBasicApiToken("admin")
        .postJSON("custom-api/create", JSONObject.fromObject(objectToSend));

    //because API is returning the same object, we assert the input message.
    assertThat(response.getContentAsString(), Matchers.containsString("JENKINS IS THE WAY !"));
    assertThat(response.getStatusCode(), Matchers.equalTo(200));
}

Some additional information

For people that are familiar with REST/JSON concept you may want to use other HTTP verbs. It should work, but since generally in Jenkins only GET and POST are used, this page only shows example for this 2 verbs.

You may also want to use several HTTP status code, following HTTP convention like 201 for created. It will also work, and the examples above are returning explicit 200 status to show how to manage the HTTP status that is return. Some statuses are managed by Jenkins Core and may be returned automatically, like 403 when the user in the request does not have the required permission or is anonymous, or 404 when the HTTP API is not found.

If you are not familiar with Jenkins architecture, you can have a look to High level view of Jenkins application and at Architecture

For more advanced reading on the HTTP layer of Jenkins, it’s managed by Stapler.

References