From f9ae54d91e67ebf4ca27d5f0bcc3c39d8f66b8c1 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 14 Feb 2024 16:56:52 +0100 Subject: [PATCH] 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 --- .../web/servlet/function/ServerResponse.java | 11 ++++++- .../servlet/function/SseServerResponse.java | 32 +++++++++++-------- .../function/SseServerResponseTests.java | 24 +++++++++++++- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java index bcffc38199..790e37e8fb 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ServerResponse.java @@ -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 { */ 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 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java index 5bb7d569a6..76b7dd75de 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/SseServerResponse.java @@ -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 { 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") diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java index db76542ad9..fc7ea6f9f1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/SseServerResponseTests.java @@ -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; 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 { 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 {