Merge commit 'v7.1.0-M1~1'

This commit is contained in:
Brian Clozel
2026-08-20 18:18:13 +02:00
54 changed files with 1319 additions and 316 deletions
@@ -181,6 +181,15 @@ class ContentDispositionTests {
assertThat(cd.getFilename()).isEqualTo("foo\\bar \"baz\" qux \\\" quux.txt");
}
@Test
void parseBackslashInName() {
String s = "form-data; name=\"foo\\\"bar\"; filename=\"foo.txt\"";
ContentDisposition cd = ContentDisposition.parse(s);
assertThat(cd.getName()).isEqualTo("foo\"bar");
assertThat(cd.getFilename()).isEqualTo("foo.txt");
assertThat(cd.toString()).isEqualTo(s);
}
@Test
void parseBackslashInLastPosition() {
ContentDisposition cd = ContentDisposition.parse("form-data; name=\"foo\"; filename=\"bar\\\"");
@@ -255,6 +255,21 @@ class PartEventHttpMessageReaderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
void partSizeTooLargeWithUnlimitedMemorySize() {
MockServerHttpRequest request = createRequest("simple.multipart", "simple-boundary");
PartEventHttpMessageReader reader = new PartEventHttpMessageReader();
reader.setMaxPartSize(10);
reader.setMaxInMemorySize(-1);
Flux<PartEvent> result = reader.read(forClass(PartEvent.class), request, emptyMap());
StepVerifier.create(result)
.expectError(DataBufferLimitException.class)
.verify();
}
@Test
void formPartTooLarge() {
MockServerHttpRequest request = createRequest(
@@ -35,7 +35,9 @@ import reactor.test.StepVerifier;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.Hints;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
@@ -92,7 +94,7 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
@Test
void splitOneBranches() {
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_ROOT), null, null, HINTS);
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), null);
StepVerifier.create(result)
.consumeNextWith(events -> {
@@ -113,8 +115,7 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
@Test
void splitMultipleBranches() {
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(toDataBufferMono(POJO_CHILD), null, null, HINTS);
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")));
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), null);
StepVerifier.create(result)
.consumeNextWith(events -> {
@@ -143,6 +144,34 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
void splitMultipleBranchesLimitExceeded() {
Flux<String> source = Flux.just(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>",
"<root><pojo><foo>foo</foo></pojo>",
"<pojo><foo>foofoo</foo>",
"<bar>barbar</bar></pojo>",
"<root/>");
XmlEventDecoder.ReceivedByteTracker byteTracker = new XmlEventDecoder.ReceivedByteTracker(30);
Map<String, Object> hints = Hints.from(XmlEventDecoder.BYTE_TRACKER_HINT, byteTracker);
Flux<XMLEvent> xmlEvents = this.xmlEventDecoder.decode(source.map(this::toToDataBuffer), null, null, hints);
Flux<List<XMLEvent>> result = Jaxb2Helper.split(xmlEvents, Set.of(new QName("pojo")), byteTracker);
StepVerifier.create(result)
.consumeNextWith(events -> {
assertThat(events).hasSize(5);
assertStartElement(events.get(0), "pojo");
assertStartElement(events.get(1), "foo");
assertCharacters(events.get(2), "foo");
assertEndElement(events.get(3), "foo");
assertEndElement(events.get(4), "pojo");
})
.expectError(DataBufferLimitException.class)
.verify();
}
private static void assertStartElement(XMLEvent event, String expectedLocalName) {
assertThat(event.isStartElement()).isTrue();
assertThat(event.asStartElement().getName().getLocalPart()).isEqualTo(expectedLocalName);
@@ -263,13 +292,18 @@ class Jaxb2XmlDecoderTests extends AbstractLeakCheckingTests {
private Mono<DataBuffer> toDataBufferMono(String value) {
return Mono.defer(() -> {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
DataBuffer buffer = toToDataBuffer(value);
return Mono.just(buffer);
});
}
private DataBuffer toToDataBuffer(String value) {
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
@jakarta.xml.bind.annotation.XmlType
@XmlSeeAlso(Child.class)
public abstract static class Parent {
@@ -27,7 +27,6 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
import static org.assertj.core.api.Assertions.assertThat;
@@ -88,28 +87,6 @@ class XmlEventDecoderTests extends AbstractLeakCheckingTests {
.verify();
}
@Test
void toXMLEventsWithLimit() {
this.decoder.setMaxInMemorySize(6);
Flux<String> source = Flux.just(
"<pojo>", "<foo>", "foofoo", "</foo>", "<bar>", "barbarbar", "</bar>", "</pojo>");
Flux<XMLEvent> events = this.decoder.decode(
source.map(this::stringBuffer), null, null, Collections.emptyMap());
StepVerifier.create(events)
.consumeNextWith(e -> assertThat(e.isStartDocument()).isTrue())
.consumeNextWith(e -> assertStartElement(e, "pojo"))
.consumeNextWith(e -> assertStartElement(e, "foo"))
.consumeNextWith(e -> assertCharacters(e, "foofoo"))
.consumeNextWith(e -> assertEndElement(e, "foo"))
.consumeNextWith(e -> assertStartElement(e, "bar"))
.expectError(DataBufferLimitException.class)
.verify();
}
@Test
void decodeErrorAalto() {
Flux<DataBuffer> source = Flux.concat(
@@ -94,4 +94,25 @@ class EscapedErrorsTests {
assertThat(ageError2.getCode()).as("Age error 2 code not escaped").isEqualTo("AGE_NOT_32 <tag>");
}
@Test
void noArgFieldErrorAccessorsEscapeRejectedValueAndMessage() {
TestBean tb = new TestBean();
tb.setName("<script>alert(1)</script>");
Errors errors = new EscapedErrors(new BindException(tb, "tb"));
errors.rejectValue("name", "NAME_INVALID", null, "message: <tag>");
FieldError fieldError = errors.getFieldError();
assertThat(fieldError.getDefaultMessage()).as("No-arg getFieldError() message escaped")
.isEqualTo("message: &lt;tag&gt;");
assertThat(fieldError.getRejectedValue()).as("No-arg getFieldError() rejected value escaped")
.isEqualTo("&lt;script&gt;alert(1)&lt;/script&gt;");
FieldError fieldErrorInList = errors.getFieldErrors().get(0);
assertThat(fieldErrorInList.getDefaultMessage()).as("No-arg getFieldErrors() message escaped")
.isEqualTo("message: &lt;tag&gt;");
assertThat(fieldErrorInList.getRejectedValue()).as("No-arg getFieldErrors() rejected value escaped")
.isEqualTo("&lt;script&gt;alert(1)&lt;/script&gt;");
}
}
@@ -74,11 +74,16 @@ class UrlHandlerFilterTests {
@Test
void redirect() throws Exception {
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").redirect(status).build();
testRedirect("/**", "/path/123/", "/path/123");
testRedirect("/**", "//path/123/", "/path/123");
testRedirect("/**", "///path/123/", "///path/123");
}
String path = "/path/123";
MockHttpServletRequest request = new MockHttpServletRequest("GET", path + "/");
private void testRedirect(String pattern, String path, String location) throws Exception {
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler(pattern).redirect(status).build();
MockHttpServletRequest request = new MockHttpServletRequest("GET", path);
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
@@ -89,7 +94,7 @@ class UrlHandlerFilterTests {
assertThat(chain.getRequest()).isNull();
assertThat(response.getStatus()).isEqualTo(status.value());
assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(path + "?" + queryString);
assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(location + "?" + queryString);
assertThat(response.isCommitted()).isTrue();
}
@@ -17,12 +17,14 @@
package org.springframework.web.filter.reactive;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
@@ -59,20 +61,23 @@ class UrlHandlerFilterTests {
}
@Test
void redirect() {
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler("/path/*").redirect(status).build();
void redirect() throws URISyntaxException {
testRedirect("/**", new URI(null, null, "/path/123/", "foo=bar", null), "/path/123?foo=bar");
// no way to create java.net.URI with leading slashes
}
String path = "/path/123";
String queryString = "foo=bar";
MockServerHttpRequest original = MockServerHttpRequest.get(path + "/?" + queryString).build();
ServerWebExchange exchange = MockServerWebExchange.from(original);
private static void testRedirect(String pattern, URI uri, String location) {
HttpStatus status = HttpStatus.PERMANENT_REDIRECT;
UrlHandlerFilter filter = UrlHandlerFilter.trailingSlashHandler(pattern).redirect(status).build();
MockServerHttpRequest request = MockServerHttpRequest.method(HttpMethod.GET, uri).build();
ServerWebExchange exchange = MockServerWebExchange.from(request);
assertThatThrownBy(() -> invokeFilter(filter, exchange))
.hasMessageContaining("No argument value was captured");
assertThat(exchange.getResponse().getStatusCode()).isEqualTo(status);
assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create(path + "?" + queryString));
assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(URI.create(location));
}
@Test
@@ -0,0 +1,81 @@
/*
* Copyright 2002-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.util;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
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;
/**
* Tests for {@link SseUtils}.
* @author Brian Clozel
*/
class SseUtilsTests {
@Test
void appendFieldValueWithoutLineSeparatorAppendsAsIs() {
StringBuilder sb = new StringBuilder();
sb.append("data:");
SseUtils.appendFieldValue("data", "no newlines here", sb);
assertThat(sb).hasToString("data:no newlines here");
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void appendFieldValueReplacesLineSeparatorWithFieldPrefix(String newLine, String description) {
StringBuilder sb = new StringBuilder();
sb.append("data:");
SseUtils.appendFieldValue("data", "first" + newLine + "second", sb);
assertThat(sb).hasToString("data:first\ndata:second");
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void appendFieldValueUsesEmptyFieldForComments(String newLine, String description) {
StringBuilder sb = new StringBuilder();
sb.append(":");
SseUtils.appendFieldValue("", "first" + newLine + "second", sb);
assertThat(sb).hasToString(":first\n:second");
}
@Test
void assertNoLineSeparatorAcceptsPlainContent() {
SseUtils.assertNoLineSeparator("no newlines here");
}
@ParameterizedTest(name = "{1}")
@MethodSource("newLineCharacters")
void assertNoLineSeparatorRejectsLineSeparator(String newLine, String description) {
assertThatIllegalArgumentException().isThrownBy(() ->
SseUtils.assertNoLineSeparator("first" + newLine + "second"));
}
static Stream<Arguments> newLineCharacters() {
return Stream.of(
Arguments.of("\n", "LF"),
Arguments.of("\r", "CR"),
Arguments.of("\r\n", "CRLF")
);
}
}