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
@Client("https://api.twitter.com/1.1") @Inject RxHttpClient httpClient;
The above example will inject a client that targets the Twitter API.
@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
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(Collections.singletonMap("name", "John"))
.toString()
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 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
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.*;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.client.annotation.Client;
import io.reactivex.Maybe;
import static io.micronaut.http.HttpRequest.GET;
@Get("/hello/{name}")
Maybe<String> hello(String name) { (1)
return httpClient.retrieve( GET("/hello/" + name) )
.firstElement(); (2)
}
Using the HTTP client without blocking
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.*
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.annotation.Client
import io.reactivex.Maybe
import static io.micronaut.http.HttpRequest.GET
@Get("/hello/{name}")
Maybe<String> hello(String name) { (1)
return httpClient.retrieve( GET("/hello/" + name) )
.firstElement() (2)
}
Using the HTTP client without blocking
import io.micronaut.http.HttpStatus
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.*
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.annotation.Client
import io.reactivex.Maybe
import io.micronaut.http.HttpRequest.GET
@Get("/hello/{name}")
internal fun hello(name: String): Maybe<String> { (1)
return httpClient.retrieve(GET<Any>("/hello/$name"))
.firstElement() (2)
}
1 | The method hello returns a Maybe 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 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
<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
micronaut:
http:
client:
logger-name: mylogger
services:
otherClient:
logger-name: other.client
And, 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 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
Flowable<String> response = client.retrieve(
GET("/hello/John")
.header("X-My-Header", "SomeValue")
);
Passing an HttpRequest to retrieve
Flowable<String> response = 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 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
@Get("/greet/{name}")
Message greet(String name) {
return new Message("Hello " + name);
}
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}")
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 {
private final String text
@JsonCreator
Message(@JsonProperty("text") String text) {
this.text = text
}
String getText() {
return 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 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
Flowable<Map> response = client.retrieve(
GET("/greet/John"), Map.class
);
Decoding the response body to a Map
Flowable<Map> response = client.retrieve(
GET("/greet/John"), Map.class
)
Decoding the response body to a Map
var response: Flowable<Map<*, *>> = client.retrieve(
GET<Any>("/greet/John"), Map::class.java
)
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
response = client.retrieve(
GET("/greet/John"),
Argument.of(Map.class, String.class, String.class) (1)
);
Decoding the response body to a Map
response = client.retrieve(
GET("/greet/John"),
Argument.of(Map.class, String.class, String.class) (1)
)
Decoding the response body to a Map
response = client.retrieve(
GET<Any>("/greet/John"),
Argument.of(Map::class.java, String::class.java, String::class.java) (1)
)
1 | The 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
Flowable<Message> response = client.retrieve(
GET("/greet/John"), Message.class
);
assertEquals(
"Hello John",
response.blockingFirst().getText()
);
Decoding the response body to a POJO
when:
Flowable<Message> response = client.retrieve(
GET("/greet/John"), Message.class
)
then:
"Hello John" == response.blockingFirst().getText()
Decoding the response body to a POJO
val response = client.retrieve(
GET<Any>("/greet/John"), Message::class.java
)
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 object is not enough and you need information about the response. In this case, instead of retrieve
you should use the exchange
method:
Recieving the Full HTTP Response
Flowable<HttpResponse<Message>> call = client.exchange(
GET("/greet/John"), Message.class (1)
);
HttpResponse<Message> response = call.blockingFirst();
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()
);
Recieving the Full HTTP Response
when:
Flowable<HttpResponse<Message>> call = client.exchange(
GET("/greet/John"), Message.class (1)
)
HttpResponse<Message> response = call.blockingFirst();
Optional<Message> message = response.getBody(Message.class) (2)
// check the status
then:
HttpStatus.OK == response.getStatus() (3)
// check the body
message.isPresent()
"Hello John" == message.get().getText()
Recieving the Full HTTP Response
val call = client.exchange(
GET<Any>("/greet/John"), Message::class.java (1)
)
val response = call.blockingFirst()
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 is used to receive the HttpResponse |
2 | The body can be 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 object from which you can obtain headers and other useful information.