From 4114532b0bd1ad191bcc327aa568cc92f1b01589 Mon Sep 17 00:00:00 2001 From: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:55:23 +0200 Subject: [PATCH] Ignore invalid SSE retry field Per the SSE specification, a "retry" field whose value is not made up solely of ASCII digits must be ignored. ServerSentEventHttpMessageReader passed the value straight to Long.parseLong, so "retry:none", an empty "retry:", or a value too large for a long raised NumberFormatException and terminated the event stream. A client cannot control what a server sends, so an unusable reconnection hint would kill an otherwise healthy subscription. Signed-off-by: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com> --- .../ServerSentEventHttpMessageReader.java | 31 ++++++++++++++++++- ...ServerSentEventHttpMessageReaderTests.java | 23 ++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java index 3966bcba7d70..1ca198f35098 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java +++ b/spring-web/src/main/java/org/springframework/http/codec/ServerSentEventHttpMessageReader.java @@ -163,7 +163,10 @@ else if (line.startsWith("event:")) { sseBuilder.event(line.substring(6).trim()); } else if (line.startsWith("retry:")) { - sseBuilder.retry(Duration.ofMillis(Long.parseLong(line.substring(6).trim()))); + Long retry = parseRetry(line.substring(6).trim()); + if (retry != null) { + sseBuilder.retry(Duration.ofMillis(retry)); + } } else if (line.startsWith(":")) { comment = (comment != null ? comment : new StringBuilder()); @@ -188,6 +191,32 @@ else if (line.startsWith(":")) { } } + /** + * Parse the value of a {@code retry} field, which is to be ignored unless it + * consists solely of ASCII digits and fits into a {@code long}. + * @return the reconnection time in milliseconds, or {@code null} to ignore the field + * @see + * HTML Living Standard: interpreting an event stream + */ + private static @Nullable Long parseRetry(String value) { + if (value.isEmpty()) { + return null; + } + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (ch < '0' || ch > '9') { + return null; + } + } + try { + return Long.parseLong(value); + } + catch (NumberFormatException ex) { + // Too large for a long: ignore the field rather than failing the stream. + return null; + } + } + private @Nullable Object decodeData(StringBuilder data, ResolvableType dataType, Map hints) { if (String.class == dataType.resolve()) { return data.substring(0, data.length() - 1); diff --git a/spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageReaderTests.java index 3693bed3755d..7c704e5cf842 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/ServerSentEventHttpMessageReaderTests.java @@ -137,6 +137,29 @@ void trimWhitespace() { .verify(); } + @Test + @SuppressWarnings("rawtypes") + void ignoreInvalidRetry() { + MockServerHttpRequest request = MockServerHttpRequest.post("/") + .body(Mono.just(stringBuffer( + "retry:none\ndata:foo\n\n" + + "retry:\ndata:bar\n\n" + + "retry:-1\ndata:baz\n\n" + + "retry:99999999999999999999\ndata:qux\n\n"))); + + Flux events = this.reader + .read(ResolvableType.forClassWithGenerics(ServerSentEvent.class, String.class), + request, Collections.emptyMap()).cast(ServerSentEvent.class); + + StepVerifier.create(events) + .expectNext(ServerSentEvent.builder().data("foo").build()) + .expectNext(ServerSentEvent.builder().data("bar").build()) + .expectNext(ServerSentEvent.builder().data("baz").build()) + .expectNext(ServerSentEvent.builder().data("qux").build()) + .expectComplete() + .verify(); + } + @Test // gh-35412 void emptyLines() { MockServerHttpRequest request = MockServerHttpRequest.post("/")