Generic Top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we'll see an easy way to send headers in Server-Sent Event (SSE) client requests using the Jersey Client API.

We'll also cover the proper way to send basic key/value headers, authentication headers, and restricted headers using the default Jersey transport connector.

2. Straight to the Point

Probably we've all run into this situation while trying to send headers using SSEs:

We use a SseEventSource to receive SSEs, but to build the SseEventSource, we need a WebTarget instance that doesn't provide us with a way to add headers. The Client instance is no help either. Sound familiar?

Remember though, headers aren't related to SSE, but to the client request itself, so we really ought to be looking there.

Let's see what we can do, then, with ClientRequestFilter.

3. Dependencies

To start our journey, we need the jersey-client dependency as well as Jersey's SSE dependency in our Maven pom.xml file:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>2.29</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-sse</artifactId>
    <version>2.29</version>
</dependency>

Note that Jersey supports JAX-RS 2.1 as of 2.29, so it looks like we'll be able to use features from that.

4. ClientRequestFilter

First, we'll implement the filter that will add the header to each client request:

public class AddHeaderOnRequestFilter implements ClientRequestFilter {

    public static final String FILTER_HEADER_VALUE = "filter-header-value";
    public static final String FILTER_HEADER_KEY = "x-filter-header";

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.getHeaders().add(FILTER_HEADER_KEY, FILTER_HEADER_VALUE);
    }
}

After that, we'll register it and consume it.

For our examples, we'll use https://sse.example.org as an imaginary endpoint from which we want our client to consume events. In reality, we'd change this to the real SSE Event server endpoint that we want our client to consume.

Client client = ClientBuilder.newBuilder()
  .register(AddHeaderOnRequestFilter.class)
  .build();

WebTarget webTarget = client.target("https://sse.example.org/");

SseEventSource sseEventSource = SseEventSource.target(webTarget).build();
sseEventSource.register((event) -> { /* Consume event here */ });
sseEventSource.open();
// do something here until ready to close
sseEventSource.close();

Now, what if we need to send more complex headers like authentication headers to our SSE endpoint?

Let's go together to the next sections to learn more about headers in the Jersey Client API.

5. Headers in Jersey Client API

Is important to know that the default Jersey transport connector implementation makes use of the HttpURLConnection class from the JDK. This class restricts the use of some headers. To avoid that restriction, we can set the system property:

System.setProperty("sun.net.http.allowRestrictedHeaders", "true");

We can find a list of restricted headers in the Jersey docs.

5.1. Simple General Headers

The most straightforward way to define a header is calling WebTarget#request to obtain an Invocation.Builder that provides the header method.

public Response simpleHeader(String headerKey, String headerValue) {
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("https://sse.example.org/");
    Invocation.Builder invocationBuilder = webTarget.request();
    invocationBuilder.header(headerKey, headerValue);
    return invocationBuilder.get();
}

And, actually, we can compact this quite nicely for added readability:

public Response simpleHeaderFluently(String headerKey, String headerValue) {
    Client client = ClientBuilder.newClient();

    return client.target("https://sse.example.org/")
      .request()
      .header(headerKey, headerValue)
      .get();
}

From here we'll use only the fluent form for the samples as it's easier to understand.

5.2. Basic Authentication

Actually, the Jersey Client API provides the HttpAuthenticationFeature class that allows us to send authentication headers easily:

public Response basicAuthenticationAtClientLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic(username, password);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .get();
}

Since we registered feature while building the client, it'll be applied to every request. The API handles the encoding of the username and password that the Basic specification requires.

Note, by the way, that we are implying HTTPS as the mode for our connection. While this is always a valuable security measure, it's fundamental when using Basic authentication as, otherwise, the password is exposed as plaintext. Jersey also supports more sophisticated security configurations.

Now, we can also spec the creds at request-time as well:

public Response basicAuthenticationAtRequestLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder().build();
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .property(HTTP_AUTHENTICATION_BASIC_USERNAME, username)
      .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, password)
      .get();
}

5.3. Digest Authentication

Jersey's HttpAuthenticationFeature also supports Digest authentication:

public Response digestAuthenticationAtClientLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.digest(username, password);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .get();
}

And, likewise, we can override at request-time:

public Response digestAuthenticationAtRequestLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.digest();
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("http://sse.example.org/")
      .request()
      .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, username)
      .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, password)
      .get();
}

5.4. Bearer Token Authentication with OAuth 2.0

OAuth 2.0 supports the notion of Bearer tokens as another authentication mechanism.

We'll need Jersey's oauth2-client dependency to give us OAuth2ClientSupportFeature which is similar to HttpAuthenticationFeature:

<dependency>
    <groupId>org.glassfish.jersey.security</groupId>
    <artifactId>oauth2-client</artifactId>
    <version>2.29</version>
</dependency>

To add a bearer token, we'll follow a similar pattern as before:

public Response bearerAuthenticationWithOAuth2AtClientLevel(String token) {
    Feature feature = OAuth2ClientSupport.feature(token);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.examples.org/")
      .request()
      .get();
}

Or, we can override at the request level, which is specifically handy when the token changes due to rotation:

public Response bearerAuthenticationWithOAuth2AtRequestLevel(String token, String otherToken) {
    Feature feature = OAuth2ClientSupport.feature(token);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .property(OAuth2ClientSupport.OAUTH2_PROPERTY_ACCESS_TOKEN, otherToken)
      .get();
}

5.5. Bearer Token Authentication with OAuth 1.0

Fourthly, if we need to integrate with a legacy code that uses OAuth 1.0, we'll need Jersey's oauth1-client dependency:

<dependency>
    <groupId>org.glassfish.jersey.security</groupId>
    <artifactId>oauth1-client</artifactId>
    <version>2.29</version>
</dependency>

And similarly to OAuth 2.0, we've got OAuth1ClientSupport that we can use:

public Response bearerAuthenticationWithOAuth1AtClientLevel(String token, String consumerKey) {
    ConsumerCredentials consumerCredential = 
      new ConsumerCredentials(consumerKey, "my-consumer-secret");
    AccessToken accessToken = new AccessToken(token, "my-access-token-secret");

    Feature feature = OAuth1ClientSupport
      .builder(consumerCredential)
      .feature()
      .accessToken(accessToken)
      .build();

    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .get();
}

With the request-level again being enabled by the OAuth1ClientSupport.OAUTH_PROPERTY_ACCESS_TOKEN property.

6. Conclusion

To summarize, in this article, we covered how to add headers to SSE client requests in Jersey using filters. We also specifically covered how to work with authentication headers.

The code for this example is available over on GitHub.

Generic bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE
Comments are closed on this article!