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
@Client("https://api.twitter.com/1.1") @Inject HttpClient httpClient;
The above example injects a client that targets the Twitter API.
@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
String uri = UriBuilder.of("/hello/{name}")
.expand(Collections.singletonMap("name", "John"))
.toString();
assertEquals("/hello/John", uri);
String result = client.toBlocking().retrieve(uri);
assertEquals("Hello John", result);
Using retrieve
when:
String uri = UriBuilder.of("/hello/{name}")
.expand(name: "John")
then:
"/hello/John" == uri
when:
String result = client.toBlocking().retrieve(uri)
then:
"Hello John" == result
Using retrieve
val uri = UriBuilder.of("/hello/{name}")
.expand(Collections.singletonMap("name", "John"))
.toString()
uri shouldBe "/hello/John"
val result = client.toBlocking().retrieve(uri)
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
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Status;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import io.micronaut.core.async.annotation.SingleResult;
import static io.micronaut.http.HttpRequest.GET;
import static io.micronaut.http.HttpStatus.CREATED;
import static io.micronaut.http.MediaType.TEXT_PLAIN;
@Get("/hello/{name}")
@SingleResult
Publisher<String> hello(String name) { (1)
return Mono.from(httpClient.retrieve(GET("/hello/" + name))); (2)
}
Using the HTTP client without blocking
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import io.micronaut.core.async.annotation.SingleResult
import reactor.core.publisher.Mono
import static io.micronaut.http.HttpRequest.GET
import static io.micronaut.http.HttpStatus.CREATED
import static io.micronaut.http.MediaType.TEXT_PLAIN
@Get("/hello/{name}")
@SingleResult
Publisher<String> hello(String name) { (1)
Mono.from(httpClient.retrieve( GET("/hello/" + name))) (2)
}
Using the HTTP client without blocking
import io.micronaut.http.HttpRequest.GET
import io.micronaut.http.HttpStatus.CREATED
import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import io.micronaut.http.annotation.Status
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import org.reactivestreams.Publisher
import reactor.core.publisher.Flux
import io.micronaut.core.async.annotation.SingleResult
@Get("/hello/{name}")
@SingleResult
internal fun hello(name: String): Publisher<String> { (1)
return Flux.from(httpClient.retrieve(GET<Any>("/hello/$name")))
.next() (2)
}
1 | The hello method returns a reactor:Mono[] which may or may not emit an item. If an item is not emitted, a 404 is returned. |
2 | The 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
<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
micronaut:
http:
client:
logger-name: mylogger
services:
otherClient:
logger-name: other.client
Then enable logging in logback.yml
:
logback.xml
<logger name="mylogger" level="DEBUG"/>
<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
Flux<String> response = Flux.from(client.retrieve(
GET("/hello/John")
.header("X-My-Header", "SomeValue")
));
Passing an HttpRequest to retrieve
Flux<String> response = Flux.from(client.retrieve(
GET("/hello/John")
.header("X-My-Header", "SomeValue")
))
Passing an HttpRequest to retrieve
val response = client.retrieve(
GET<Any>("/hello/John")
.header("X-My-Header", "SomeValue")
)
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
@Get("/greet/{name}")
Message greet(String name) {
return new Message("Hello " + name);
}
Returning JSON from a controller
@Get("/greet/{name}")
Message greet(String name) {
new Message("Hello $name")
}
Returning JSON from a controller
@Get("/greet/{name}")
internal fun greet(name: String): Message {
return Message("Hello $name")
}
The method above returns a POJO of type Message
which looks like:
Message POJO
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Message {
private final String text;
@JsonCreator
public Message(@JsonProperty("text") String text) {
this.text = text;
}
public String getText() {
return text;
}
}
Message POJO
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
class Message {
final String text
@JsonCreator
Message(@JsonProperty("text") String text) {
this.text = text
}
}
Message POJO
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
class Message @JsonCreator
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
Flux<Map> response = Flux.from(client.retrieve(
GET("/greet/John"), Map.class
));
Decoding the response body to a Map
Flux<Map> response = Flux.from(client.retrieve(
GET("/greet/John"), Map
))
Decoding the response body to a Map
var response: Flux<Map<*, *>> = Flux.from(client.retrieve(
GET<Any>("/greet/John"), Map::class.java
))
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
response = Flux.from(client.retrieve(
GET("/greet/John"),
Argument.of(Map.class, String.class, String.class) (1)
));
Decoding the response body to a Map
response = Flux.from(client.retrieve(
GET("/greet/John"),
Argument.of(Map, String, String) (1)
))
Decoding the response body to a Map
response = Flux.from(client.retrieve(
GET<Any>("/greet/John"),
Argument.of(Map::class.java, String::class.java, String::class.java) (1)
))
1 | The 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
Flux<Message> response = Flux.from(client.retrieve(
GET("/greet/John"), Message.class
));
assertEquals("Hello John", response.blockFirst().getText());
Decoding the response body to a POJO
when:
Flux<Message> response = Flux.from(client.retrieve(
GET("/greet/John"), Message
))
then:
"Hello John" == response.blockFirst().getText()
Decoding the response body to a POJO
val response = Flux.from(client.retrieve(
GET<Any>("/greet/John"), Message::class.java
))
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
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
GET("/greet/John"), Message.class (1)
));
HttpResponse<Message> response = call.blockFirst();
Optional<Message> message = response.getBody(Message.class); (2)
// check the status
assertEquals(HttpStatus.OK, response.getStatus()); (3)
// check the body
assertTrue(message.isPresent());
assertEquals("Hello John", message.get().getText());
Receiving the Full HTTP Response
when:
Flux<HttpResponse<Message>> call = Flux.from(client.exchange(
GET("/greet/John"), Message (1)
))
HttpResponse<Message> response = call.blockFirst();
Optional<Message> message = response.getBody(Message) (2)
// check the status
then:
HttpStatus.OK == response.getStatus() (3)
// check the body
message.isPresent()
"Hello John" == message.get().getText()
Receiving the Full HTTP Response
val call = client.exchange(
GET<Any>("/greet/John"), Message::class.java (1)
)
val response = Flux.from(call).blockFirst()
val message = response.getBody(Message::class.java) (2)
// check the status
response.status shouldBe HttpStatus.OK (3)
// check the body
message.isPresent shouldBe true
message.get().text shouldBe "Hello John"
1 | The exchange method receives the HttpResponse |
2 | The body is retrieved using the getBody(..) method of the response |
3 | Other 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.