7.1.1 Sending your first HTTP request

Obtaining a HttpClient

There are a few ways by which you can obtain a reference to a HttpClient. The most common way is using the Client annotation. For example:

Injecting an HTTP client

  1. @Client("https://api.twitter.com/1.1") @Inject RxHttpClient httpClient;

The above example will inject a client that targets the Twitter API.

  1. @field:Client("\${myapp.api.twitter.url}") @Inject lateinit var httpClient: RxHttpClient

The above Kotlin example will inject a client that targets the Twitter API using a configuration path. Note the required escaping (backslash) on "\${path.to.config}" which is required due to Kotlin string interpolation.

The Client annotation is also a custom scope that will manage the creation of HttpClient instances and ensure they are shutdown when the application shuts down.

The value you pass to the Client annotation can be one of the following:

  • A absolute URI. Example [https://api.twitter.com/1.1](https://api.twitter.com/1.1)

  • A relative URI, in which case the server targeted will be the current server (useful for testing)

  • A service identifier. See the section on Service Discovery for more information on this topic.

Another way to create an HttpClient is with the create static method of the RxHttpClient, however this approach is not recommended as you will have to ensure you manually shutdown the client and of course no dependency injection will occur for the created client.

Performing an HTTP GET

Generally there are two methods of interest when working with the HttpClient. The first method is called retrieve, which will execute an HTTP request and return the body in whichever type you request (by default a String) as an RxJava Flowable.

The retrieve method accepts an HttpRequest object or a String URI to the endpoint you wish to request.

The following example shows how to use retrieve to execute an HTTP GET and receive the response body as a String:

Using retrieve

  1. String uri = UriBuilder.of("/hello/{name}")
  2. .expand(Collections.singletonMap("name", "John"))
  3. .toString();
  4. assertEquals("/hello/John", uri);
  5. String result = client.toBlocking().retrieve(uri);
  6. assertEquals(
  7. "Hello John",
  8. result
  9. );

Using retrieve

  1. when:
  2. String uri = UriBuilder.of("/hello/{name}")
  3. .expand(Collections.singletonMap("name", "John"))
  4. .toString()
  5. then:
  6. "/hello/John" == uri
  7. when:
  8. String result = client.toBlocking().retrieve(uri)
  9. then:
  10. "Hello John" == result

Using retrieve

  1. val uri = UriBuilder.of("/hello/{name}")
  2. .expand(Collections.singletonMap("name", "John"))
  3. .toString()
  4. uri shouldBe "/hello/John"
  5. val result = client.toBlocking().retrieve(uri)
  6. result shouldBe "Hello John"

Note that in this example, for illustration purposes we are calling toBlocking() to return a blocking version of the client. However, in production code you should not do this and instead rely on the non-blocking nature of the Micronaut HTTP server.

For example the following @Controller method calls another endpoint in a non-blocking manner:

Using the HTTP client without blocking

  1. import io.micronaut.http.HttpStatus;
  2. import io.micronaut.http.MediaType;
  3. import io.micronaut.http.annotation.*;
  4. import io.micronaut.http.client.RxHttpClient;
  5. import io.micronaut.http.client.annotation.Client;
  6. import io.reactivex.Maybe;
  7. import static io.micronaut.http.HttpRequest.GET;
  8. @Get("/hello/{name}")
  9. Maybe<String> hello(String name) { (1)
  10. return httpClient.retrieve( GET("/hello/" + name) )
  11. .firstElement(); (2)
  12. }

Using the HTTP client without blocking

  1. import io.micronaut.http.HttpStatus
  2. import io.micronaut.http.MediaType
  3. import io.micronaut.http.annotation.*
  4. import io.micronaut.http.client.RxHttpClient
  5. import io.micronaut.http.client.annotation.Client
  6. import io.reactivex.Maybe
  7. import static io.micronaut.http.HttpRequest.GET
  8. @Get("/hello/{name}")
  9. Maybe<String> hello(String name) { (1)
  10. return httpClient.retrieve( GET("/hello/" + name) )
  11. .firstElement() (2)
  12. }

Using the HTTP client without blocking

  1. import io.micronaut.http.HttpStatus
  2. import io.micronaut.http.MediaType
  3. import io.micronaut.http.annotation.*
  4. import io.micronaut.http.client.RxHttpClient
  5. import io.micronaut.http.client.annotation.Client
  6. import io.reactivex.Maybe
  7. import io.micronaut.http.HttpRequest.GET
  8. @Get("/hello/{name}")
  9. internal fun hello(name: String): Maybe<String> { (1)
  10. return httpClient.retrieve(GET<Any>("/hello/$name"))
  11. .firstElement() (2)
  12. }
1The method hello returns a Maybe which may or may not emit an item. If an item is not emitted a 404 is returned.
2The retrieve method is called which returns a Flowable which has a firstElement method that returns the first emitted item or nothing
Using RxJava (or Reactor if you prefer) you can easily and efficiently compose multiple HTTP client calls without blocking (which will limit the throughput and scalability of your application).

Debugging / Tracing the HTTP Client

To debug the requests being sent and received from the HTTP client you can enable tracing logging via your logback.xml file:

logback.xml

  1. <logger name="io.micronaut.http.client" level="TRACE"/>

Client Specific Debugging / Tracing

To enable client-specific logging you could configure the default logger for all HTTP clients. And, you could also configure different loggers for different clients using Client Specific Configuration. For example, in application.yml:

application.yml

  1. micronaut:
  2. http:
  3. client:
  4. logger-name: mylogger
  5. services:
  6. otherClient:
  7. logger-name: other.client

And, then enable logging in logback.yml:

logback.xml

  1. <logger name="mylogger" level="DEBUG"/>
  2. <logger name="other.client" level="TRACE"/>

Customizing the HTTP Request

The previous example demonstrated using the static methods of the HttpRequest interface to construct a MutableHttpRequest instance. Like the name suggests a MutableHttpRequest can be mutated including the ability to add headers, customize the request body and so on. For example:

Passing an HttpRequest to retrieve

  1. Flowable<String> response = client.retrieve(
  2. GET("/hello/John")
  3. .header("X-My-Header", "SomeValue")
  4. );

Passing an HttpRequest to retrieve

  1. Flowable<String> response = client.retrieve(
  2. GET("/hello/John")
  3. .header("X-My-Header", "SomeValue")
  4. )

Passing an HttpRequest to retrieve

  1. val response = client.retrieve(
  2. GET<Any>("/hello/John")
  3. .header("X-My-Header", "SomeValue")
  4. )

The above example adds an additional header called X-My-Header to the request before it is sent. The MutableHttpRequest interface has a bunch more convenience methods that make it easy to modify the request in common ways.

Reading JSON Responses

Typically with Microservices a message encoding format is used such as JSON. Micronaut’s HTTP client leverages Jackson for JSON parsing hence whatever type Jackson can decode can passed as a second argument to retrieve.

For example consider the following @Controller method that returns a JSON response:

Returning JSON from a controller

  1. @Get("/greet/{name}")
  2. Message greet(String name) {
  3. return new Message("Hello " + name);
  4. }

Returning JSON from a controller

  1. @Get("/greet/{name}")
  2. Message greet(String name) {
  3. return new Message("Hello " + name)
  4. }

Returning JSON from a controller

  1. @Get("/greet/{name}")
  2. internal fun greet(name: String): Message {
  3. return Message("Hello $name")
  4. }

The method above returns a POJO of type Message which looks like:

Message POJO

  1. import com.fasterxml.jackson.annotation.JsonCreator;
  2. import com.fasterxml.jackson.annotation.JsonProperty;
  3. public class Message {
  4. private final String text;
  5. @JsonCreator
  6. public Message(@JsonProperty("text") String text) {
  7. this.text = text;
  8. }
  9. public String getText() {
  10. return text;
  11. }
  12. }

Message POJO

  1. import com.fasterxml.jackson.annotation.JsonCreator
  2. import com.fasterxml.jackson.annotation.JsonProperty
  3. class Message {
  4. private final String text
  5. @JsonCreator
  6. Message(@JsonProperty("text") String text) {
  7. this.text = text
  8. }
  9. String getText() {
  10. return text
  11. }
  12. }

Message POJO

  1. import com.fasterxml.jackson.annotation.JsonCreator
  2. import com.fasterxml.jackson.annotation.JsonProperty
  3. class Message @JsonCreator
  4. constructor(@param:JsonProperty("text") val text: String)
Jackson annotations are used to map the constructor

On the client end you can call this endpoint and decode the JSON into a map using the retrieve method as follows:

Decoding the response body to a Map

  1. Flowable<Map> response = client.retrieve(
  2. GET("/greet/John"), Map.class
  3. );

Decoding the response body to a Map

  1. Flowable<Map> response = client.retrieve(
  2. GET("/greet/John"), Map.class
  3. )

Decoding the response body to a Map

  1. var response: Flowable<Map<*, *>> = client.retrieve(
  2. GET<Any>("/greet/John"), Map::class.java
  3. )

The above examples decodes the response into a Map, representing the JSON. If you wish to customize the type of the key and string you can use the Argument.of(..) method:

Decoding the response body to a Map

  1. response = client.retrieve(
  2. GET("/greet/John"),
  3. Argument.of(Map.class, String.class, String.class) (1)
  4. );

Decoding the response body to a Map

  1. response = client.retrieve(
  2. GET("/greet/John"),
  3. Argument.of(Map.class, String.class, String.class) (1)
  4. )

Decoding the response body to a Map

  1. response = client.retrieve(
  2. GET<Any>("/greet/John"),
  3. Argument.of(Map::class.java, String::class.java, String::class.java) (1)
  4. )
1The Argument.of method is used to return a Map whether the key and value are typed as String

Whilst retrieving JSON as a map can be desirable, more often than not you will want to decode objects into Plain Old Java Objects (POJOs). To do that simply pass the type instead:

Decoding the response body to a POJO

  1. Flowable<Message> response = client.retrieve(
  2. GET("/greet/John"), Message.class
  3. );
  4. assertEquals(
  5. "Hello John",
  6. response.blockingFirst().getText()
  7. );

Decoding the response body to a POJO

  1. when:
  2. Flowable<Message> response = client.retrieve(
  3. GET("/greet/John"), Message.class
  4. )
  5. then:
  6. "Hello John" == response.blockingFirst().getText()

Decoding the response body to a POJO

  1. val response = client.retrieve(
  2. GET<Any>("/greet/John"), Message::class.java
  3. )
  4. response.blockingFirst().text shouldBe "Hello John"

Note how you can use the same Java type on both the client and the server. The implication of this is that typically you will want to define a common API project where you define the interfaces and types that define your API.

Decoding Other Content Types

If the server you are communicating with uses a custom content type that is not JSON by default Micronaut’s HTTP client will not know how to decode this type.

To resolve this issue you can register MediaTypeCodec as a bean and it will be automatically picked up and used to decode (or encode) messages.

Receiving the Full HTTP Response

Sometimes, receiving just the body of the response is not enough and you need other information in the response like headers, cookies, etc. In this case, instead of retrieve you should use the exchange method:

Receiving the Full HTTP Response

  1. Flowable<HttpResponse<Message>> call = client.exchange(
  2. GET("/greet/John"), Message.class (1)
  3. );
  4. HttpResponse<Message> response = call.blockingFirst();
  5. Optional<Message> message = response.getBody(Message.class); (2)
  6. // check the status
  7. assertEquals(
  8. HttpStatus.OK,
  9. response.getStatus() (3)
  10. );
  11. // check the body
  12. assertTrue(message.isPresent());
  13. assertEquals(
  14. "Hello John",
  15. message.get().getText()
  16. );

Receiving the Full HTTP Response

  1. when:
  2. Flowable<HttpResponse<Message>> call = client.exchange(
  3. GET("/greet/John"), Message.class (1)
  4. )
  5. HttpResponse<Message> response = call.blockingFirst();
  6. Optional<Message> message = response.getBody(Message.class) (2)
  7. // check the status
  8. then:
  9. HttpStatus.OK == response.getStatus() (3)
  10. // check the body
  11. message.isPresent()
  12. "Hello John" == message.get().getText()

Receiving the Full HTTP Response

  1. val call = client.exchange(
  2. GET<Any>("/greet/John"), Message::class.java (1)
  3. )
  4. val response = call.blockingFirst()
  5. val message = response.getBody(Message::class.java) (2)
  6. // check the status
  7. response.status shouldBe HttpStatus.OK (3)
  8. // check the body
  9. message.isPresent shouldBe true
  10. message.get().text shouldBe "Hello John"
1The exchange method is used to receive the HttpResponse
2The body can be retrieved using the getBody(..) method of the response
3Other aspects of the response, such as the HttpStatus can be checked

The above example receives the full HttpResponse object from which you can obtain headers and other useful information.