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
@@ -172,17 +172,17 @@ public final class ContentDisposition {
sb.append(this.type);
}
if (this.name != null) {
sb.append("; name=\"").append(this.name).append('\"');
sb.append("; name=\"");
appendName(sb, this.name).append('\"');
}
if (this.filename != null) {
if (this.charset == null || StandardCharsets.US_ASCII.equals(this.charset)) {
sb.append("; filename=\"")
.append(encodeQuotedPairs(this.filename))
.append('\"');
sb.append("; filename=\"");
appendName(sb, this.filename).append('\"');
}
else {
sb.append("; filename=\"")
.append(transliterateToAscii(encodeQuotedPairs(this.filename)))
.append(transliterateToAscii(appendName(new StringBuilder(), this.filename).toString()))
.append("\"; filename*=")
.append(encodeRfc5987Filename(this.filename, this.charset));
}
@@ -253,7 +253,7 @@ public final class ContentDisposition {
part.substring(eqIndex + 2, part.length() - 1) :
part.substring(eqIndex + 1));
if (attribute.equals("name") ) {
name = value;
name = (value.indexOf('\\') != -1 ? decodeQuotedPairs(value) : value);
}
else if (attribute.equals("filename*") ) {
int idx1 = value.indexOf('\'');
@@ -503,19 +503,20 @@ public final class ContentDisposition {
return sb.toString();
}
private static String encodeQuotedPairs(String filename) {
if (filename.indexOf('"') == -1 && filename.indexOf('\\') == -1) {
return filename;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < filename.length() ; i++) {
char c = filename.charAt(i);
if (c == '"' || c == '\\') {
sb.append('\\');
private static StringBuilder appendName(StringBuilder buffer, String name) {
for (int i = 0; i < name.length() ; i++) {
char c = name.charAt(i);
// strip control characters
if (c <= 0x1F || c == 0x7F) {
continue;
}
sb.append(c);
// encode quoted pairs
if (c == '"' || c == '\\') {
buffer.append('\\');
}
buffer.append(c);
}
return sb.toString();
return buffer;
}
private static String decodeQuotedPairs(String filename) {
@@ -20,8 +20,8 @@ import java.time.Duration;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.util.SseUtils;
/**
* Representation for a Server-Sent Event for use with Spring's reactive Web support.
@@ -112,7 +112,7 @@ public final class ServerSentEvent<T> {
}
if (this.comment != null) {
sb.append(':');
appendEscaped(this.comment, "\n:", sb);
SseUtils.appendFieldValue("", this.comment, sb);
sb.append('\n');
}
if (this.data != null) {
@@ -125,30 +125,6 @@ public final class ServerSentEvent<T> {
sb.append(fieldName).append(':').append(fieldValue).append('\n');
}
private void appendEscaped(String input, String replacement, StringBuilder sb) {
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
sb.append(input);
}
else {
int length = input.length();
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == '\r') {
if (i + 1 < length && input.charAt(i + 1) == '\n') {
i++;
}
sb.append(replacement);
}
else if (c == '\n') {
sb.append(replacement);
}
else {
sb.append(c);
}
}
}
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ServerSentEvent<?> that &&
@@ -265,23 +241,18 @@ public final class ServerSentEvent<T> {
@Override
public Builder<T> id(String id) {
checkEvent(id);
SseUtils.assertNoLineSeparator(id);
this.id = id;
return this;
}
@Override
public Builder<T> event(String event) {
checkEvent(event);
SseUtils.assertNoLineSeparator(event);
this.event = event;
return this;
}
private static void checkEvent(String content) {
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
"illegal character '\\n' or '\\r' in event content");
}
@Override
public Builder<T> retry(Duration retry) {
this.retry = retry;
@@ -40,6 +40,7 @@ import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.util.SseUtils;
/**
* {@code HttpMessageWriter} for {@code "text/event-stream"} responses.
@@ -142,27 +143,7 @@ public class ServerSentEventHttpMessageWriter implements HttpMessageWriter<Objec
}
private void writeStringData(String input, StringBuilder sb) {
if (input.indexOf('\n') == -1 && input.indexOf('\r') == -1) {
sb.append(input);
}
else {
int length = input.length();
for (int i = 0; i < length; i++) {
char c = input.charAt(i);
if (c == '\r') {
if (i + 1 < length && input.charAt(i + 1) == '\n') {
i++;
}
sb.append("\ndata:");
}
else if (c == '\n') {
sb.append("\ndata:");
}
else {
sb.append(c);
}
}
}
SseUtils.appendFieldValue("data", input, sb);
sb.append("\n\n");
}
@@ -188,6 +188,9 @@ public class PartEventHttpMessageReader extends LoggingCodecSupport implements H
if (this.maxPartSize == -1) {
maxSize = this.maxInMemorySize;
}
else if (this.maxInMemorySize == -1) {
maxSize = (int) Math.min(Integer.MAX_VALUE, this.maxPartSize);
}
else {
// maxInMemorySize is an int, so we can safely cast the long result of Math.min
maxSize = (int) Math.min(this.maxInMemorySize, this.maxPartSize);
@@ -34,6 +34,7 @@ import org.jspecify.annotations.Nullable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.SynchronousSink;
import org.springframework.core.io.buffer.DataBufferLimitException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -141,8 +142,10 @@ abstract class Jaxb2Helper {
* </li>
* </ol>
*/
public static Flux<List<XMLEvent>> split(Flux<XMLEvent> xmlEventFlux, Set<QName> names) {
return xmlEventFlux.handle(new SplitHandler(names));
public static Flux<List<XMLEvent>> split(
Flux<XMLEvent> xmlEventFlux, Set<QName> names, XmlEventDecoder.@Nullable ReceivedByteTracker byteTracker) {
return xmlEventFlux.handle(new SplitHandler(names, byteTracker));
}
@@ -150,14 +153,17 @@ abstract class Jaxb2Helper {
private final Set<QName> names;
private final XmlEventDecoder.ReceivedByteTracker byteTracker;
private @Nullable List<XMLEvent> events;
private int elementDepth = 0;
private int barrier = Integer.MAX_VALUE;
public SplitHandler(Set<QName> names) {
public SplitHandler(Set<QName> names, XmlEventDecoder.@Nullable ReceivedByteTracker byteTracker) {
this.names = names;
this.byteTracker = (byteTracker != null ? byteTracker : XmlEventDecoder.ReceivedByteTracker.NO_OP);
}
@Override
@@ -179,11 +185,19 @@ abstract class Jaxb2Helper {
if (event.isEndElement()) {
this.elementDepth--;
if (this.elementDepth == this.barrier) {
this.barrier = Integer.MAX_VALUE;
Assert.state(this.events != null, "No XMLEvent List");
sink.next(this.events);
this.barrier = Integer.MAX_VALUE;
this.events = null;
}
}
if (this.events == null) {
this.byteTracker.reset();
}
else if (this.byteTracker.isMaxInMemorySizeExceeded()) {
throw new DataBufferLimitException(
"Exceeded limit on max bytes per XML node: " + this.byteTracker.getMaxInMemorySize());
}
}
}
@@ -145,12 +145,16 @@ public class Jaxb2XmlDecoder extends AbstractDecoder<Object> {
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
XmlEventDecoder.ReceivedByteTracker byteTracker =
new XmlEventDecoder.ReceivedByteTracker(this.maxInMemorySize);
Flux<XMLEvent> xmlEventFlux = this.xmlEventDecoder.decode(
inputStream, ResolvableType.forClass(XMLEvent.class), mimeType, hints);
inputStream, ResolvableType.forClass(XMLEvent.class), mimeType,
Hints.merge(hints, XmlEventDecoder.BYTE_TRACKER_HINT, byteTracker));
Class<?> outputClass = elementType.toClass();
Set<QName> typeNames = Jaxb2Helper.toQNames(outputClass);
Flux<List<XMLEvent>> splitEvents = Jaxb2Helper.split(xmlEventFlux, typeNames);
Flux<List<XMLEvent>> splitEvents = Jaxb2Helper.split(xmlEventFlux, typeNames, byteTracker);
return splitEvents.map(events -> {
Object value = unmarshal(events, outputClass);
@@ -84,6 +84,12 @@ import org.springframework.util.xml.StaxUtils;
*/
public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
/**
* Hint key for a {@link ReceivedByteTracker} instance that callers can use
* to track the number of received bytes.
*/
public static final String BYTE_TRACKER_HINT = XmlEventDecoder.class.getName() + ".byteTracker";
private static final XMLInputFactory inputFactory = StaxUtils.createDefensiveInputFactory();
private static final boolean AALTO_PRESENT = ClassUtils.isPresent(
@@ -100,10 +106,13 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
/**
* Set the max number of bytes that can be buffered by this decoder. This
* is either the size the entire input when decoding as a whole, or when
* using async parsing via Aalto XML, it is size one top-level XML tree.
* When the limit is exceeded, {@link DataBufferLimitException} is raised.
* Set the max number of bytes this decoder should buffer in memory resulting
* in a {@link DataBufferLimitException} when the limit is exceeded.
* <p>When joining all buffers and decoding as a whole, the limit is applied
* to the entire input.
* <p>>When using Aalto XML async parsing, the limit does not apply at the
* level of this decoder because the XML events parsed from each buffer are
* emitted immediately and the buffer is released.
* <p>By default this is set to 256K.
* @param byteCount the max number of bytes to buffer, or -1 for unlimited
* @since 5.1.11
@@ -126,7 +135,7 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
if (this.useAalto) {
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(this.maxInMemorySize);
AaltoDataBufferToXmlEvent mapper = new AaltoDataBufferToXmlEvent(hints);
return Flux.from(input)
.flatMapIterable(mapper)
.doFinally(signalType -> mapper.endOfInput());
@@ -155,7 +164,7 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
/*
* Separate static class to isolate Aalto dependency.
*/
private static class AaltoDataBufferToXmlEvent implements Function<DataBuffer, List<? extends XMLEvent>> {
private static final class AaltoDataBufferToXmlEvent implements Function<DataBuffer, List<? extends XMLEvent>> {
private static final AsyncXMLInputFactory inputFactory =
StaxUtils.createDefensiveInputFactory(InputFactoryImpl::new);
@@ -165,22 +174,19 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
private final XMLEventAllocator eventAllocator = EventAllocatorImpl.getDefaultInstance();
private final int maxInMemorySize;
@Nullable
private final ReceivedByteTracker byteTracker;
private int byteCount;
private int elementDepth;
public AaltoDataBufferToXmlEvent(int maxInMemorySize) {
this.maxInMemorySize = maxInMemorySize;
private AaltoDataBufferToXmlEvent(@Nullable Map<String, Object> hints) {
this.byteTracker = (hints != null ? (ReceivedByteTracker) hints.get(BYTE_TRACKER_HINT) : null);
}
@Override
public List<? extends XMLEvent> apply(DataBuffer dataBuffer) {
try {
increaseByteCount(dataBuffer);
if (this.byteTracker != null) {
this.byteTracker.incrementByteCount(dataBuffer);
}
AsyncByteBufferFeeder inputFeeder = this.streamReader.getInputFeeder();
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
while (iterator.hasNext()) {
@@ -199,12 +205,8 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
if (event.isEndDocument()) {
break;
}
checkDepthAndResetByteCount(event);
}
}
if (this.maxInMemorySize > 0 && this.byteCount > this.maxInMemorySize) {
raiseLimitException();
}
return events;
}
catch (XMLStreamException ex) {
@@ -215,40 +217,55 @@ public class XmlEventDecoder extends AbstractDecoder<XMLEvent> {
}
}
private void increaseByteCount(DataBuffer dataBuffer) {
if (this.maxInMemorySize > 0) {
if (dataBuffer.readableByteCount() > Integer.MAX_VALUE - this.byteCount) {
raiseLimitException();
}
else {
this.byteCount += dataBuffer.readableByteCount();
}
}
}
private void checkDepthAndResetByteCount(XMLEvent event) {
if (this.maxInMemorySize > 0) {
if (event.isStartElement()) {
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
this.elementDepth++;
}
else if (event.isEndElement()) {
this.elementDepth--;
this.byteCount = this.elementDepth == 1 ? 0 : this.byteCount;
}
}
}
private void raiseLimitException() {
throw new DataBufferLimitException(
"Exceeded limit on max bytes per XML top-level node: " + this.maxInMemorySize);
}
public void endOfInput() {
this.streamReader.getInputFeeder().endOfInput();
}
}
/**
* Callers of {@link XmlEventDecoder} that buffer emitted XML events at a
* higher level, can pass an instance of this tracker as an
* {@link XmlEventDecoder#BYTE_TRACKER_HINT} to monitor the total number of
* bytes received, and to reset periodically.
* <p>For use with Aalto XML async parsing only, in which case this decoder
* parses releases each buffer immediately.
*/
public static class ReceivedByteTracker {
/** An instance to use when there is no limit. */
public static final ReceivedByteTracker NO_OP = new ReceivedByteTracker(-1);
private final int maxInMemorySize;
private int byteCount;
public ReceivedByteTracker(int maxInMemorySize) {
this.maxInMemorySize = maxInMemorySize;
}
public int getMaxInMemorySize() {
return this.maxInMemorySize;
}
public boolean isMaxInMemorySizeExceeded() {
return (this.maxInMemorySize != -1 && this.byteCount > this.maxInMemorySize);
}
public void reset() {
this.byteCount = 0;
}
private void incrementByteCount(DataBuffer buffer) {
if (this.maxInMemorySize != -1) {
this.byteCount += buffer.readableByteCount();
}
}
@Override
public String toString() {
return this.byteCount + " bytes";
}
}
}
@@ -212,9 +212,8 @@ class JettyCoreServerHttpResponse extends AbstractServerHttpResponse implements
}
@Override
public @Nullable SameSite getSameSite() {
// Adding non-null return site breaks tests.
return null;
public SameSite getSameSite() {
return SameSite.from(this.responseCookie.getSameSite());
}
@Override
@@ -170,12 +170,12 @@ public class EscapedErrors implements Errors {
@Override
public List<FieldError> getFieldErrors() {
return this.source.getFieldErrors();
return escapeObjectErrors(this.source.getFieldErrors());
}
@Override
public @Nullable FieldError getFieldError() {
return this.source.getFieldError();
return escapeObjectError(this.source.getFieldError());
}
@Override
@@ -308,6 +308,9 @@ public final class UrlHandlerFilter extends OncePerRequestFilter {
throws IOException {
String location = trimTrailingSlash(request.getRequestURI());
if (location.length() > 2 && location.startsWith("//")) {
location = (location.charAt(2) != '/' ? location.substring(1) : location);
}
if (StringUtils.hasText(request.getQueryString())) {
location += "?" + request.getQueryString();
}
@@ -299,12 +299,14 @@ public final class UrlHandlerFilter implements WebFilter {
@Override
public Mono<Void> handleInternal(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String query = request.getURI().getRawQuery();
String location = trimTrailingSlash(request);
if (location.length() > 2 && location.startsWith("//")) {
location = (location.charAt(2) != '/' ? location.substring(1) : location);
}
String query = request.getURI().getRawQuery();
if (StringUtils.hasText(query)) {
location += "?" + query;
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(this.statusCode);
response.getHeaders().set(HttpHeaders.LOCATION, location);
@@ -0,0 +1,77 @@
/*
* 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 org.springframework.util.Assert;
/**
* Utility methods for writing content as
* <a href="https://html.spec.whatwg.org/multipage/server-sent-events.html">Server-Sent Events</a>,
* shared by the Servlet and Reactive SSE support.
*
* @author Brian Clozel
* @since 7.0.9
*/
public abstract class SseUtils {
/**
* Append {@code value} to {@code output}, replacing each line separator
* ({@code "\n"}, {@code "\r"}, or {@code "\r\n"}) it contains with a new
* {@code field} line (that is, {@code "\n" + field + ":"}). This keeps a
* multi-line field value from breaking out of the current SSE field when
* written on the wire.
* @param field the name of the SSE field that {@code value} belongs to
* (for example, {@code "data"}), or an empty string for a comment
* @param value the field value to escape and append
* @param output the {@code StringBuilder} to append the escaped value to
*/
public static void appendFieldValue(String field, String value, StringBuilder output) {
if (value.indexOf('\n') == -1 && value.indexOf('\r') == -1) {
output.append(value);
return;
}
String lineSeparatorReplacement = "\n" + field + ":";
int length = value.length();
for (int i = 0; i < length; i++) {
char c = value.charAt(i);
if (c == '\r') {
if (i + 1 < length && value.charAt(i + 1) == '\n') {
i++;
}
output.append(lineSeparatorReplacement);
}
else if (c == '\n') {
output.append(lineSeparatorReplacement);
}
else {
output.append(c);
}
}
}
/**
* Assert that the given single-line SSE field value, such as an
* {@code id} or {@code event} name, does not contain a line separator.
* @param content the field value to check
* @throws IllegalArgumentException if {@code content} contains {@code "\n"} or {@code "\r"}
*/
public static void assertNoLineSeparator(String content) {
Assert.isTrue(content.indexOf('\n') == -1 && content.indexOf('\r') == -1,
"illegal character '\\n' or '\\r' in event content");
}
}
@@ -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")
);
}
}