Http Request with Spring WebClient

In Spring 5, a framework for reactive web development was added: Spring webflux. This adds support for non-blocking architectures while coexisting side by side with the current Spring Web MVC APIs. You may create asynchronous web apps with WebFlux by utilizing functional APIs and reactive streams, which better support concurrency and scale.

As part of this, Spring 5 has moved the existing RestTemplate in maintenance mode and introduced the new WebClient API. WebClient can be used to create both synchronous or asynchronous HTTP requests with a functional fluent API, which integrates seamlessly with your current Spring settings and the WebFlux reactive framework.

What is WebClient?

As per official documentation, WebClient is a non-blocking, reactive client to performs HTTP requests, exposing reactive API over underlying HTTP client libraries. It uses various HTTP client libraries like Reactor Netty or Jetty reactive HTTP client, Apache HttpComponents and others to perform HTTP requests.

All the technical aspects of communication over HTTP are handled by the HTTP client library, and WebClient enables the declarative composition of asynchronous logic without the need to deal with threads or concurrency.

By default, WebClient uses Reactor Netty as the HttpClient. We can, however, switch to another HTTP client library.

Project Setup for WebClient Example

Let’s create a simple Spring Boot project with the help of Spring Initializer and add the Webflux dependency. The dependency spring-boot-starter-webflux is a starter dependency for building web applications. This dependency contains a dependency spring-webflux which has the WebClient class.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

We will build a minimal REST web service to process student data with the following @RestController.

@RestController
public class StudentController {

    private List<Student> students;

    @PostConstruct
    public void createData() {
        students = new ArrayList<>();
        students.add(new Student(1, "John", 9.08, "Kolkata, WB"));
        students.add(new Student(2, "Jack", 9.08, "Patna, Bihar"));
        students.add(new Student(3, "Ramesh", 9.08, "Lucknow, UP"));
        students.add(new Student(4, "Shail", 9.08, "Bengalauru, Karnatak"));
        students.add(new Student(5, "Karan", 9.08, "Mumbai, Maharashtra"));
    }


    @GetMapping("/students/{id}")
    public Student getStudent(@PathVariable("id") Integer id) {
        return students.stream().filter(student -> student.getId() == id).findFirst().get();
    }

    @GetMapping("/students")
    public List<Student> getStudents() {
        return students;
    }

    @GetMapping("/students/login/{id}")
    public ResponseEntity<Student> getStudentLogin(@PathVariable("id") Integer id) {
        HttpHeaders respHeader = new HttpHeaders();
        respHeader.set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");

        Student std = students.stream().filter(student -> student.getId() == id).findFirst().get();

        return new ResponseEntity<>(std, respHeader, HttpStatus.OK);
    }

    @PostMapping("/students")
    public ResponseEntity<String> addNewStudent (@RequestBody Student student) {
        Integer id = students.size();
        student.setId(id);

        students.add(student);

        return new ResponseEntity<>("{\"message\":\"Student record added successfully\"}", HttpStatus.CREATED);
    }

    @PutMapping("/students")
    public ResponseEntity<?> updateStudent(@RequestBody Student student) {
        int index = IntStream.range(0, students.size())
                .filter(i -> students.get(i).getId() == student.getId())
                .findFirst()
                .orElse(-1);
        if (index != -1) {
            students.set(index, student);
            return new ResponseEntity<> (HttpStatus.NO_CONTENT);
        }
        return null;
    }

    @DeleteMapping("/students")
    public ResponseEntity<?> deleteStudent(@RequestBody Student student) {
        int index = IntStream.range(0, students.size())
                .filter(i -> students.get(i).getId() == student.getId())
                .findFirst()
                .orElse(-1);
        if (index != -1) {
            students.remove(index);
            return new ResponseEntity<> (HttpStatus.NO_CONTENT);
        }
        return null;
    }

    @GetMapping("/students/error/{id}")
    public ResponseEntity<?> getStudentError(@PathVariable("id") Integer id) {
        return new ResponseEntity<String>("{\"message\":\"Student not found\"}", HttpStatus.NOT_FOUND);
    }
}

The REST service can create, read, update, and delete student resource and supports HTTP GET, POST, PUT, and DELETE request.

Next, we will create a Bean instance of WebClient.

@Bean
public WebClient webClient() {
    return WebClient.create();
}

In the following section, we will utilize WebClient to make use of all these APIs.

Perform HTTP GET Request

To send a GET request, we will use the following code snippet:

  • Here we define a request using WebClient instance, specifying the HTTP request method and URI

  • We will receive a ResponseSpec for the response.

public void getStudentAsJsonString() {

    String url = "http://localhost:8080/students/1";


    WebClient.ResponseSpec responseSpec = webClient.get()
            .uri(url)
            .retrieve();
}

This is all that is necessary to send a request, but it’s important to note that no request has actually been sent at this point. Being a reactive API, the request is not truly sent until an attempt is made to read the response or wait for it.

How do we get the response?

Handling an HTTP Response with WebClient

In the above example, we used .retrieve() to get a ResponseSpec for a request. It is an asynchronous operation that doesn’t block or wait for the request, which means that at the following line, the request is still pending and thus we can’t access any of the response details.

Before we can extract value from the async operation, we need to understand the Flux and Mono types from Reactor.

Flux

A Flux is a representation of a stream of elements. It is a sequence which asynchronously emits any number of elements (0 to N) in future, before completing (either successfully or with an error).

A Flux is a stream that can be transformed or buffered into a List, reduced down to a single item, concatenated and merged with other Fluxes, or blocked on to wait for a value.

Mono

A Mono is a more specific type of a Flux, that will asynchronously emit 0 or 1 element before it completes.

In comparison to Java, Mono is similar to CompletableFuture; it represents a single future value.

For more information on Reactive types and their relationship to traditional Java types, please refer to Spring docs.

Reading the response

To extract the response body, we will have to get a Mono that is an async future value. To trigger the request and obtain the response body content once it is available, we must somehow unwrap the Mono.

Asynchronous values can be unwrapped in multiple ways. Here we will use the simplest option by blocking to wait for data to arrive.

String studentStr = responseSpec.bodyToMono(String.class).block();

In the above code, we will get a string that contains the raw response body. If needed we can parse the response into a specific format, or use a Flux here instead to receive a stream of response parts (e.g.; from an event-based API).

Please take into consideration that we are not manually verifying the status here. When we call .retrieve(), the client checks the status code automatically for us and provides a logical default by returning an error for any 4xx or 5xx answers.

Perform HTTP POST Request

Let us look at an example of how to perform a POST request with the WebClient. Here we will use .post() method for invoking post API.

public void addNewStudent() {

    String url = "http://localhost:8080/students";

    Student student = new Student("Rohit", 7.8, "BBSR");

    String response = webClient.post()
            .uri(url)
            .header("Accept-Language", "en-US")
            .contentType(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromValue(student))
            .accept(MediaType.APPLICATION_JSON)
            .retrieve()
            .bodyToMono(String.class)
            .block();

    System.out.println(response);
}

As we can see, WebClient gives us the option to specify headers using either generic keys and values (.header(key, value)) or specialised methods for typical scenarios (.contentType(type)).

In general, it is better to use dedicated methods since their stricter type will enable us to supply the correct values as they include runtime validation to identify possible erroneous setups.

To send the request body, we can pass request body in the following ways:

  • We can pass the body object inside .body() with a BodyInserter which will build a body content from the object value, multipart value, data buffers or other encodable types.

  • We can use Flux (including Mono) with .body() which can stream content asynchronously to build the request body.

  • We can use .bodyValue() to directly provide a string or other encodable objects.

Similarly, we can send PUT, DELETE or PATCH requests to update or delete records by using the put(), patch() or delete() method.

The above should be sufficient to enable you to make and send simple requests and receive responses, but if you want to develop significant applications on top of this, we still need to cover a few additional topics.

Reading Response headers

Up until this point, we have ignored the response headers and concentrated on reading the response body. Most of the time, that's good, and the crucial headers will be taken care of for us. However, a lot many APIs also offer valuable metadata in the response headers, in addition to the body.

We can easily get the response headers, by using .toEntity() method which gives us ResponseEntiy wrapped in Mono as shown below:

public void getStudentLogin() {

    String url = "http://localhost:8080/students/login/1";


    WebClient.ResponseSpec response = webClient.get()
            .uri(url)
            .retrieve();

    ResponseEntity<Student> resp = response.toEntity(Student.class).block();
    System.out.println(resp.getBody());

    HttpHeaders headers = resp.getHeaders();
    System.out.println(headers.get("Authorization"));
}

Parsing Response Bodies to a specific type

In the previous examples, we have handled responses by parsing them to String, but Spring can also automatically parse the response into higher-level types by providing more specific types while extracting the response, as shown below:

public void getStudentAsPOJO() {

    String url = "http://localhost:8080/students/1";


    WebClient.ResponseSpec responseSpec = webClient.get()
            .uri(url)
            .retrieve();

    Student student = responseSpec.bodyToMono(Student.class).block();

    System.out.println(student);
}

Getting Response Status

By default .retrieve() checks for error status. That’s good enough for simple cases, but you're likely to come across REST APIs that provide more specific success status codes in their response (like returning 201 or 204 values) or APIs where you need to implement custom handling for a certain error status.

Similar to what we did for reading headers, we can use ResponseEntity to get the status code, but this can be only done in the success case, as an error status will throw an error before we receive the ResponseEntity.

To handle such an error status code, we will have to use onStatus handler. This handler can match the status code and can return Mono<Throwable> to throw specific errors or Mono.empty() to stop that error code from being treated as an error.

public void getStudentError() {

    String url = "http://localhost:8080/students/error/1";


    ResponseEntity<String> response = webClient.get()
            .uri(url)
            .retrieve()
            .onStatus(
                    status -> status == HttpStatus.NOT_FOUND,
                    clientResponse -> Mono.empty()
            )
            .toEntity(String.class)
            .block();

    if (response != null && response.getStatusCode() == HttpStatus.NOT_FOUND) {
        System.out.println(response.getBody());
    }
}

Conclusion

  • Spring WebFlux is introduced as a reactive web framework in Spring 5, co-existing with Spring Web MVC but providing support for non-blocking designs using reactive streams and functional APIs.

  • WebClient can be used for making both synchronous or asynchronous HTTP requests

  • To send an HTTP GET Request we will have to use .get() method, and for HTTP POST request we will have to use .post() method along with .body() to send the request body

  • Response bodies can be parsed to specific higher-level types by by providing more specific types.

  • Response headers can be extracted from the response by parsing the response to ResponseEntity

  • To handle specific error status codes we will have to use onStatus handler

You can refer the source code used in the article on GitHub.