diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index 72e97939108..7dddb1f26ca 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -18,7 +18,9 @@ package org.springframework.web.server.adapter; import java.util.Arrays; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -32,6 +34,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.codec.LoggingCodecSupport; import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.http.codec.multipart.Part; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; @@ -249,6 +252,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa return getDelegate().handle(exchange) .doOnSuccess(aVoid -> logResponse(exchange)) .onErrorResume(ex -> handleUnresolvedError(exchange, ex)) + .then(cleanupMultipart(exchange)) .then(Mono.defer(response::setComplete)); } @@ -323,4 +327,23 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()); } + private Mono cleanupMultipart(ServerWebExchange exchange) { + return exchange.getMultipartData() + .onErrorResume(t -> Mono.empty()) // ignore errors reading multipart data + .flatMapIterable(Map::values) + .flatMapIterable(Function.identity()) + .flatMap(this::deletePart) + .then(); + } + + private Mono deletePart(Part part) { + return part.delete().onErrorResume(ex -> { + if (logger.isWarnEnabled()) { + logger.warn("Failed to perform cleanup of multipart items", ex); + } + return Mono.empty(); + }); + } + + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java index f433ccdfb40..419ae2ceb41 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/BodyExtractors.java @@ -137,6 +137,9 @@ public abstract class BodyExtractors { /** * Extractor to read multipart data into a {@code MultiValueMap}. + *

Note: that resources used for part handling, + * like storage for the uploaded files, is not deleted automatically, but + * should be done via {@link Part#delete()}. * @return {@code BodyExtractor} for multipart data */ // Parameterized for server-side use @@ -151,6 +154,9 @@ public abstract class BodyExtractors { /** * Extractor to read multipart data into {@code Flux}. + *

Note: that resources used for part handling, + * like storage for the uploaded files, is not deleted automatically, but + * should be done via {@link Part#delete()}. * @return {@code BodyExtractor} for multipart request parts */ // Parameterized for server-side use diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java index c73aa7dc3a2..1677ea9a04a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartIntegrationTests.java @@ -25,6 +25,7 @@ import java.util.Map; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -168,7 +169,9 @@ class MultipartIntegrationTests extends AbstractRouterFunctionIntegrationTests { assertThat(parts.size()).isEqualTo(2); assertThat(((FilePart) parts.get("fooPart")).filename()).isEqualTo("foo.txt"); assertThat(((FormFieldPart) parts.get("barPart")).value()).isEqualTo("bar"); - return ServerResponse.ok().build(); + return Flux.fromIterable(parts.values()) + .concatMap(Part::delete) + .then(ServerResponse.ok().build()); } catch(Exception e) { return Mono.error(e); @@ -183,7 +186,9 @@ class MultipartIntegrationTests extends AbstractRouterFunctionIntegrationTests { assertThat(parts.size()).isEqualTo(2); assertThat(((FilePart) parts.get(0)).filename()).isEqualTo("foo.txt"); assertThat(((FormFieldPart) parts.get(1)).value()).isEqualTo("bar"); - return ServerResponse.ok().build(); + return Flux.fromIterable(parts) + .concatMap(Part::delete) + .then(ServerResponse.ok().build()); } catch(Exception e) { return Mono.error(e);