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 represent 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 hudson.model.RootAction;
import net.sf.json.JSONObject;
import org.junit.Rule;
import org.junit.Test;
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 static 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 /* (9) */) {

        assertNotNull(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() { /* (10) */
        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 sub class of HttpResponse that indicates that the response content will be JSON

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

  10. The method get-error500 is added in the example to show how to set a 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

public class JsonAPITest {

    @Rule
    public JenkinsRule j = new JenkinsRule();

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

    @Test
    public void testGetJSON() throws Exception {

        JenkinsRule.WebClient webClient = j.createWebClient();

        // Testing a simple GET that should answer 200 OK and a JSON
        JenkinsRule.JSONWebResponse response = webClient.getJSON(GET_API_URL);
        assertTrue(response.getContentAsString().contains("I am JenkinsRule hello"));
        assertEquals(response.getStatusCode(), 200);
    }

    @Test
    public void testAdvancedGetJSON() 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
        response = webClient.getJSON(GET_API_URL);
        assertEquals(response.getStatusCode(), 403);

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

Handling POST with Jenkins

This section shows how to expose an HTTP endpoint that takes a structured JSON Object as input, and do 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 token, please visit:

And then send the POST request:

curl -XPOST \
    -H "Content-Type: application/json" \
    --user myuser:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    http://localhost:40393/jenkins/testing-cli/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
public void testPostJSON() 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( "testing-cli/create", JSONObject.fromObject(objectToSend));

    //because API is returning the same object, we assert the input message.
    assertTrue(response.getContentAsString().contains("Jenkins is the way !"));
    assertEquals(response.getStatusCode(), 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 example above are returning explicit 200 status to show how to manage the HTTP status that is return. Some status are managed by Jenkins Core and may be return 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