mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Support multi-line comments in Server Sent Events
Prior to this commit, comments sent with Server Sent Events could break the wire format when sent over the network when comments contained line breaks. While comments are mainly used for sending keepalive messages, they can also be used for sending debug data. This commit ensures that line breaks are properly handled in comments. Fixes gh-36866
This commit is contained in:
@@ -22,7 +22,6 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Representation for a Server-Sent Event for use with Spring's reactive Web support.
|
||||
@@ -112,7 +111,9 @@ public final class ServerSentEvent<T> {
|
||||
appendAttribute("retry", this.retry.toMillis(), sb);
|
||||
}
|
||||
if (this.comment != null) {
|
||||
sb.append(':').append(StringUtils.replace(this.comment, "\n", "\n:")).append('\n');
|
||||
sb.append(':');
|
||||
appendEscaped(this.comment, "\n:", sb);
|
||||
sb.append('\n');
|
||||
}
|
||||
if (this.data != null) {
|
||||
sb.append("data:");
|
||||
@@ -124,6 +125,30 @@ public final class ServerSentEvent<T> {
|
||||
sb.append(fieldName).append(':').append(fieldValue).append('\n');
|
||||
}
|
||||
|
||||
private void appendEscaped(String input, String replacement, StringBuilder sb) {
|
||||
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
|
||||
sb.append(input);
|
||||
}
|
||||
else {
|
||||
int length = input.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = input.charAt(i);
|
||||
if (c == '\r') {
|
||||
if (i + 1 < length && input.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
sb.append(replacement);
|
||||
}
|
||||
else if (c == '\n') {
|
||||
sb.append(replacement);
|
||||
}
|
||||
else {
|
||||
sb.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof ServerSentEvent<?> that &&
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
@@ -44,6 +45,15 @@ class ServerSentEventTests {
|
||||
ServerSentEvent.<String>builder().event("first" + newLine + "second").build());
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
@MethodSource("newLineCharacters")
|
||||
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:");
|
||||
}
|
||||
|
||||
private static Stream<Arguments> newLineCharacters() {
|
||||
return Stream.of(
|
||||
Arguments.of("\n", "LF"),
|
||||
|
||||
+16
-5
@@ -224,7 +224,9 @@ public class SseEmitter extends ResponseBodyEmitter {
|
||||
|
||||
@Override
|
||||
public SseEventBuilder comment(String comment) {
|
||||
append(':').append(StringUtils.replace(comment, "\n", "\n:")).append('\n');
|
||||
append(':');
|
||||
appendEscaped(comment, "\n:");
|
||||
append('\n');
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -259,6 +261,16 @@ public class SseEmitter extends ResponseBodyEmitter {
|
||||
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
|
||||
this.dataToSend.add(new DataWithMediaType(input, mediaType));
|
||||
}
|
||||
else {
|
||||
appendEscaped(input, "\ndata:");
|
||||
saveAppendedText(mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendEscaped(String input, String replacement) {
|
||||
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
|
||||
append(input);
|
||||
}
|
||||
else {
|
||||
int length = input.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
@@ -267,16 +279,15 @@ public class SseEmitter extends ResponseBodyEmitter {
|
||||
if (i + 1 < length && input.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
this.sb.append("\ndata:");
|
||||
append(replacement);
|
||||
}
|
||||
else if (c == '\n') {
|
||||
this.sb.append("\ndata:");
|
||||
append(replacement);
|
||||
}
|
||||
else {
|
||||
this.sb.append(c);
|
||||
append(c);
|
||||
}
|
||||
}
|
||||
saveAppendedText(mediaType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
@@ -168,6 +168,17 @@ class SseEmitterTests {
|
||||
.send(event().name("first" + newLineChars + "second")));
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{1}")
|
||||
@MethodSource("newLineCharacters")
|
||||
void supportMultiLineComments(String newLineChars, String description) throws Exception {
|
||||
this.emitter.send(event().comment("foo" + newLineChars + "bar" + newLineChars + "baz").data("payload"));
|
||||
this.handler.assertSentObjectCount(3);
|
||||
this.handler.assertObject(0, ":foo\n:bar\n:baz\ndata:", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertObject(1, "payload");
|
||||
this.handler.assertObject(2, "\n\n", TEXT_PLAIN_UTF8);
|
||||
this.handler.assertWriteCount(1);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> newLineCharacters() {
|
||||
return Stream.of(
|
||||
Arguments.of("\n", "LF"),
|
||||
|
||||
Reference in New Issue
Block a user