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?
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?
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" );
}
} );
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.
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.
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 ByteBuffer
s 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.