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("/")