Remove Undertow-specific support and testing

Undertow does not support Servlet 6.1, we need to remove compatibility
tests as well as Undertow-specific classes for WebSocket and reactive
support.

Closes gh-35354
This commit is contained in:
Brian Clozel
2025-08-20 10:32:33 +02:00
parent 887ef75700
commit fce7b3d420
45 changed files with 28 additions and 2512 deletions
@@ -37,9 +37,8 @@ import org.springframework.util.Assert;
* event-listener read APIs and Reactive Streams.
*
* <p>Specifically a base class for reading from the HTTP request body with
* Servlet non-blocking I/O and Undertow XNIO as well as handling incoming
* WebSocket messages with standard Jakarta WebSocket (JSR-356), Jetty, and
* Undertow.
* Servlet non-blocking I/O as well as handling incoming
* WebSocket messages with standard Jakarta WebSocket (JSR-356), and Jetty.
*
* @author Arjen Poutsma
* @author Violeta Georgieva
@@ -34,8 +34,8 @@ import org.springframework.util.StringUtils;
* event-listener write APIs and Reactive Streams.
*
* <p>Specifically a base class for writing to the HTTP response body with
* Servlet non-blocking I/O and Undertow XNIO as well for writing WebSocket
* messages through the Jakarta WebSocket API (JSR-356), Jetty, and Undertow.
* Servlet non-blocking I/O as well for writing WebSocket
* messages through the Jakarta WebSocket API (JSR-356), and Jetty.
*
* @author Arjen Poutsma
* @author Violeta Georgieva
@@ -1,272 +0,0 @@
/*
* 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.http.server.reactive;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import io.undertow.util.HeaderMap;
import io.undertow.util.HeaderValues;
import io.undertow.util.HttpString;
import org.jspecify.annotations.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
/**
* {@code MultiValueMap} implementation for wrapping Undertow HTTP headers.
*
* @author Brian Clozel
* @author Sam Brannen
* @since 5.1.1
*/
class UndertowHeadersAdapter implements MultiValueMap<String, String> {
private final HeaderMap headers;
UndertowHeadersAdapter(HeaderMap headers) {
this.headers = headers;
}
@Override
public String getFirst(String key) {
return this.headers.getFirst(key);
}
@Override
public void add(String key, @Nullable String value) {
this.headers.add(HttpString.tryFromString(key), value);
}
@Override
@SuppressWarnings("unchecked")
public void addAll(String key, List<? extends String> values) {
this.headers.addAll(HttpString.tryFromString(key), (List<String>) values);
}
@Override
public void addAll(MultiValueMap<String, String> values) {
values.forEach((key, list) -> this.headers.addAll(HttpString.tryFromString(key), list));
}
@Override
public void set(String key, @Nullable String value) {
this.headers.put(HttpString.tryFromString(key), value);
}
@Override
public void setAll(Map<String, String> values) {
values.forEach((key, list) -> this.headers.put(HttpString.tryFromString(key), list));
}
@Override
public Map<String, String> toSingleValueMap() {
Map<String, String> singleValueMap = CollectionUtils.newLinkedHashMap(this.headers.size());
this.headers.forEach(values ->
singleValueMap.put(values.getHeaderName().toString(), values.getFirst()));
return singleValueMap;
}
@Override
public int size() {
return this.headers.size();
}
@Override
public boolean isEmpty() {
return (this.headers.size() == 0);
}
@Override
public boolean containsKey(Object key) {
return (key instanceof String headerName && this.headers.contains(headerName));
}
@Override
public boolean containsValue(Object value) {
return (value instanceof String &&
this.headers.getHeaderNames().stream()
.map(this.headers::get)
.anyMatch(values -> values.contains(value)));
}
@Override
public @Nullable List<String> get(Object key) {
return (key instanceof String headerName ? this.headers.get(headerName) : null);
}
@Override
public @Nullable List<String> put(String key, List<String> value) {
HeaderValues previousValues = this.headers.get(key);
this.headers.putAll(HttpString.tryFromString(key), value);
return previousValues;
}
@Override
public @Nullable List<String> remove(Object key) {
if (key instanceof String headerName) {
Collection<String> removed = this.headers.remove(headerName);
if (removed != null) {
return new ArrayList<>(removed);
}
}
return null;
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
map.forEach((key, values) ->
this.headers.putAll(HttpString.tryFromString(key), values));
}
@Override
public void clear() {
this.headers.clear();
}
@Override
public Set<String> keySet() {
return new HeaderNames();
}
@Override
public Collection<List<String>> values() {
return this.headers.getHeaderNames().stream()
.map(this.headers::get)
.collect(Collectors.toList());
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
return new AbstractSet<>() {
@Override
public Iterator<Entry<String, List<String>>> iterator() {
return new EntryIterator();
}
@Override
public int size() {
return headers.size();
}
};
}
@Override
public String toString() {
return org.springframework.http.HttpHeaders.formatHeaders(this);
}
private class EntryIterator implements Iterator<Entry<String, List<String>>> {
private final Iterator<HttpString> names = headers.getHeaderNames().iterator();
@Override
public boolean hasNext() {
return this.names.hasNext();
}
@Override
public Entry<String, List<String>> next() {
return new HeaderEntry(this.names.next());
}
}
private class HeaderEntry implements Entry<String, List<String>> {
private final HttpString key;
HeaderEntry(HttpString key) {
this.key = key;
}
@Override
public String getKey() {
return this.key.toString();
}
@Override
public List<String> getValue() {
return headers.get(this.key);
}
@Override
public List<String> setValue(List<String> value) {
List<String> previousValues = headers.get(this.key);
headers.putAll(this.key, value);
return previousValues;
}
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(headers.getHeaderNames().iterator());
}
@Override
public int size() {
return headers.getHeaderNames().size();
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<HttpString> iterator;
private @Nullable String currentName;
private HeaderNamesIterator(Iterator<HttpString> iterator) {
this.iterator = iterator;
}
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public String next() {
this.currentName = this.iterator.next().toString();
return this.currentName;
}
@Override
public void remove() {
if (this.currentName == null) {
throw new IllegalStateException("No current Header in iterator");
}
if (!headers.contains(this.currentName)) {
throw new IllegalStateException("Header not present: " + this.currentName);
}
headers.remove(this.currentName);
}
}
}
@@ -1,141 +0,0 @@
/*
* 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.http.server.reactive;
import java.io.IOException;
import java.net.URISyntaxException;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.logging.Log;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpLogging;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
* Adapt {@link HttpHandler} to the Undertow {@link io.undertow.server.HttpHandler}.
*
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @since 5.0
*/
public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler {
private static final Log logger = HttpLogging.forLogName(UndertowHttpHandlerAdapter.class);
private final HttpHandler httpHandler;
private DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
public UndertowHttpHandlerAdapter(HttpHandler httpHandler) {
Assert.notNull(httpHandler, "HttpHandler must not be null");
this.httpHandler = httpHandler;
}
public void setDataBufferFactory(DataBufferFactory bufferFactory) {
Assert.notNull(bufferFactory, "DataBufferFactory must not be null");
this.bufferFactory = bufferFactory;
}
public DataBufferFactory getDataBufferFactory() {
return this.bufferFactory;
}
@Override
public void handleRequest(HttpServerExchange exchange) {
exchange.dispatch(() -> {
UndertowServerHttpRequest request = null;
try {
request = new UndertowServerHttpRequest(exchange, getDataBufferFactory());
}
catch (URISyntaxException ex) {
if (logger.isWarnEnabled()) {
logger.debug("Failed to get request URI: " + ex.getMessage());
}
exchange.setStatusCode(400);
return;
}
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, getDataBufferFactory(), request);
if (request.getMethod() == HttpMethod.HEAD) {
response = new HttpHeadResponseDecorator(response);
}
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(exchange, request);
this.httpHandler.handle(request, response).subscribe(resultSubscriber);
});
}
private static class HandlerResultSubscriber implements Subscriber<Void> {
private final HttpServerExchange exchange;
private final String logPrefix;
public HandlerResultSubscriber(HttpServerExchange exchange, UndertowServerHttpRequest request) {
this.exchange = exchange;
this.logPrefix = request.getLogPrefix();
}
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Void aVoid) {
// no-op
}
@Override
public void onError(Throwable ex) {
logger.trace(this.logPrefix + "Failed to complete: " + ex.getMessage());
if (this.exchange.isResponseStarted()) {
try {
logger.debug(this.logPrefix + "Closing connection");
this.exchange.getConnection().close();
}
catch (IOException ex2) {
// ignore
}
}
else {
logger.debug(this.logPrefix + "Setting HttpServerExchange status to 500 Server Error");
this.exchange.setStatusCode(500);
this.exchange.endExchange();
}
}
@Override
public void onComplete() {
logger.trace(this.logPrefix + "Handling completed");
this.exchange.endExchange();
}
}
}
@@ -1,197 +0,0 @@
/*
* 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.http.server.reactive;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicLong;
import javax.net.ssl.SSLSession;
import io.undertow.connector.ByteBufferPool;
import io.undertow.connector.PooledByteBuffer;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import org.jspecify.annotations.Nullable;
import org.xnio.channels.StreamSourceChannel;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Adapt {@link ServerHttpRequest} to the Undertow {@link HttpServerExchange}.
*
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0
*/
class UndertowServerHttpRequest extends AbstractServerHttpRequest {
private static final AtomicLong logPrefixIndex = new AtomicLong();
private final HttpServerExchange exchange;
private final RequestBodyPublisher body;
public UndertowServerHttpRequest(HttpServerExchange exchange, DataBufferFactory bufferFactory)
throws URISyntaxException {
super(HttpMethod.valueOf(exchange.getRequestMethod().toString()), initUri(exchange), "",
new HttpHeaders(new UndertowHeadersAdapter(exchange.getRequestHeaders())));
this.exchange = exchange;
this.body = new RequestBodyPublisher(exchange, bufferFactory);
this.body.registerListeners(exchange);
}
private static URI initUri(HttpServerExchange exchange) throws URISyntaxException {
Assert.notNull(exchange, "HttpServerExchange is required");
String requestURL = exchange.getRequestURL();
String query = exchange.getQueryString();
String requestUriAndQuery = (StringUtils.hasLength(query) ? requestURL + "?" + query : requestURL);
return new URI(requestUriAndQuery);
}
@Override
protected MultiValueMap<String, HttpCookie> initCookies() {
MultiValueMap<String, HttpCookie> cookies = new LinkedMultiValueMap<>();
for (Cookie cookie : this.exchange.requestCookies()) {
HttpCookie httpCookie = new HttpCookie(cookie.getName(), cookie.getValue());
cookies.add(cookie.getName(), httpCookie);
}
return cookies;
}
@Override
public @Nullable InetSocketAddress getLocalAddress() {
return this.exchange.getDestinationAddress();
}
@Override
public @Nullable InetSocketAddress getRemoteAddress() {
return this.exchange.getSourceAddress();
}
@Override
protected @Nullable SslInfo initSslInfo() {
SSLSession session = this.exchange.getConnection().getSslSession();
if (session != null) {
return new DefaultSslInfo(session);
}
return null;
}
@Override
public Flux<DataBuffer> getBody() {
return Flux.from(this.body);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getNativeRequest() {
return (T) this.exchange;
}
@Override
protected String initId() {
return ObjectUtils.getIdentityHexString(this.exchange.getConnection()) +
"-" + logPrefixIndex.incrementAndGet();
}
private class RequestBodyPublisher extends AbstractListenerReadPublisher<DataBuffer> {
private final StreamSourceChannel channel;
private final DataBufferFactory bufferFactory;
private final ByteBufferPool byteBufferPool;
public RequestBodyPublisher(HttpServerExchange exchange, DataBufferFactory bufferFactory) {
super(UndertowServerHttpRequest.this.getLogPrefix());
this.channel = exchange.getRequestChannel();
this.bufferFactory = bufferFactory;
this.byteBufferPool = exchange.getConnection().getByteBufferPool();
}
private void registerListeners(HttpServerExchange exchange) {
exchange.addExchangeCompleteListener((ex, next) -> {
onAllDataRead();
next.proceed();
});
this.channel.getReadSetter().set(c -> onDataAvailable());
this.channel.getCloseSetter().set(c -> onAllDataRead());
this.channel.resumeReads();
}
@Override
protected void checkOnDataAvailable() {
this.channel.resumeReads();
// We are allowed to try, it will return null if data is not available
onDataAvailable();
}
@Override
protected void readingPaused() {
this.channel.suspendReads();
}
@Override
protected @Nullable DataBuffer read() throws IOException {
PooledByteBuffer pooledByteBuffer = this.byteBufferPool.allocate();
try (pooledByteBuffer) {
ByteBuffer byteBuffer = pooledByteBuffer.getBuffer();
int read = this.channel.read(byteBuffer);
if (rsReadLogger.isTraceEnabled()) {
rsReadLogger.trace(getLogPrefix() + "Read " + read + (read != -1 ? " bytes" : ""));
}
if (read > 0) {
byteBuffer.flip();
DataBuffer dataBuffer = this.bufferFactory.allocateBuffer(read);
dataBuffer.write(byteBuffer);
return dataBuffer;
}
else if (read == -1) {
onAllDataRead();
}
return null;
}
}
@Override
protected void discardData() {
// Nothing to discard since we pass data buffers on immediately..
}
}
}
@@ -1,344 +0,0 @@
/*
* 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.http.server.reactive;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import org.jspecify.annotations.Nullable;
import org.reactivestreams.Processor;
import org.reactivestreams.Publisher;
import org.xnio.channels.StreamSinkChannel;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoSink;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.util.Assert;
/**
* Adapt {@link ServerHttpResponse} to the Undertow {@link HttpServerExchange}.
*
* @author Marek Hawrylczak
* @author Rossen Stoyanchev
* @author Arjen Poutsma
* @author Juergen Hoeller
* @since 5.0
*/
class UndertowServerHttpResponse extends AbstractListenerServerHttpResponse implements ZeroCopyHttpOutputMessage {
private final HttpServerExchange exchange;
private final UndertowServerHttpRequest request;
private @Nullable StreamSinkChannel responseChannel;
UndertowServerHttpResponse(
HttpServerExchange exchange, DataBufferFactory bufferFactory, UndertowServerHttpRequest request) {
super(bufferFactory, createHeaders(exchange));
this.exchange = exchange;
this.request = request;
}
private static HttpHeaders createHeaders(HttpServerExchange exchange) {
Assert.notNull(exchange, "HttpServerExchange must not be null");
UndertowHeadersAdapter headersMap = new UndertowHeadersAdapter(exchange.getResponseHeaders());
return new HttpHeaders(headersMap);
}
@SuppressWarnings("unchecked")
@Override
public <T> T getNativeResponse() {
return (T) this.exchange;
}
@Override
public HttpStatusCode getStatusCode() {
HttpStatusCode status = super.getStatusCode();
return (status != null ? status : HttpStatusCode.valueOf(this.exchange.getStatusCode()));
}
@Override
protected void applyStatusCode() {
HttpStatusCode status = super.getStatusCode();
if (status != null) {
this.exchange.setStatusCode(status.value());
}
}
@Override
protected void applyHeaders() {
}
@Override
protected void applyCookies() {
for (String name : getCookies().keySet()) {
for (ResponseCookie httpCookie : getCookies().get(name)) {
Cookie cookie = new CookieImpl(name, httpCookie.getValue());
if (!httpCookie.getMaxAge().isNegative()) {
cookie.setMaxAge((int) httpCookie.getMaxAge().getSeconds());
}
if (httpCookie.getDomain() != null) {
cookie.setDomain(httpCookie.getDomain());
}
if (httpCookie.getPath() != null) {
cookie.setPath(httpCookie.getPath());
}
cookie.setSecure(httpCookie.isSecure());
cookie.setHttpOnly(httpCookie.isHttpOnly());
// TODO: add "Partitioned" attribute when Undertow supports it
cookie.setSameSiteMode(httpCookie.getSameSite());
this.exchange.setResponseCookie(cookie);
}
}
}
@Override
public Mono<Void> writeWith(Path file, long position, long count) {
return doCommit(() ->
Mono.create(sink -> {
try {
FileChannel source = FileChannel.open(file, StandardOpenOption.READ);
TransferBodyListener listener = new TransferBodyListener(source, position, count, sink);
sink.onDispose(listener::closeSource);
StreamSinkChannel destination = this.exchange.getResponseChannel();
destination.getWriteSetter().set(listener::transfer);
listener.transfer(destination);
}
catch (IOException ex) {
sink.error(ex);
}
}));
}
@Override
protected Processor<? super Publisher<? extends DataBuffer>, Void> createBodyFlushProcessor() {
return new ResponseBodyFlushProcessor();
}
private ResponseBodyProcessor createBodyProcessor() {
if (this.responseChannel == null) {
this.responseChannel = this.exchange.getResponseChannel();
}
return new ResponseBodyProcessor(this.responseChannel);
}
private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
private final StreamSinkChannel channel;
private volatile @Nullable ByteBuffer byteBuffer;
/** Keep track of write listener calls, for {@link #writePossible}. */
private volatile boolean writePossible;
public ResponseBodyProcessor(StreamSinkChannel channel) {
super(request.getLogPrefix());
Assert.notNull(channel, "StreamSinkChannel must not be null");
this.channel = channel;
this.channel.getWriteSetter().set(c -> {
this.writePossible = true;
onWritePossible();
});
this.channel.suspendWrites();
}
@Override
protected boolean isWritePossible() {
this.channel.resumeWrites();
return this.writePossible;
}
@Override
protected boolean write(DataBuffer dataBuffer) throws IOException {
ByteBuffer buffer = this.byteBuffer;
if (buffer == null) {
return false;
}
// Track write listener calls from here on.
this.writePossible = false;
// In case of IOException, onError handling should call discardData(DataBuffer)..
int total = buffer.remaining();
int written = writeByteBuffer(buffer);
if (rsWriteLogger.isTraceEnabled()) {
rsWriteLogger.trace(getLogPrefix() + "Wrote " + written + " of " + total + " bytes");
}
if (written != total) {
return false;
}
// We wrote all, so can still write more.
this.writePossible = true;
DataBufferUtils.release(dataBuffer);
this.byteBuffer = null;
return true;
}
private int writeByteBuffer(ByteBuffer byteBuffer) throws IOException {
int written;
int totalWritten = 0;
do {
written = this.channel.write(byteBuffer);
totalWritten += written;
}
while (byteBuffer.hasRemaining() && written > 0);
return totalWritten;
}
@Override
protected void dataReceived(DataBuffer dataBuffer) {
super.dataReceived(dataBuffer);
ByteBuffer byteBuffer = ByteBuffer.allocate(dataBuffer.readableByteCount());
dataBuffer.toByteBuffer(byteBuffer);
this.byteBuffer = byteBuffer;
}
@Override
protected boolean isDataEmpty(DataBuffer dataBuffer) {
return (dataBuffer.readableByteCount() == 0);
}
@Override
protected void writingComplete() {
this.channel.getWriteSetter().set(null);
this.channel.resumeWrites();
}
@Override
protected void writingFailed(Throwable ex) {
cancel();
onError(ex);
}
@Override
protected void discardData(DataBuffer dataBuffer) {
DataBufferUtils.release(dataBuffer);
}
}
private class ResponseBodyFlushProcessor extends AbstractListenerWriteFlushProcessor<DataBuffer> {
public ResponseBodyFlushProcessor() {
super(request.getLogPrefix());
}
@Override
protected Processor<? super DataBuffer, Void> createWriteProcessor() {
return UndertowServerHttpResponse.this.createBodyProcessor();
}
@Override
protected void flush() throws IOException {
StreamSinkChannel channel = UndertowServerHttpResponse.this.responseChannel;
if (channel != null) {
if (rsWriteFlushLogger.isTraceEnabled()) {
rsWriteFlushLogger.trace(getLogPrefix() + "flush");
}
channel.flush();
}
}
@Override
protected boolean isWritePossible() {
StreamSinkChannel channel = UndertowServerHttpResponse.this.responseChannel;
if (channel != null) {
// We can always call flush, just ensure writes are on.
channel.resumeWrites();
return true;
}
return false;
}
@Override
protected boolean isFlushPending() {
return false;
}
}
private static class TransferBodyListener {
private final FileChannel source;
private final MonoSink<Void> sink;
private long position;
private long count;
public TransferBodyListener(FileChannel source, long position, long count, MonoSink<Void> sink) {
this.source = source;
this.sink = sink;
this.position = position;
this.count = count;
}
public void transfer(StreamSinkChannel destination) {
try {
while (this.count > 0) {
long len = destination.transferFrom(this.source, this.position, this.count);
if (len != 0) {
this.position += len;
this.count -= len;
}
else {
destination.resumeWrites();
return;
}
}
this.sink.success();
}
catch (IOException ex) {
this.sink.error(ex);
}
}
public void closeSource() {
try {
this.source.close();
}
catch (IOException ignore) {
}
}
}
}
@@ -5,7 +5,7 @@
* {@link org.springframework.http.server.reactive.HttpHandler} for processing.
*
* <p>Also provides implementations adapting to different runtimes
* including Servlet containers, Netty + Reactor IO, and Undertow.
* including Servlet containers and Netty + Reactor IO.
*/
@NullMarked
package org.springframework.http.server.reactive;
@@ -266,7 +266,7 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
if (dest.isAbsolute() && !dest.exists()) {
// Servlet Part.write is not guaranteed to support absolute file paths:
// may translate the given path to a relative location within a temp dir
// (for example, on Jetty whereas Tomcat and Undertow detect absolute paths).
// (for example, on Jetty whereas Tomcat detects absolute paths).
// At least we offloaded the file from memory storage; it'll get deleted
// from the temp dir eventually in any case. And for our user's purposes,
// we can manually copy it to the requested location as a fallback.