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;
}
}
-
Non-exhaustive list of imports to use.
-
Must be an extension to be discovered as a service by Jenkins.
-
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.
-
Since there is no HTML/Jelly rendering, no icons are needed.
-
Since there is no HTML/Jelly rendering, no display name is needed.
-
getUrlName()
is the root of the JSON API. Each WebMethod in the class is prefixed by this. -
@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 usingname =
-
JsonHttpResponse
is a subclass ofHttpResponse
that indicates that the response content will be JSON -
An HTTP status can be set in the response.
-
@QueryParameter
indicates that this parameter is injected from HTTP query parameter. By default, the JAVA parameter name is used (in this caseparamValue
), but a different name can be specified by usingvalue =
annotation attribute. -
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.