7.1.1 Sending your first HTTP request

Obtaining a HttpClient

There are a few ways to obtain a reference to an HttpClient. The most common is to use the Client annotation. For example:

Injecting an HTTP client

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

The above example injects a client that targets the Twitter API.

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

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

The Client annotation is also a custom scope that manages the creation of HttpClient instances and ensures they are stopped when the application shuts down.

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

  • An absolute URI, e.g. [https://api.twitter.com/1.1](https://api.twitter.com/1.1)

  • A relative URI, in which case the targeted server 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 static create method of HttpClient, however this approach is not recommended as you must 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 is retrieve, which executes an HTTP request and returns the body in whichever type you request (by default a String) as Publisher.

The retrieve method accepts an HttpRequest 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("Hello John", result);

Using retrieve

  1. when:
  2. String uri = UriBuilder.of("/hello/{name}")
  3. .expand(name: "John")
  4. then:
  5. "/hello/John" == uri
  6. when:
  7. String result = client.toBlocking().retrieve(uri)
  8. then:
  9. "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 call 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.annotation.Body;
  2. import io.micronaut.http.annotation.Controller;
  3. import io.micronaut.http.annotation.Get;
  4. import io.micronaut.http.annotation.Post;
  5. import io.micronaut.http.annotation.Status;
  6. import io.micronaut.http.client.HttpClient;
  7. import io.micronaut.http.client.annotation.Client;
  8. import org.reactivestreams.Publisher;
  9. import reactor.core.publisher.Mono;
  10. import io.micronaut.core.async.annotation.SingleResult;
  11. import static io.micronaut.http.HttpRequest.GET;
  12. import static io.micronaut.http.HttpStatus.CREATED;
  13. import static io.micronaut.http.MediaType.TEXT_PLAIN;
  14. @Get("/hello/{name}")
  15. @SingleResult
  16. Publisher<String> hello(String name) { (1)
  17. return Mono.from(httpClient.retrieve(GET("/hello/" + name))); (2)
  18. }

Using the HTTP client without blocking

  1. import io.micronaut.http.annotation.Body
  2. import io.micronaut.http.annotation.Controller
  3. import io.micronaut.http.annotation.Get
  4. import io.micronaut.http.annotation.Post
  5. import io.micronaut.http.annotation.Status
  6. import io.micronaut.http.client.HttpClient
  7. import io.micronaut.http.client.annotation.Client
  8. import org.reactivestreams.Publisher
  9. import io.micronaut.core.async.annotation.SingleResult
  10. import reactor.core.publisher.Mono
  11. import static io.micronaut.http.HttpRequest.GET
  12. import static io.micronaut.http.HttpStatus.CREATED
  13. import static io.micronaut.http.MediaType.TEXT_PLAIN
  14. @Get("/hello/{name}")
  15. @SingleResult
  16. Publisher<String> hello(String name) { (1)
  17. Mono.from(httpClient.retrieve( GET("/hello/" + name))) (2)
  18. }

Using the HTTP client without blocking

  1. import io.micronaut.http.HttpRequest.GET
  2. import io.micronaut.http.HttpStatus.CREATED
  3. import io.micronaut.http.MediaType.TEXT_PLAIN
  4. import io.micronaut.http.annotation.Body
  5. import io.micronaut.http.annotation.Controller
  6. import io.micronaut.http.annotation.Get
  7. import io.micronaut.http.annotation.Post
  8. import io.micronaut.http.annotation.Status
  9. import io.micronaut.http.client.HttpClient
  10. import io.micronaut.http.client.annotation.Client
  11. import org.reactivestreams.Publisher
  12. import reactor.core.publisher.Flux
  13. import io.micronaut.core.async.annotation.SingleResult
  14. @Get("/hello/{name}")
  15. @SingleResult
  16. internal fun hello(name: String): Publisher<String> { (1)
  17. return Flux.from(httpClient.retrieve(GET<Any>("/hello/$name")))
  18. .next() (2)
  19. }
1The hello method returns a reactor:Mono[] 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 reactor:Flux[]. This has a firstElement method that returns the first emitted item or nothing
Using Reactor (or RxJava if you prefer) you can easily and efficiently compose multiple HTTP client calls without blocking (which limits the throughput and scalability of your application).

Debugging / Tracing the HTTP Client

To debug 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 can configure the default logger for all HTTP clients. You can 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

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 demonstrates 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, etc. For example:

Passing an HttpRequest to retrieve

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

Passing an HttpRequest to retrieve

  1. Flux<String> response = Flux.from(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 a header (X-My-Header) to the response before it is sent. The MutableHttpRequest interface has more convenience methods that make it easy to modify the request in common ways.

Reading JSON Responses

Microservices typically use a message encoding format such as JSON. Micronaut’s HTTP client leverages Jackson for JSON parsing, hence any type Jackson can decode can be 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. 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. final String text
  5. @JsonCreator
  6. Message(@JsonProperty("text") String text) {
  7. this.text = text
  8. }
  9. }

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 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. Flux<Map> response = Flux.from(client.retrieve(
  2. GET("/greet/John"), Map.class
  3. ));

Decoding the response body to a Map

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

Decoding the response body to a Map

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

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

Decoding the response body to a Map

  1. response = Flux.from(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 = Flux.from(client.retrieve(
  2. GET("/greet/John"),
  3. Argument.of(Map, String, String) (1)
  4. ))

Decoding the response body to a Map

  1. response = Flux.from(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 returns a Map where the key and value types are String

Whilst retrieving JSON as a map can be desirable, typically you want to decode objects into POJOs. To do that, pass the type instead:

Decoding the response body to a POJO

  1. Flux<Message> response = Flux.from(client.retrieve(
  2. GET("/greet/John"), Message.class
  3. ));
  4. assertEquals("Hello John", response.blockFirst().getText());

Decoding the response body to a POJO

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

Decoding the response body to a POJO

  1. val response = Flux.from(client.retrieve(
  2. GET<Any>("/greet/John"), Message::class.java
  3. ))
  4. response.blockFirst().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 define a common API project where you define the interfaces and types that define your API.

Decoding Other Content Types

If the server you communicate 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, 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 from the response such as headers, cookies, etc. In this case, instead of retrieve use the exchange method:

Receiving the Full HTTP Response

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

Receiving the Full HTTP Response

  1. when:
  2. Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
  3. GET("/greet/John"), Message (1)
  4. ))
  5. HttpResponse<Message> response = call.blockFirst();
  6. Optional<Message> message = response.getBody(Message) (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 = Flux.from(call).blockFirst()
  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 receives the HttpResponse
2The body is 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 from which you can obtain headers and other useful information.