7.4 HTTP Client Filters
Often, you need to include the same HTTP headers or URL parameters in a set of requests against a third-party API or when calling another Microservice.
To simplify this, Micronaut includes the ability to define HttpClientFilter classes that are applied to all matching HTTP client requests.
As an example say you want to build a client to communicate with the Bintray REST API. It would be terribly tedious to have to specify authentication for every single HTTP call.
To resolve this burden you can define a filter. The following is an example BintrayService
:
class BintrayApi {
public static final String URL = 'https://api.bintray.com'
}
@Singleton
class BintrayService {
final RxHttpClient client;
final String org;
BintrayService(
@Client(BintrayApi.URL) RxHttpClient client, (1)
@Value("${bintray.organization}") String org ) {
this.client = client;
this.org = org;
}
Flowable<HttpResponse<String>> fetchRepositories() {
return client.exchange(HttpRequest.GET("/repos/" + org), String.class); (2)
}
Flowable<HttpResponse<String>> fetchPackages(String repo) {
return client.exchange(HttpRequest.GET("/repos/" + org + "/" + repo + "/packages"), String.class); (2)
}
}
class BintrayApi {
public static final String URL = 'https://api.bintray.com'
}
@Singleton
class BintrayService {
final RxHttpClient client
final String org
BintrayService(
@Client(BintrayApi.URL) RxHttpClient client, (1)
@Value('${bintray.organization}') String org ) {
this.client = client
this.org = org
}
Flowable<HttpResponse<String>> fetchRepositories() {
return client.exchange(HttpRequest.GET("/repos/$org"), String) (2)
}
Flowable<HttpResponse<String>> fetchPackages(String repo) {
return client.exchange(HttpRequest.GET("/repos/${org}/${repo}/packages"), String) (2)
}
}
class BintrayApi {
public static final String URL = 'https://api.bintray.com'
}
@Singleton
internal class BintrayService(
@param:Client(BintrayApi.URL) val client: RxHttpClient, (1)
@param:Value("\${bintray.organization}") val org: String) {
fun fetchRepositories(): Flowable<HttpResponse<String>> {
return client.exchange(HttpRequest.GET<Any>("/repos/$org"), String::class.java) (2)
}
fun fetchPackages(repo: String): Flowable<HttpResponse<String>> {
return client.exchange(HttpRequest.GET<Any>("/repos/$org/$repo/packages"), String::class.java) (2)
}
}
1 | An RxHttpClient is injected for the Bintray API |
2 | The organization is configurable via configuration |
The Bintray API is secured. To authenticate you need to add an Authorization
header for every request. You could modify fetchRepositories
and fetchPackages
methods to include the necessary HTTP Header for each request. Using a filter is much simpler though:
@Filter("/repos/**") (1)
class BintrayFilter implements HttpClientFilter {
final String username;
final String token;
BintrayFilter(
@Value("${bintray.username}") String username, (2)
@Value("${bintray.token}") String token ) { (2)
this.username = username;
this.token = token;
}
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
return chain.proceed(
request.basicAuth(username, token) (3)
);
}
}
@Filter('/repos/**') (1)
class BintrayFilter implements HttpClientFilter {
final String username
final String token
BintrayFilter(
@Value('${bintray.username}') String username, (2)
@Value('${bintray.token}') String token ) { (2)
this.username = username
this.token = token
}
@Override
Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
return chain.proceed(
request.basicAuth(username, token) (3)
)
}
}
@Filter("/repos/**") (1)
internal class BintrayFilter(
@param:Value("\${bintray.username}") val username: String, (2)
@param:Value("\${bintray.token}") val token: String)(2)
: HttpClientFilter {
override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>> {
return chain.proceed(
request.basicAuth(username, token) (3)
)
}
}
1 | You can match only a subset of paths with a Client filter. |
2 | The username and token are injected via configuration |
3 | The basicAuth method is used include the HTTP BASIC credentials |
Now, whenever you invoke the bintrayService.fetchRepositories()
method, the Authorization
HTTP header is included in the request.
Injecting Another Client into a HttpClientFilter
It should be noted that in order to create a RxHttpClient Micronaut needs to resolve all of the HttpClientFilter
instances which creates a circular dependency in the case where you need to inject another RxHttpClient or a @Client
bean into an instance of a HttpClientFilter
.
To resolve this issue you should use the javax.inject.Provider
interface to inject another RxHttpClient or a @Client
bean into an instance of HttpClientFilter
.
The following example which implements a filter that allows authentication between services on Google Cloud Run demonstrates how to use Provider
to inject another client:
import io.micronaut.context.annotation.Requires;
import io.micronaut.context.env.Environment;
import io.micronaut.http.*;
import io.micronaut.http.annotation.Filter;
import io.micronaut.http.client.RxHttpClient;
import io.micronaut.http.filter.ClientFilterChain;
import io.micronaut.http.filter.HttpClientFilter;
import io.reactivex.Flowable;
import org.reactivestreams.Publisher;
import javax.inject.Provider;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
@Requires(env = Environment.GOOGLE_COMPUTE)
@Filter(patterns = "/google-auth/api/**")
public class GoogleAuthFilter implements HttpClientFilter {
private final Provider<RxHttpClient> authClientProvider;
public GoogleAuthFilter(Provider<RxHttpClient> httpClientProvider) { (1)
this.authClientProvider = httpClientProvider;
}
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
Flowable<String> token = Flowable.fromCallable(() -> encodeURI(request))
.flatMap(authURI -> authClientProvider.get().retrieve(HttpRequest.GET(authURI).header( (2)
"Metadata-Flavor", "Google"
)));
return token.flatMap(t -> chain.proceed(request.bearerAuth(t)));
}
private String encodeURI(MutableHttpRequest<?> request) throws UnsupportedEncodingException {
URI fullURI = request.getUri();
String receivingURI = fullURI.getScheme() + "://" + fullURI.getHost();
return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
URLEncoder.encode(receivingURI, "UTF-8");
}
}
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.Filter
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import io.reactivex.Flowable
import org.reactivestreams.Publisher
import javax.inject.Provider
@Requires(env = Environment.GOOGLE_COMPUTE)
@Filter(patterns = "/google-auth/api/**")
class GoogleAuthFilter implements HttpClientFilter {
private final Provider<RxHttpClient> authClientProvider
GoogleAuthFilter(Provider<RxHttpClient> httpClientProvider) { (1)
this.authClientProvider = httpClientProvider
}
@Override
Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
Flowable<String> token = Flowable.fromCallable(() -> encodeURI(request))
.flatMap(authURI -> authClientProvider.get().retrieve(HttpRequest.GET(authURI).header( (2)
"Metadata-Flavor", "Google"
)))
return token.flatMap(t -> chain.proceed(request.bearerAuth(t)))
}
private static String encodeURI(MutableHttpRequest<?> request) {
String receivingURI = "$request.uri.scheme://$request.uri.host"
return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
URLEncoder.encode(receivingURI, "UTF-8")
}
}
import io.micronaut.context.annotation.Requires
import io.micronaut.context.env.Environment
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.annotation.Filter
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import io.reactivex.Flowable
import org.reactivestreams.Publisher
import java.net.URLEncoder
import javax.inject.Provider
@Requires(env = [Environment.GOOGLE_COMPUTE])
@Filter(patterns = ["/google-auth/api/**"])
class GoogleAuthFilter (
private val authClientProvider: Provider<RxHttpClient>) : HttpClientFilter { (1)
override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>?> {
val token = Flowable.fromCallable { encodeURI(request) }
.flatMap { authURI: String ->
authClientProvider.get().retrieve(HttpRequest.GET<Any>(authURI).header( (2)
"Metadata-Flavor", "Google"
))
}
return token.flatMap { t -> chain.proceed(request.bearerAuth(t)) }
}
private fun encodeURI(request: MutableHttpRequest<*>): String {
val receivingURI = "${request.uri.scheme}://${request.uri.host}"
return "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=" +
URLEncoder.encode(receivingURI, "UTF-8")
}
}
1 | The javax.inject.Provider interface is used to inject another client, avoiding a circular reference |
2 | The get() method of the Provider interface is used to obtain the client instance. |
Filter Matching By Annotation
For cases where a filter should be applied to a client, regardless of the URL, filters can be matched by the presence of an annotation applied to both the filter and the client. Given the following client:
import io.micronaut.http.annotation.Get;
import io.micronaut.http.client.annotation.Client;
@BasicAuth (1)
@Client("/message")
public interface BasicAuthClient {
@Get
String getMessage();
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@BasicAuth (1)
@Client("/message")
interface BasicAuthClient {
@Get
String getMessage()
}
import io.micronaut.http.annotation.Get
import io.micronaut.http.client.annotation.Client
@BasicAuth (1)
@Client("/message")
interface BasicAuthClient {
@Get
fun getMessage(): String
}
1 | The @BasicAuth annotation is applied to the client |
The following filter will filter the client requests:
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpRequest;
import io.micronaut.http.filter.ClientFilterChain;
import io.micronaut.http.filter.HttpClientFilter;
import org.reactivestreams.Publisher;
import javax.inject.Singleton;
@BasicAuth (1)
@Singleton (2)
public class BasicAuthClientFilter implements HttpClientFilter {
@Override
public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
return chain.proceed(request.basicAuth("user", "pass"));
}
}
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import org.reactivestreams.Publisher
import javax.inject.Singleton
@BasicAuth (1)
@Singleton (2)
class BasicAuthClientFilter implements HttpClientFilter {
@Override
Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) {
chain.proceed(request.basicAuth("user", "pass"))
}
}
import io.micronaut.http.HttpResponse
import io.micronaut.http.MutableHttpRequest
import io.micronaut.http.filter.ClientFilterChain
import io.micronaut.http.filter.HttpClientFilter
import org.reactivestreams.Publisher
import javax.inject.Singleton
@BasicAuth (1)
@Singleton (2)
class BasicAuthClientFilter : HttpClientFilter {
override fun doFilter(request: MutableHttpRequest<*>, chain: ClientFilterChain): Publisher<out HttpResponse<*>> {
return chain.proceed(request.basicAuth("user", "pass"))
}
}
1 | The same annotation, @BasicAuth , is applied to the filter |
2 | Normally the @Filter annotation makes filters singletons by default. Because the @Filter annotation is not used, the desired scope must be applied |
The @BasicAuth
annotation is just an example and can be replaced with your own custom annotation.
import io.micronaut.http.annotation.FilterMatcher;
import java.lang.annotation.*;
@FilterMatcher (1)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PARAMETER})
public @interface BasicAuth {
}
import io.micronaut.http.annotation.FilterMatcher
import java.lang.annotation.*
@FilterMatcher (1)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.TYPE, ElementType.PARAMETER])
@interface BasicAuth {
}
import io.micronaut.http.annotation.FilterMatcher
@FilterMatcher (1)
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER)
annotation class BasicAuth
1 | The only requirement for custom annotations is that the @HttpFilterStereotype annotation must be present |