Merge branch '7.0.x'

This commit is contained in:
Brian Clozel
2026-06-03 11:23:03 +02:00
4 changed files with 64 additions and 7 deletions
@@ -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"),
@@ -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);
}
}
@@ -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"),