5

I'm looking for examples using a plain JDK11+ http client reading server sent events, without extra dependencies. I can't find anything about sse in the documentation either.

Any hints?

bart van deenen
  • 661
  • 6
  • 16

4 Answers4

3

EDIT 1: Info here and here on the format of the incoming data.

EDIT 2: Updated the code sample to handle the data: part of the protocol. There are also event:, id:, and retry: parts (see links above), but I do not plan to add handling for those.

I can't find an official BodySubscriber to do SSE, but it's not that hard to write one. Here's a rough impl (but note the TODOs):

public class SseSubscriber implements BodySubscriber<Void>
{
    protected static final Pattern dataLinePattern = Pattern.compile( "^data: ?(.*)$" );

    protected static String extractMessageData( String[] messageLines )
    {
        var s = new StringBuilder( );
        for ( var line : messageLines )
        {
            var m = dataLinePattern.matcher( line );
            if ( m.matches( ) )
            {
                s.append( m.group( 1 ) );
            }
        }
        return s.toString( );
    }

    protected final Consumer<? super String> messageDataConsumer;
    protected final CompletableFuture<Void> future;
    protected volatile Subscription subscription;
    protected volatile String deferredText;

    public SseSubscriber( Consumer<? super String> messageDataConsumer )
    {
        this.messageDataConsumer = messageDataConsumer;
        this.future = new CompletableFuture<>( );
        this.subscription = null;
        this.deferredText = null;
    }

    @Override
    public void onSubscribe( Subscription subscription )
    {
        this.subscription = subscription;
        try
        {
            this.deferredText = "";
            this.subscription.request( 1 );
        }
        catch ( Exception e )
        {
            this.future.completeExceptionally( e );
            this.subscription.cancel( );
        }
    }

    @Override
    public void onNext( List<ByteBuffer> buffers )
    {
        try
        {
            // Volatile read
            var deferredText = this.deferredText;

            for ( var buffer : buffers )
            {
                // TODO: Safe to assume multi-byte chars don't get split across buffers?
                var s = deferredText + UTF_8.decode( buffer );

                // -1 means don't discard trailing empty tokens ... so the final token will
                // be whatever is left after the last \n\n (possibly the empty string, but
                // not necessarily), which is the part we need to defer until the next loop
                // iteration
                var tokens = s.split( "\n\n", -1 );

                // Final token gets deferred, not processed here
                for ( var i = 0; i < tokens.length - 1; i++ )
                {
                    var message = tokens[ i ];
                    var lines = message.split( "\n" );
                    var data = extractMessageData( lines );
                    this.messageDataConsumer.accept( data );
                    // TODO: Handle lines that start with "event:", "id:", "retry:"
                }

                // Defer the final token
                deferredText = tokens[ tokens.length - 1 ];
            }

            // Volatile write
            this.deferredText = deferredText;

            this.subscription.request( 1 );
        }
        catch ( Exception e )
        {
            this.future.completeExceptionally( e );
            this.subscription.cancel( );
        }
    }

    @Override
    public void onError( Throwable e )
    {
        this.future.completeExceptionally( e );
    }

    @Override
    public void onComplete( )
    {
        try
        {
            this.future.complete( null );
        }
        catch ( Exception e )
        {
            this.future.completeExceptionally( e );
        }
    }

    @Override
    public CompletionStage<Void> getBody( )
    {
        return this.future;
    }
}

Then to use it:

var req = HttpRequest.newBuilder( )
                     .GET( )
                     .uri( new URI( "http://service/path/to/events" )
                     .setHeader( "Accept", "text/event-stream" )
                     .build( );

this.client.sendAsync( req, respInfo ->
{
    if ( respInfo.statusCode( ) == 200 )
    {
        return new SseSubscriber( messageData ->
        {
            // TODO: Handle messageData
        } );
    }
    else
    {
        throw new RuntimeException( "Request failed" );
    }
} );
stacktracer
  • 171
  • 1
  • 5
3

Java 11 based implementation of SSE (Server-sent Events) client here:
SSE Client

It provides a pretty simple usage of processing of SSE messages.

Example usage:

EventHandler eventHandler = eventText -> { process(eventText); };
        SSEClient sseClient = 
SSEClient sseClient = SSEClient.builder().url(url).eventHandler(eventHandler)
    .build();
sseClient.start();

Note: I am the author of this SSE client.

Liran
  • 144
  • 2
  • 6
0

We just built a lightweight SSE handler you can find here https://github.com/prefab-cloud/java-simple-sse-client . It leaves managing the Java HttpClient to you, and turning bytes into lines to the HttpClient's LineSubscriber so its not a lot of code.

jkebinger
  • 3,944
  • 4
  • 19
  • 14
0

I created one with the help of some of the other posts in this thread. It turns out that if you use some of the pre-defined methods, you don't have to deal with things like ByteBuffers at all.

public record ServerSentEvent(String name, String data, String id, String retry) {}


public class SseTestClient {

    private final HttpClient httpClient = newHttpClient();
    private final List<ServerSentEvent> receivedEvents = new CopyOnWriteArrayList<>();

    public Future<?> subscribe(String url) throws IOException {
        Consumer<ServerSentEvent> eventConsumer = event -> receivedEvents.add(event);
        var bodyHandler = BodyHandlers.fromLineSubscriber(new EventstreamSubscriber(eventConsumer));
        var request = HttpRequest.newBuilder(URI.create(url)).GET().build();
        return httpClient.sendAsync(request, bodyHandler);
    }

    public List<ServerSentEvent> getReceivedEvents() {
        return receivedEvents;
    }
}

public class EventstreamSubscriber implements Subscriber<String> {

    private Subscription subscription;
    private final List<String> bufferedLines = new ArrayList<>();
    private final Consumer<ServerSentEvent> eventConsumer;

    public EventstreamSubscriber(Consumer<ServerSentEvent> eventConsumer) {
        this.eventConsumer = eventConsumer;
    }

    @Override
    public void onSubscribe(Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(String line) {
        if (line.isEmpty()) {
            ServerSentEvent serverSentEvent = mapToEvent(bufferedLines);
            eventConsumer.accept(serverSentEvent);
            bufferedLines.clear();
        } else {
            bufferedLines.add(line);
        }
        subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) {
        throw new RuntimeException("Error recieved");
    }

    @Override
    public void onComplete() {}

    private ServerSentEvent mapToEvent(final List<String> lines) {
        Map<String, String> eventMap = lines.stream()
                .map(line -> line.split(":", 2))
                .filter(pair -> !pair[0].isEmpty())
                .collect(toMap(pair -> pair[0], pair -> pair[1], String::concat));

        return new ServerSentEvent(eventMap.get("event"), eventMap.get("data"), eventMap.get("id"), eventMap.get("retry"));
    }
}

I've been using it for some basic testing. It doesn't do proper error handling. If you intend to consume lots of events with it, you should alter the eventConsumer to not save the event to a list.

Yonas
  • 298
  • 2
  • 8