Skip to content

Commit

Permalink
Clean multipart up only when data is read
Browse files Browse the repository at this point in the history
This commit ensures that any storage used for multipart handling only
gets cleaned up if multipart data is actually retrieved via
ServerWebExchange::getMultipartData.

Closes spring-projectsgh-30590
  • Loading branch information
poutsma authored and mdeinum committed Jun 29, 2023
1 parent 63ba534 commit 4c2b65e
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 14 deletions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -141,6 +141,13 @@ default <T> T getAttributeOrDefault(String name, T defaultValue) {
*/
Mono<MultiValueMap<String, Part>> getMultipartData();

/**
* Cleans up any storage used for multipart handling.
* @since 6.0.10
* @see Part#delete()
*/
Mono<Void> cleanupMultipart();

/**
* Return the {@link LocaleContext} using the configured
* {@link org.springframework.web.server.i18n.LocaleContextResolver}.
Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -108,6 +108,11 @@ public Mono<MultiValueMap<String, Part>> getMultipartData() {
return getDelegate().getMultipartData();
}

@Override
public Mono<Void> cleanupMultipart() {
return getDelegate().cleanupMultipart();
}

@Override
public boolean isNotModified() {
return getDelegate().isNotModified();
Expand Down
Expand Up @@ -93,6 +93,8 @@ public class DefaultServerWebExchange implements ServerWebExchange {

private final Mono<MultiValueMap<String, Part>> multipartDataMono;

private volatile boolean multipartRead = false;

@Nullable
private final ApplicationContext applicationContext;

Expand Down Expand Up @@ -131,7 +133,7 @@ public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse re
this.sessionMono = sessionManager.getSession(this).cache();
this.localeContextResolver = localeContextResolver;
this.formDataMono = initFormData(request, codecConfigurer, getLogPrefix());
this.multipartDataMono = initMultipartData(request, codecConfigurer, getLogPrefix());
this.multipartDataMono = initMultipartData(codecConfigurer, getLogPrefix());
this.applicationContext = applicationContext;
}

Expand All @@ -154,10 +156,9 @@ private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpReques
.cache();
}

private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpRequest request,
ServerCodecConfigurer configurer, String logPrefix) {
private Mono<MultiValueMap<String, Part>> initMultipartData(ServerCodecConfigurer configurer, String logPrefix) {

MediaType contentType = getContentType(request);
MediaType contentType = getContentType(this.request);
if (contentType == null || !contentType.getType().equalsIgnoreCase("multipart")) {
return EMPTY_MULTIPART_DATA;
}
Expand All @@ -168,7 +169,8 @@ private static Mono<MultiValueMap<String, Part>> initMultipartData(ServerHttpReq
}

return reader
.readMono(MULTIPART_DATA_TYPE, request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.readMono(MULTIPART_DATA_TYPE, this.request, Hints.from(Hints.LOG_PREFIX_HINT, logPrefix))
.doOnNext(ignored -> this.multipartRead = true)
.switchIfEmpty(EMPTY_MULTIPART_DATA)
.cache();
}
Expand Down Expand Up @@ -243,6 +245,22 @@ public Mono<MultiValueMap<String, Part>> getMultipartData() {
return this.multipartDataMono;
}

@Override
public Mono<Void> cleanupMultipart() {
if (this.multipartRead) {
return getMultipartData()
.onErrorResume(t -> Mono.empty()) // ignore errors reading multipart data
.flatMapIterable(Map::values)
.flatMapIterable(Function.identity())
.flatMap(part -> part.delete()
.onErrorResume(ex -> Mono.empty()))
.then();
}
else {
return Mono.empty();
}
}

@Override
public LocaleContext getLocaleContext() {
return this.localeContextResolver.resolveLocaleContext(this);
Expand Down
Expand Up @@ -324,28 +324,30 @@ private static class DelegatingServerWebExchange implements ServerWebExchange {

private final Mono<MultiValueMap<String, Part>> multipartDataMono;

private volatile boolean multipartRead = false;


DelegatingServerWebExchange(ServerHttpRequest request, Map<String, Object> attributes,
ServerWebExchange delegate, List<HttpMessageReader<?>> messageReaders) {

this.request = request;
this.attributes = attributes;
this.delegate = delegate;
this.formDataMono = initFormData(request, messageReaders);
this.formDataMono = initFormData(messageReaders);
this.multipartDataMono = initMultipartData(request, messageReaders);
}

@SuppressWarnings("unchecked")
private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request,
List<HttpMessageReader<?>> readers) {

private Mono<MultiValueMap<String, String>> initFormData(List<HttpMessageReader<?>> readers) {
try {
MediaType contentType = request.getHeaders().getContentType();
MediaType contentType = this.request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
return ((HttpMessageReader<MultiValueMap<String, String>>) readers.stream()
.filter(reader -> reader.canRead(FORM_DATA_TYPE, MediaType.APPLICATION_FORM_URLENCODED))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No form data HttpMessageReader.")))
.readMono(FORM_DATA_TYPE, request, Hints.none())
.readMono(FORM_DATA_TYPE, this.request, Hints.none())
.doOnNext(ignored -> this.multipartRead = true)
.switchIfEmpty(EMPTY_FORM_DATA)
.cache();
}
Expand Down Expand Up @@ -398,7 +400,23 @@ public Mono<MultiValueMap<String, Part>> getMultipartData() {
return this.multipartDataMono;
}

// Delegating methods
@Override
public Mono<Void> cleanupMultipart() {
if (this.multipartRead) {
return getMultipartData()
.onErrorResume(t -> Mono.empty()) // ignore errors reading multipart data
.flatMapIterable(Map::values)
.flatMapIterable(Function.identity())
.flatMap(part -> part.delete()
.onErrorResume(ex -> Mono.empty()))
.then();
}
else {
return Mono.empty();
}
}

// Delegating methods

@Override
public ServerHttpResponse getResponse() {
Expand Down

0 comments on commit 4c2b65e

Please sign in to comment.