Skip to content

The New nGrinder HTTP Client

Imbyungjun edited this page Dec 14, 2022 · 7 revisions

Until nGrinder 3.5.3, legacy HTTP client has not updated and exposed on many vulnerabilities. Also, modern HTTP specs were not supported like HTTP2.

From nGrinder 3.5.4, nGrinder provides new HTTP client that supports HTTP2, latest cookie specs, and HTTP methods PATCH.

The new HTTP client for nGrinder must be light-weight so the agent can focus on performance test. The new HTTP client is implemented based on Apache httpcomponents-core.

The new HTTP client supports both HTTP1 and HTTP2. You can enforce protocol version with invoking setVersionPolicy(). Available options are FORCE_HTTP_1, FORCE_HTTP_2 and NEGOTIATE . Default version policy is NEGOTIATE. So, HTTP client negotiates the protocol version with target server.

Basic Request Sample

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse

import org.apache.hc.core5.http2.HttpVersionPolicy

@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request
    public static Map<String, String> headers = [:]
    public static Map<String, String> params = [:]

    @BeforeProcess
    public static void beforeProcess() {
        test = new GTest(1, "sample")
        request = new HTTPRequest()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;
        grinder.logger.info("before thread.");
    }

    @Before
    public void before() {
        request.setHeaders(headers)
        request.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)  // or HttpVersionPolicy.FORCE_HTTP_2
        grinder.logger.info("before. init headers and cookies");
    }

    @Test
    public void test(){
        HTTPResponse result = request.GET("{input_your_api_url}", params)
        grinder.logger.info("protocol version: {}", result.version);
        grinder.logger.info("body: {}", result.bodyText);

        if (result.statusCode == 301 || result.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        } else {
            assertThat(result.statusCode, is(200));
        }
    }
}

Supported Parameters

Legacy nGrinder HTTP client supports only NVPair as a parameter. It was enough for setting request parameters or headers but, in case you have to use bunch of parameters, you also had to write more boilerplate codes.

Now, the new HTTP client supports org.apache.hc.core5.http.NameValuePair and org.apache.hc.core5.http.Header as a parameter. But, you don't have to worry about instantiating implementation class. Map is also supported as a parameter. If you pass a parameter as Map, new HTTP client converts it to appropriate parameter type and request is executed. The NVPair is also supported for easy to migrate existing performance test scripts.

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse

@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request

    @BeforeProcess
    public static void beforeProcess() {
        test = new GTest(1, "sample")
        request = new HTTPRequest()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;
        grinder.logger.info("before thread.");
    }

    @Test
    public void test(){
        // You can pass a parameter with Groovy Map literal
        HTTPResponse result = request.GET("{input_your_api_url}", [ "param1": "value1" ], [ "header1": "value1" ])

        if (result.statusCode == 301 || result.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        } else {
            assertThat(result.statusCode, is(200));
        }
    }
}

Or you can pass a parameter with instantiating a list of org.apache.hc.core5.http.message.BasicHeader or org.apache.hc.core5.http.message.BasicNameValuePair yourself.

import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicNameValuePair;

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse

@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request

    public List<Header> headers = [ new BasicHeader("header1", "value1") ]
    public List<NameValuePair> params = [ new BasicNameValuePair("param1", "value1") ]

    @BeforeProcess
    public static void beforeProcess() {
        test = new GTest(1, "sample")
        request = new HTTPRequest()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;
        grinder.logger.info("before thread.");
    }

    @Test
    public void test(){
        HTTPResponse result = request.GET("{input_your_api_url}", params, headers)

        if (result.statusCode == 301 || result.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        } else {
            assertThat(result.statusCode, is(200));
        }
    }
}

Partial Read Sample

In case the test target API produces a large response body, the performance test will derive humongous network traffic and it's almost same as DDOS attack. You can reduce network traffic by reading only a part of response body. The partial response reading feature is only supported on HTTP1 protocol currently.

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse

import org.apache.hc.core5.http2.HttpVersionPolicy

@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request
    public static Map<String, String> headers = [:]
    public static Map<String, String> params = [:]

    @BeforeProcess
    public static void beforeProcess() {
        test = new GTest(1, "sample")
        request = new HTTPRequest()
        grinder.logger.info("before process.");
    }

    @BeforeThread
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true;

        request.setReadBytes(1024)  // Set how many bytes will you read from response body
        request.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)

        grinder.logger.info("before thread.");
    }

    @Before
    public void before() {
        request.setHeaders(headers)
        grinder.logger.info("before. init headers and cookies");
    }

    @Test
    public void test(){
        HTTPResponse result = request.GET("{input_your_api_url}", params)
        grinder.logger.info("protocol version: {}", result.version);
        grinder.logger.info("body: {}", result.bodyText);

        if (result.statusCode == 301 || result.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        } else {
            assertThat(result.statusCode, is(200));
        }
    }
}

CookieManager with Login Sample

The new HTTP client provides the CookieManager, so you can handle cookies with it. Some APIs may require you to log in first to access them properly. Let's see the login sample script using the CookieManager.

import org.ngrinder.http.cookie.CookieManager;
import org.ngrinder.http.cookie.Cookie;

import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse

/**
 * A simple example test script using login cookie to access target API 
 * with new nGrinder HTTP client.
 */
@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request
    public static HTTPResponse response

    public static Map<String, String> headers = [:]
    public static Map<String, Object> params = [:]
    public static List<Cookie> cookies = []

    @BeforeProcess
    public static void beforeProcess() {
        test = new GTest(1, "sample")
        request = new HTTPRequest()
    }

    @BeforeThread 
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true
    }
    
    @Before
    public void before() {
        headers.put("Content-Type", "application/x-www-form-urlencoded")

        params.put("username", "admin")
        params.put("password", "admin")

        // You can add a cookie explicitly
        cookies.add(new Cookie("cookie_name1", "cookie_value1"))
        cookies.add(new Cookie("cookie_name2", "cookie_value2", "please_modify_this.com", "/", Integer.MAX_VALUE))
        CookieManager.addCookies(cookies)
    }

    @Test
    public void test(){
        response = request.GET("http://please_modify_this.com/api")
        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.info("You may have to log in first.")
        } else {
            assertThat(response.getStatusCode(), not(is(200)))
        }

        // After login request, all the requests performed in current thread contains login cookie
        request.POST("http://please_modify_this.com/login", params, headers)
        grinder.logger.info("cookies: {}", CookieManager.getCookies())

        response = request.GET("http://please_modify_this.com/api")
        assertThat(response.statusCode, is(200));
    }
}

Send a multipart form data

The new HTTP client supports request with multipart form data. To use resources in performance test script context, have to be located in ${PROJECT_ROOT}/resources/ in normal project, ${PROJECT_ROOT}/src/main/java/resources/ in groovy gradle project.

import org.ngrinder.http.multipart.MultipartEntityBuilder

@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request

    @BeforeProcess
    public static void beforeProcess() {
        HTTPRequestControl.setConnectionTimeout(300000)
        test = new GTest(1, "sample")
        request = new HTTPRequest()
        grinder.logger.info("before process.")
    }

    @BeforeThread
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true
        grinder.logger.info("before thread.")
    }

    @Test
    public void test(){
        def multipart = MultipartEntityBuilder.create()
                .addEntity("message", "Hello, nGrinder!")
                .addEntity("file", new File("resources/sample.jpg"))  // if you're working on groovy gradle project, path will be just "sample.jpg"
                .build();
        HTTPResponse response = request.POST("http://please_modify_this.com/", multipart)

        if (response.statusCode == 301 || response.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
        } else {
            assertThat(response.statusCode, is(200))
        }
    }
}

Convert response body as JSON

You can get response body as JSON(Map) with user defined converter.

import groovy.json.JsonSlurper

def toJSON = { new JsonSlurper().parseText(it) }

@RunWith(GrinderRunner)
class TestRunner {

    public static GTest test
    public static HTTPRequest request

    @BeforeProcess
    public static void beforeProcess() {
        test = new GTest(1, "Test1")
        request = new HTTPRequest()
    }

    @BeforeThread
    public void beforeThread() {
        test.record(this, "test")
        grinder.statistics.delayReports = true
    }

    @Test
    public void test(){
        HTTPResponse response = request.GET("http://please_modify_this.com")

        assertThat(response.getBodyText(), is("{\"hello\":\"world!\"}"))
        assertThat(response.getBody(toJSON), is(["hello":"world!"]))
        assertThat(response.getBody(toJSON).hello, is("world!"))
    }
}
Clone this wiki locally