mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Escape SSE view fragments
Prior to this commit, the MVC and WebFlux view fragments rendering would only partially escape rendered view fragments before sending then as SSE events. This could in some cases break the SSE stream with invalid data. This commit ensures that the rendered views are properly escaped before they are sent as SSE events. Fixes gh-37061
This commit is contained in:
+25
-1
@@ -603,7 +603,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
|
||||
finally {
|
||||
DataBufferUtils.release(buffer);
|
||||
}
|
||||
text = text.replace("\n", "\ndata:");
|
||||
text = escapeSseFragment(text);
|
||||
return bufferFactory.wrap(text.getBytes(charset));
|
||||
});
|
||||
|
||||
@@ -614,6 +614,30 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
|
||||
byte[] bytes = text.getBytes(charset);
|
||||
return bufferFactory.wrap(bytes);
|
||||
}
|
||||
|
||||
private String escapeSseFragment(String content) {
|
||||
if (content.indexOf('\n') == -1 && content.indexOf('\r') == -1) {
|
||||
return content;
|
||||
}
|
||||
StringBuilder fragment = new StringBuilder();
|
||||
int length = content.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = content.charAt(i);
|
||||
if (c == '\r') {
|
||||
if (i + 1 < length && content.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
fragment.append("\ndata:");
|
||||
}
|
||||
else if (c == '\n') {
|
||||
fragment.append("\ndata:");
|
||||
}
|
||||
else {
|
||||
fragment.append(c);
|
||||
}
|
||||
}
|
||||
return fragment.toString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
@@ -119,6 +119,21 @@ class FragmentViewResolutionResultHandlerTests {
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeViewFragment() {
|
||||
Fragment fragment = Fragment.create("fragment1", Map.of("foo", "Foo\n and Bar"));
|
||||
testSse(Flux.just(fragment),
|
||||
on(Handler.class).resolveReturnType(Flux.class, Fragment.class),
|
||||
"""
|
||||
event:fragment1
|
||||
data:<p>
|
||||
data: Hello Foo
|
||||
data: and Bar
|
||||
data:</p>
|
||||
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void renderServerSentEventFragmentStream() {
|
||||
|
||||
|
||||
+21
-2
@@ -476,8 +476,27 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
|
||||
public byte[] getFragmentContent() {
|
||||
this.writer.flush();
|
||||
String content = this.outputStream.toString(this.charset);
|
||||
content = content.replace("\n", "\ndata:");
|
||||
return content.getBytes(this.charset);
|
||||
if (content.indexOf('\n') == -1 && content.indexOf('\r') == -1) {
|
||||
return content.getBytes(this.charset);
|
||||
}
|
||||
StringBuilder fragment = new StringBuilder();
|
||||
int length = content.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
char c = content.charAt(i);
|
||||
if (c == '\r') {
|
||||
if (i + 1 < length && content.charAt(i + 1) == '\n') {
|
||||
i++;
|
||||
}
|
||||
fragment.append("\ndata:");
|
||||
}
|
||||
else if (c == '\n') {
|
||||
fragment.append("\ndata:");
|
||||
}
|
||||
else {
|
||||
fragment.append(c);
|
||||
}
|
||||
}
|
||||
return fragment.toString().getBytes(this.charset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
@@ -146,6 +146,31 @@ class FragmentRenderingStreamTests {
|
||||
"""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void escapeViewFragment() throws Exception {
|
||||
MethodParameter type = on(TestController.class).resolveReturnType(SseEmitter.class);
|
||||
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
this.handler.handleReturnValue(emitter, type, new ModelAndViewContainer(), webRequest);
|
||||
|
||||
assertThat(this.request.isAsyncStarted()).isTrue();
|
||||
assertThat(this.response.getStatus()).isEqualTo(200);
|
||||
|
||||
ModelAndView mav1 = new ModelAndView("fragment1", Map.of("foo", "Foo\n and Bar"));
|
||||
|
||||
emitter.send(SseEmitter.event().data(mav1));
|
||||
|
||||
assertThat(this.response.getContentType()).isEqualTo("text/event-stream");
|
||||
assertThat(this.response.getContentAsString()).isEqualTo(("""
|
||||
event:fragment1
|
||||
data:<p>
|
||||
data: Hello Foo
|
||||
data: and Bar
|
||||
data:</p>
|
||||
|
||||
"""));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings({"unused", "DataFlowIssue"})
|
||||
private static class TestController {
|
||||
|
||||
Reference in New Issue
Block a user