Browse Source

Allow sending SSE events without data

Prior to this commit, the SseBuilder API in WebMvc.fn would only allow
to send Server-Sent Events with `data:` items in them. The spec doesnn't
disallow this and other specifications rely on such patterns to signal
the completion of a stream, like:

```
event:next
data:some data

event:complete

```

This commit adds a new `send()` method without any argument that sends
the buffered content (id, event comment and retry) without data.

Fixes: gh-32270
pull/32272/head
Brian Clozel 2 years ago
parent
commit
f9ae54d91e
  1. 11
      spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java
  2. 32
      spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java
  3. 24
      spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java

11
spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -570,6 +570,15 @@ public interface ServerResponse { @@ -570,6 +570,15 @@ public interface ServerResponse {
*/
void send(Object object) throws IOException;
/**
* Sends the buffered content as a server-sent event, without data.
* Only the {@link #event(String) events} and {@link #comment(String) comments}
* will be sent.
* @throws IOException in case of I/O errors
* @since 6.1.4
*/
void send() throws IOException;
/**
* Add an SSE "id" line.
* @param id the event identifier

32
spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java

@ -135,6 +135,23 @@ final class SseServerResponse extends AbstractServerResponse { @@ -135,6 +135,23 @@ final class SseServerResponse extends AbstractServerResponse {
data(object);
}
@Override
public void send() throws IOException {
this.builder.append('\n');
try {
OutputStream body = this.outputMessage.getBody();
body.write(builderBytes());
body.flush();
}
catch (IOException ex) {
this.sendFailed = true;
throw ex;
}
finally {
this.builder.setLength(0);
}
}
@Override
public SseBuilder id(String id) {
Assert.hasLength(id, "Id must not be empty");
@ -186,20 +203,7 @@ final class SseServerResponse extends AbstractServerResponse { @@ -186,20 +203,7 @@ final class SseServerResponse extends AbstractServerResponse {
for (String line : lines) {
field("data", line);
}
this.builder.append('\n');
try {
OutputStream body = this.outputMessage.getBody();
body.write(builderBytes());
body.flush();
}
catch (IOException ex) {
this.sendFailed = true;
throw ex;
}
finally {
this.builder.setLength(0);
}
this.send();
}
@SuppressWarnings("unchecked")

24
spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,8 +32,10 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse; @@ -32,8 +32,10 @@ import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ServerResponse.SseBuilder}.
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Brian Clozel
*/
class SseServerResponseTests {
@ -151,6 +153,26 @@ class SseServerResponseTests { @@ -151,6 +153,26 @@ class SseServerResponseTests {
assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected);
}
@Test
void sendWithoutData() throws Exception {
ServerResponse response = ServerResponse.sse(sse -> {
try {
sse.event("custom").send();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
ServerResponse.Context context = Collections::emptyList;
ModelAndView mav = response.writeTo(this.mockRequest, this.mockResponse, context);
assertThat(mav).isNull();
String expected = "event:custom\n\n";
assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected);
}
private static final class Person {

Loading…
Cancel
Save