mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 08:24:13 +00:00
Enforce "data: " prefix for outgoing SSE data payloads
Prior to this commit, SSE support in Spring would write payloads with the "data:" prefix (without space). While this is OK with the standard, this makes it harder for implementations to support reading and writing payloads with Spring (the round trip use case). This commit introduces a breaking change and now enforces "data: " in all variants. This has the potential of breaking some low level test suites with text/plain or custom media types, but this should overall make the situation better for developers. Closes gh-37242
This commit is contained in:
@@ -96,7 +96,7 @@ public final class ServerSentEvent<T> {
|
||||
|
||||
/**
|
||||
* Return a StringBuilder with the id, event, retry, and comment fields fully
|
||||
* serialized, and also appending "data:" if there is data.
|
||||
* serialized, and also appending "data: " if there is data.
|
||||
* @since 6.2.1
|
||||
*/
|
||||
public String format() {
|
||||
@@ -116,7 +116,7 @@ public final class ServerSentEvent<T> {
|
||||
sb.append('\n');
|
||||
}
|
||||
if (this.data != null) {
|
||||
sb.append("data:");
|
||||
sb.append("data: ");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ public class Jackson2JsonEncoder extends AbstractJackson2Encoder {
|
||||
|
||||
private static PrettyPrinter initSsePrettyPrinter() {
|
||||
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
|
||||
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
|
||||
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata: "));
|
||||
return printer;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ public class JacksonJsonEncoder extends AbstractJacksonEncoder<JsonMapper> {
|
||||
|
||||
private static PrettyPrinter initSsePrettyPrinter() {
|
||||
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
|
||||
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
|
||||
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata: "));
|
||||
return printer;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -175,7 +175,7 @@ public abstract class AbstractJacksonHttpMessageConverter<T extends ObjectMapper
|
||||
|
||||
private PrettyPrinter initSsePrettyPrinter() {
|
||||
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
|
||||
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
|
||||
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata: "));
|
||||
return prettyPrinter;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) {
|
||||
this.defaultObjectMapper = objectMapper;
|
||||
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
|
||||
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
|
||||
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata: "));
|
||||
this.ssePrettyPrinter = prettyPrinter;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,9 +31,9 @@ public abstract class SseUtils {
|
||||
/**
|
||||
* Append {@code value} to {@code output}, replacing each line separator
|
||||
* ({@code "\n"}, {@code "\r"}, or {@code "\r\n"}) it contains with a new
|
||||
* {@code field} line (that is, {@code "\n" + field + ":"}). This keeps a
|
||||
* multi-line field value from breaking out of the current SSE field when
|
||||
* written on the wire.
|
||||
* {@code field} line (that is, {@code "\n" + field + ": "}, or
|
||||
* {@code "\n:"} for a comment). This keeps a multi-line field value from
|
||||
* breaking out of the current SSE field when written on the wire.
|
||||
* @param field the name of the SSE field that {@code value} belongs to
|
||||
* (for example, {@code "data"}), or an empty string for a comment
|
||||
* @param value the field value to escape and append
|
||||
@@ -44,7 +44,7 @@ public abstract class SseUtils {
|
||||
output.append(value);
|
||||
return;
|
||||
}
|
||||
String lineSeparatorReplacement = "\n" + field + ":";
|
||||
String lineSeparatorReplacement = "\n" + field + (field.isEmpty() ? ":" : ": ");
|
||||
int length = value.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = value.charAt(i);
|
||||
|
||||
+18
-18
@@ -85,7 +85,7 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
|
||||
StepVerifier.create(outputMessage.getBody())
|
||||
.consumeNextWith(stringConsumer(
|
||||
"id:c42\nevent:foo\nretry:123\n:bla\n:bla bla\n:bla bla bla\ndata:bar\n\n"))
|
||||
"id:c42\nevent:foo\nretry:123\n:bla\n:bla bla\n:bla bla bla\ndata: bar\n\n"))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
@@ -99,8 +99,8 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
testWrite(source, outputMessage, String.class);
|
||||
|
||||
StepVerifier.create(outputMessage.getBody())
|
||||
.consumeNextWith(stringConsumer("data:foo\n\n"))
|
||||
.consumeNextWith(stringConsumer("data:bar\n\n"))
|
||||
.consumeNextWith(stringConsumer("data: foo\n\n"))
|
||||
.consumeNextWith(stringConsumer("data: bar\n\n"))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
@@ -114,9 +114,9 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
testWrite(source, outputMessage, String.class);
|
||||
|
||||
StepVerifier.create(outputMessage.getBody())
|
||||
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
|
||||
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
|
||||
.consumeNextWith(stringConsumer("data:first\ndata:second\n\n"))
|
||||
.consumeNextWith(stringConsumer("data: first\ndata: second\n\n"))
|
||||
.consumeNextWith(stringConsumer("data: first\ndata: second\n\n"))
|
||||
.consumeNextWith(stringConsumer("data: first\ndata: second\n\n"))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
@@ -136,7 +136,7 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
.consumeNextWith(dataBuffer -> {
|
||||
String value = dataBuffer.toString(charset);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
assertThat(value).isEqualTo("data:\u00A3\n\n");
|
||||
assertThat(value).isEqualTo("data: \u00A3\n\n");
|
||||
})
|
||||
.expectComplete()
|
||||
.verify();
|
||||
@@ -151,10 +151,10 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
testWrite(source, outputMessage, Pojo.class);
|
||||
|
||||
StepVerifier.create(outputMessage.getBody())
|
||||
.consumeNextWith(stringConsumer("data:"))
|
||||
.consumeNextWith(stringConsumer("data: "))
|
||||
.consumeNextWith(stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"))
|
||||
.consumeNextWith(stringConsumer("\n\n"))
|
||||
.consumeNextWith(stringConsumer("data:"))
|
||||
.consumeNextWith(stringConsumer("data: "))
|
||||
.consumeNextWith(stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"))
|
||||
.consumeNextWith(stringConsumer("\n\n"))
|
||||
.expectComplete()
|
||||
@@ -173,19 +173,19 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
testWrite(source, outputMessage, Pojo.class);
|
||||
|
||||
StepVerifier.create(outputMessage.getBody())
|
||||
.consumeNextWith(stringConsumer("data:"))
|
||||
.consumeNextWith(stringConsumer("data: "))
|
||||
.consumeNextWith(stringConsumer("""
|
||||
{
|
||||
data: "foo" : "foofoo",
|
||||
data: "bar" : "barbar"
|
||||
data:}"""))
|
||||
data: "foo" : "foofoo",
|
||||
data: "bar" : "barbar"
|
||||
data: }"""))
|
||||
.consumeNextWith(stringConsumer("\n\n"))
|
||||
.consumeNextWith(stringConsumer("data:"))
|
||||
.consumeNextWith(stringConsumer("data: "))
|
||||
.consumeNextWith(stringConsumer("""
|
||||
{
|
||||
data: "foo" : "foofoofoo",
|
||||
data: "bar" : "barbarbar"
|
||||
data:}"""))
|
||||
data: "foo" : "foofoofoo",
|
||||
data: "bar" : "barbarbar"
|
||||
data: }"""))
|
||||
.consumeNextWith(stringConsumer("\n\n"))
|
||||
.expectComplete()
|
||||
.verify();
|
||||
@@ -203,7 +203,7 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||
|
||||
assertThat(outputMessage.getHeaders().getContentType()).isEqualTo(mediaType);
|
||||
StepVerifier.create(outputMessage.getBody())
|
||||
.consumeNextWith(stringConsumer("data:", charset))
|
||||
.consumeNextWith(stringConsumer("data: ", charset))
|
||||
.consumeNextWith(stringConsumer("{\"foo\":\"foo\uD834\uDD1E\",\"bar\":\"bar\uD834\uDD1E\"}", charset))
|
||||
.consumeNextWith(stringConsumer("\n\n", charset))
|
||||
.expectComplete()
|
||||
|
||||
@@ -51,7 +51,7 @@ class ServerSentEventTests {
|
||||
void supportMultiLineComments(String newLine, String description) {
|
||||
ServerSentEvent<String> event = ServerSentEvent.<String>builder()
|
||||
.comment("foo" + newLine + "bar" + newLine + "baz").data("payload").build();
|
||||
assertThat(event.format()).isEqualTo(":foo\n:bar\n:baz\ndata:");
|
||||
assertThat(event.format()).isEqualTo(":foo\n:bar\n:baz\ndata: ");
|
||||
}
|
||||
|
||||
private static Stream<Arguments> newLineCharacters() {
|
||||
|
||||
+1
-1
@@ -415,7 +415,7 @@ class JacksonJsonHttpMessageConverterTests {
|
||||
MediaType.APPLICATION_JSON, outputMessage, null);
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(result).isEqualTo("{\ndata: \"name\" : \"Jason\"\ndata:}");
|
||||
assertThat(result).isEqualTo("{\ndata: \"name\" : \"Jason\"\ndata: }");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+1
-1
@@ -385,7 +385,7 @@ class MappingJackson2HttpMessageConverterTests {
|
||||
this.converter.writeInternal(bean, null, outputMessage);
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(result).isEqualTo("{\ndata: \"name\" : \"Jason\"\ndata:}");
|
||||
assertThat(result).isEqualTo("{\ndata: \"name\" : \"Jason\"\ndata: }");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -44,9 +44,9 @@ class SseUtilsTests {
|
||||
@MethodSource("newLineCharacters")
|
||||
void appendFieldValueReplacesLineSeparatorWithFieldPrefix(String newLine, String description) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("data:");
|
||||
sb.append("data: ");
|
||||
SseUtils.appendFieldValue("data", "first" + newLine + "second", sb);
|
||||
assertThat(sb).hasToString("data:first\ndata:second");
|
||||
assertThat(sb).hasToString("data: first\ndata: second");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
|
||||
Reference in New Issue
Block a user