1

I have a Java server and a JavaScript client that have successfully been using Server Sent-Events to send updates. Recently we have been forced to switch between using NGINX built into kubernetes as a proxy between the server and the client to using a kong based proxy that extends NGINX with a different configuration. The new proxy buffers the SSE going through it breaking the protocol and because other applications are using the new proxy I am not allowed to turn off all buffering which was how the previous proxy got around the issue. As an alternative, I have been attempting to add 3 http headers to the SSE response to trigger NGINX to turn off buffering for this particular endpoint:

"Content-Type" : "text/event-stream"
"Cache-Control", "no-cache"
"X-Accel-Buffering", "no"

I have been able to add the "Cache-Control" and "X-Accel-Buffering" headers relatively easily, but I have tried several different approaches to add the "Content-Type" header and nothing is working.

Below is the first attempt, notice the "Content-Type" header is set identically to the other two headers in RestServiceImpl, but from my logs the other two headers are added while "Content-Type" is not.

@Api
@Path("/service")
public interface RestService {

    @GET
    @Path("/sseconnect")
    @Produces(SseFeature.SERVER_SENT_EVENTS)
    EventOutput listenToBroadcast(@Context HttpServletResponse response);

}

@Component
public class RestServiceImpl implements RestService {

    @Autowired
    private Broadcaster broadcaster;

    public RestServiceImpl() {
    }

    @Override
    public EventOutput listenToBroadcast(HttpServletResponse response) {
        response.addHeader("Content-Type", "text/event-stream");
        response.addHeader("Cache-Control", "no-cache");
        response.addHeader("X-Accel-Buffering", "no");
        return broadcaster.add();
    }
}

public class Broadcaster extends SseBroadcaster {

    private final OutboundEvent.Builder eventBuilder =
        new OutboundEvent.Builder();

    public EventOutput add() {
        final EventOutput eventOutput = new EventOutput();
        super.add(eventOutput);
        return eventOutput;
    }

    public void sendEvents(Events events, String name) {
        final OutboundEvent outboundEvent = eventBuilder.name(name)
            .mediaType(MediaType.APPLICATION_JSON_TYPE)
            .data(Events.class, events).build();
        super.broadcast(outboundEvent);
    }
}

For my second attempt I tried modifying how the "Content-Type" header was added as follows:

@Override
public EventOutput listenToBroadcast(HttpServletResponse response) {
    response.setContentType("text/event-stream");
    response.addHeader("Cache-Control", "no-cache");
    response.addHeader("X-Accell-Buffering", no);
    return broadcaster.add();
}

This had the same effect, "Cache-Control" and "X-Accell-Bufferig" were added, but "Content-Type" was not.

For the third attempt I replaced HttpServletResponse with ContainerResponse

@Override
public EventOutput listenToBroadcast(ContainerResponse containerResponse)
{
    final MultivaluedMap<String, Object> headers =
        containerResponse.getHeaders();
    headers.add("Content-Type", "text/event-stream");
    headers.add("Cache-Control", "no-cache");
    headers.add("X-Accel-Buffering", "no");
    return broadcaster.add();
}

This solution broke the endpoint and ended up giving me a 406 error, so I can't say definitively that it wouldn't have worked. Could be a bad implementation but I am not seeing anything.

For my fourth attempt I tried adding the headers with a ContainerResponseFilter.

@Provider
@PreMatching
public class SseResponseFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext,
        ContainerResponseContext responseContext)
            throws IOException {

        final MultivaluedMap<String, Object> headers =
            responseContext.getHeaders();
        final List<Object> contentTypeValues = new ArrayList<Object>();
        contentTypeValues.add("text/event-stream");
        headers.put("Content-Type", contentTypeValues);

        final List<Object> cacheControlValues = new ArrayList<Object>();
        cacheControlValues.add("no-cache");
        headers.put("Cache-Control", cacheControlValues);

        final List<Object> xAccelBufferingValues =
            new ArrayList<Object>();
        xAccelBufferingValues.add("no");
        headers.put("X-Accel-Buffering", xAccelBufferingValues);
    }
}

This attempt added the "Cache-Control" and "X-Accel-Buffering" headers to every endpoint in the application, but didn't add "Content-Type" anywhere. Note, some of these other api's were returning a Response object that allowed the "Content-Type" to be set and these REST endpoints were getting the "Content-Type" header set correctly for each particular endpoint. The SSE library returns EventOutput which unfortunately doesn't let me set the Content-Type header like Response does.

For the fifth attempt I replaced the ContainerResponseFilter with a WriterInterceptor

@Provider
public class SseWriterInterceptor implements WriterInterceptor {

    @Override
    public void arroundWriteTo(WriterInterceptorContext context)
        throws IOException, WebApplicationException {

        final MultivaluedMap<String, Object> headers =
            context.getHeaders();
        final List<Object> contentTypeValues = new ArrayList<Object>();
        contentTypeValues.add("text/event-stream");
        headers.put("Content-Type", contentTypeValues);

        final List<Object> cacheControlValues = new ArrayList<Object>();
        cacheControlValues.add("no-cache");
        headers.put("Cache-Control", cacheControlValues);

        final List<Object> xAccelBufferingValues =
            new ArrayList<Object>();
        xAccelBufferingValues.add("no");
        headers.put("X-Accel-Buffering", xAccelBufferingValues);
    }
}

This solution worked like the previous where it added "Cache-Control" and "X-Accel-Buffering" to all endpoints in the application but left off "Content-Type" again.

To sum it all up I can't seem to figure out what is preventing me from successfully adding the "Content-Type" header to my SSE endpoint.

This question is similar to: Why does Jersey swallow my "Content-Encoding" header However, the given solution of adding the "Content-Type" to the Response object doesn't work because I am using SSE which returns a EventOutput object that does not have a way to add the "Content-Type" header.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • What is the value of the `Content-Type` header if you just let Jersey set it? –  Jul 20 '19 at 20:11
  • @LutzHorn Jersey is not setting the "Content-Type" header at all (ie. that specific http header is not present), in any of the variations I tried (including not setting anything). The only way I have seen that actually works to set the "Content-Type" header is to add it to a Response object, which I am not using here. – JabberwokyStrollin Jul 22 '19 at 18:38

1 Answers1

0

It turns out Jersey was not the culprit behind the "Content-Type" header not being set. The application is built as part of a continuous deployment environment that tacks on a number of filters for things like logging and security scanning. One of these filters was preventing the "Content-Type" header from being populated when using the SSE library.

I was able to determine this was the cause after running the application on my local machine vs running it in its kubernetes based development environment and seeing the "Content-Type" header populate properly with the first solution in local.