diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/DefaultParts.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/DefaultParts.java
new file mode 100644
index 00000000000..293ef04f178
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/DefaultParts.java
@@ -0,0 +1,297 @@
+/*
+ * 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.converter.multipart;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.Assert;
+
+/**
+ * Default implementations of {@link Part} and subtypes.
+ *
+ * @author Arjen Poutsma
+ * @author Brian Clozel
+ */
+abstract class DefaultParts {
+
+ /**
+ * Create a new {@link FormFieldPart} with the given parameters.
+ * @param headers the part headers
+ * @param value the form field value
+ * @return the created part
+ */
+ public static FormFieldPart formFieldPart(HttpHeaders headers, String value) {
+ Assert.notNull(headers, "Headers must not be null");
+ Assert.notNull(value, "Value must not be null");
+
+ return new DefaultFormFieldPart(headers, value);
+ }
+
+ /**
+ * Create a new {@link Part} or {@link FilePart} based on a flux of data
+ * buffers. Returns {@link FilePart} if the {@code Content-Disposition} of
+ * the given headers contains a filename, or a "normal" {@link Part}
+ * otherwise.
+ * @param headers the part headers
+ * @param dataBuffer the content of the part
+ * @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()}
+ */
+ public static Part part(HttpHeaders headers, DataBuffer dataBuffer) {
+ Assert.notNull(headers, "Headers must not be null");
+ Assert.notNull(dataBuffer, "DataBuffer must not be null");
+
+ return partInternal(headers, new DataBufferContent(dataBuffer));
+ }
+
+ /**
+ * Create a new {@link Part} or {@link FilePart} based on the given file.
+ * Returns {@link FilePart} if the {@code Content-Disposition} of the given
+ * headers contains a filename, or a "normal" {@link Part} otherwise
+ * @param headers the part headers
+ * @param file the file
+ * @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()}
+ */
+ public static Part part(HttpHeaders headers, Path file) {
+ Assert.notNull(headers, "Headers must not be null");
+ Assert.notNull(file, "File must not be null");
+
+ return partInternal(headers, new FileContent(file));
+ }
+
+
+ private static Part partInternal(HttpHeaders headers, Content content) {
+ String filename = headers.getContentDisposition().getFilename();
+ if (filename != null) {
+ return new DefaultFilePart(headers, content);
+ }
+ else {
+ return new DefaultPart(headers, content);
+ }
+ }
+
+
+ /**
+ * Abstract base class for {@link Part} implementations.
+ */
+ private abstract static class AbstractPart implements Part {
+
+ private final HttpHeaders headers;
+
+ protected AbstractPart(HttpHeaders headers) {
+ Assert.notNull(headers, "HttpHeaders is required");
+ this.headers = headers;
+ }
+
+ @Override
+ public String name() {
+ String name = headers().getContentDisposition().getName();
+ Assert.state(name != null, "No part name available");
+ return name;
+ }
+
+ @Override
+ public HttpHeaders headers() {
+ return this.headers;
+ }
+ }
+
+
+ /**
+ * Default implementation of {@link FormFieldPart}.
+ */
+ private static class DefaultFormFieldPart extends AbstractPart implements FormFieldPart {
+
+ private final String value;
+
+ public DefaultFormFieldPart(HttpHeaders headers, String value) {
+ super(headers);
+ this.value = value;
+ }
+
+ @Override
+ public InputStream content() {
+ byte[] bytes = this.value.getBytes(MultipartUtils.charset(headers()));
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public String value() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ String name = headers().getContentDisposition().getName();
+ if (name != null) {
+ return "DefaultFormFieldPart{" + name() + "}";
+ }
+ else {
+ return "DefaultFormFieldPart";
+ }
+ }
+ }
+
+
+ /**
+ * Default implementation of {@link Part}.
+ */
+ private static class DefaultPart extends AbstractPart {
+
+ protected final Content content;
+
+ public DefaultPart(HttpHeaders headers, Content content) {
+ super(headers);
+ this.content = content;
+ }
+
+ @Override
+ public InputStream content() throws IOException {
+ return this.content.content();
+ }
+
+ @Override
+ public void delete() throws IOException {
+ this.content.delete();
+ }
+
+ @Override
+ public String toString() {
+ String name = headers().getContentDisposition().getName();
+ if (name != null) {
+ return "DefaultPart{" + name + "}";
+ }
+ else {
+ return "DefaultPart";
+ }
+ }
+ }
+
+
+ /**
+ * Default implementation of {@link FilePart}.
+ */
+ private static final class DefaultFilePart extends DefaultPart implements FilePart {
+
+ public DefaultFilePart(HttpHeaders headers, Content content) {
+ super(headers, content);
+ }
+
+ @Override
+ public String filename() {
+ String filename = headers().getContentDisposition().getFilename();
+ Assert.state(filename != null, "No filename found");
+ return filename;
+ }
+
+ @Override
+ public void transferTo(Path dest) throws IOException {
+ this.content.transferTo(dest);
+ }
+
+ @Override
+ public String toString() {
+ ContentDisposition contentDisposition = headers().getContentDisposition();
+ String name = contentDisposition.getName();
+ String filename = contentDisposition.getFilename();
+ if (name != null) {
+ return "DefaultFilePart{" + name + " (" + filename + ")}";
+ }
+ else {
+ return "DefaultFilePart{(" + filename + ")}";
+ }
+ }
+ }
+
+
+ /**
+ * Part content abstraction.
+ */
+ private interface Content {
+
+ InputStream content() throws IOException;
+
+ void transferTo(Path dest) throws IOException;
+
+ void delete() throws IOException;
+ }
+
+
+ /**
+ * {@code Content} implementation based on an in-memory {@code InputStream}.
+ */
+ private static final class DataBufferContent implements Content {
+
+ private final DataBuffer content;
+
+ public DataBufferContent(DataBuffer content) {
+ this.content = content;
+ }
+
+ @Override
+ public InputStream content() {
+ return this.content.asInputStream();
+ }
+
+ @Override
+ public void transferTo(Path dest) throws IOException {
+ Files.copy(this.content.asInputStream(), dest, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ @Override
+ public void delete() throws IOException {
+ }
+ }
+
+
+ /**
+ * {@code Content} implementation based on a file.
+ */
+ private static final class FileContent implements Content {
+
+ private final Path file;
+
+ public FileContent(Path file) {
+ this.file = file;
+ }
+
+ @Override
+ public InputStream content() throws IOException {
+ return Files.newInputStream(this.file.toAbsolutePath(), StandardOpenOption.READ);
+ }
+
+ @Override
+ public void transferTo(Path dest) throws IOException {
+ Files.copy(this.file, dest, StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ @Override
+ public void delete() throws IOException {
+ Files.delete(this.file);
+ }
+
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/FilePart.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/FilePart.java
new file mode 100644
index 00000000000..9d5594a3d0e
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/FilePart.java
@@ -0,0 +1,72 @@
+/*
+ * 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.converter.multipart;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+
+/**
+ * Specialization of {@link Part} that represents an uploaded file received in
+ * a multipart request.
+ *
+ * @author Brian Clozel
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @since 7.1
+ */
+public interface FilePart extends Part {
+
+ /**
+ * Return the original filename in the client's filesystem.
+ *
Note: Please keep in mind this filename is supplied
+ * by the client and should not be used blindly. In addition to not using
+ * the directory portion, the file name could also contain characters such
+ * as ".." and others that can be used maliciously. It is recommended to not
+ * use this filename directly. Preferably generate a unique one and save
+ * this one somewhere for reference, if necessary.
+ * @return the original filename, or the empty String if no file has been chosen
+ * in the multipart form, or {@code null} if not defined or not available
+ * @see RFC 7578, Section 4.2
+ * @see Unrestricted File Upload
+ */
+ String filename();
+
+ /**
+ * Convenience method to copy the content of the file in this part to the
+ * given destination file. If the destination file already exists, it will
+ * be truncated first.
+ *
The default implementation delegates to {@link #transferTo(Path)}.
+ * @param dest the target file
+ * @throws IllegalStateException if the part isn't a file
+ * @see #transferTo(Path)
+ */
+ default void transferTo(File dest) throws IOException {
+ transferTo(dest.toPath());
+ }
+
+ /**
+ * Convenience method to copy the content of the file in this part to the
+ * given destination file. If the destination file already exists, it will
+ * be truncated first.
+ * @param dest the target file
+ * @throws IllegalStateException if the part isn't a file
+ * @see #transferTo(File)
+ */
+ void transferTo(Path dest) throws IOException;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/FormFieldPart.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/FormFieldPart.java
new file mode 100644
index 00000000000..8a920fb7da7
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/FormFieldPart.java
@@ -0,0 +1,34 @@
+/*
+ * 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.converter.multipart;
+
+
+/**
+ * Specialization of {@link Part} for a form field.
+ *
+ * @author Brian Clozel
+ * @author Rossen Stoyanchev
+ * @since 7.1
+ */
+public interface FormFieldPart extends Part {
+
+ /**
+ * Return the form field value.
+ */
+ String value();
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java
new file mode 100644
index 00000000000..5526bdbcbf5
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/multipart/MultipartHttpMessageConverter.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright 2026-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.converter.multipart;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.jspecify.annotations.Nullable;
+
+import org.springframework.core.ResolvableType;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.buffer.DataBufferLimitException;
+import org.springframework.http.ContentDisposition;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.StreamingHttpOutputMessage;
+import org.springframework.http.converter.AbstractHttpMessageConverter;
+import org.springframework.http.converter.ByteArrayHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.http.converter.ResourceHttpMessageConverter;
+import org.springframework.http.converter.SmartHttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.util.Assert;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * Implementation of {@link HttpMessageConverter} to read and write
+ * multipart data (for example, file uploads).
+ *
+ *
This converter can read {@code "multipart/form-data"}
+ * and {@code "multipart/mixed"} messages as
+ * {@link MultiValueMap MultiValueMap<String, Part>}, and
+ * write {@link MultiValueMap MultiValueMap<String, Object>} as
+ * multipart messages.
+ *
+ *
On Servlet containers, the reading of multipart messages should be
+ * delegated to the {@link org.springframework.web.multipart.MultipartResolver}.
+ *
+ *
Multipart Data
+ *
+ *
By default, {@code "multipart/form-data"} is used as the content type when
+ * {@linkplain #write writing} multipart data. It is also possible to write
+ * multipart data using other multipart subtypes such as {@code "multipart/mixed"}
+ * and {@code "multipart/related"}, as long as the multipart subtype is registered
+ * as a {@linkplain #getSupportedMediaTypes supported media type} and the
+ * desired multipart subtype is specified as the content type when
+ * {@linkplain #write writing} the multipart data. Note that {@code "multipart/mixed"}
+ * is registered as a supported media type by default.
+ *
+ *
When writing multipart data, this converter uses other
+ * {@link HttpMessageConverter HttpMessageConverters} to write the respective
+ * MIME parts. By default, basic converters are registered for byte array,
+ * {@code String}, and {@code Resource}. This can be set with the main
+ * {@link #MultipartHttpMessageConverter(Iterable) constructor}.
+ *
+ *
Examples
+ *
+ *
The following snippet shows how to submit an HTML form using the
+ * {@code "multipart/form-data"} content type.
+ *
+ *
+ *
+ * @author Brian Clozel
+ * @author Arjen Poutsma
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @since 7.1
+ * @see org.springframework.util.MultiValueMap
+ */
+public class MultipartHttpMessageConverter implements SmartHttpMessageConverter> {
+
+ private final List> partConverters;
+
+ private @Nullable Path tempDirectory;
+
+ private List supportedMediaTypes = new ArrayList<>();
+
+ private Charset charset = StandardCharsets.UTF_8;
+
+ private @Nullable Charset multipartCharset;
+
+ private int maxInMemorySize = 256 * 1024;
+
+ private int maxHeadersSize = 10 * 1024;
+
+ private long maxDiskUsagePerPart = -1;
+
+ private int maxParts = -1;
+
+ /**
+ * Create a new converter instance with the given converter instances for reading and
+ * writing parts.
+ * @param converters the converters to use for reading and writing parts
+ */
+ public MultipartHttpMessageConverter(Iterable> converters) {
+ this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
+ this.supportedMediaTypes.add(MediaType.MULTIPART_MIXED);
+ this.supportedMediaTypes.add(MediaType.MULTIPART_RELATED);
+
+ this.partConverters = new ArrayList<>();
+ converters.forEach(this.partConverters::add);
+ }
+
+ /**
+ * Create a new converter instance with default converter instances for reading and
+ * writing parts.
+ * @see ByteArrayHttpMessageConverter
+ * @see StringHttpMessageConverter
+ * @see ResourceHttpMessageConverter
+ */
+ public MultipartHttpMessageConverter() {
+ this(List.of( new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
+ new ResourceHttpMessageConverter()));
+ }
+
+ /**
+ * Set the list of {@link MediaType} objects supported by this converter.
+ * @see #addSupportedMediaTypes(MediaType...)
+ * @see #getSupportedMediaTypes()
+ */
+ public void setSupportedMediaTypes(List supportedMediaTypes) {
+ Assert.notNull(supportedMediaTypes, "'supportedMediaTypes' must not be null");
+ // Ensure internal list is mutable.
+ this.supportedMediaTypes = new ArrayList<>(supportedMediaTypes);
+ }
+
+ /**
+ * Add {@link MediaType} objects to be supported by this converter.
+ *
The supplied {@code MediaType} objects will be appended to the list
+ * of {@linkplain #getSupportedMediaTypes() supported MediaType objects}.
+ * @param supportedMediaTypes a var-args list of {@code MediaType} objects to add
+ * @see #setSupportedMediaTypes(List)
+ */
+ public void addSupportedMediaTypes(MediaType... supportedMediaTypes) {
+ Assert.notNull(supportedMediaTypes, "'supportedMediaTypes' must not be null");
+ Assert.noNullElements(supportedMediaTypes, "'supportedMediaTypes' must not contain null elements");
+ Collections.addAll(this.supportedMediaTypes, supportedMediaTypes);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see #setSupportedMediaTypes(List)
+ * @see #addSupportedMediaTypes(MediaType...)
+ */
+ @Override
+ public List getSupportedMediaTypes() {
+ return Collections.unmodifiableList(this.supportedMediaTypes);
+ }
+
+
+ /**
+ * Return the configured converters for MIME parts.
+ */
+ public List> getPartConverters() {
+ return Collections.unmodifiableList(this.partConverters);
+ }
+
+ /**
+ * Set the default character set to use for reading and writing form data when
+ * the request or response {@code Content-Type} header does not explicitly
+ * specify it.
+ *
As of 4.3, this is also used as the default charset for the conversion
+ * of text bodies in a multipart request.
+ *
As of 5.0, this is also used for part headers including
+ * {@code Content-Disposition} (and its filename parameter) unless (the mutually
+ * exclusive) {@link #setMultipartCharset multipartCharset} is also set, in
+ * which case part headers are encoded as ASCII and filename is encoded
+ * with the {@code encoded-word} syntax from RFC 2047.
+ *
By default, this is set to "UTF-8".
+ */
+ public void setCharset(@Nullable Charset charset) {
+ if (charset != this.charset) {
+ this.charset = (charset != null ? charset : StandardCharsets.UTF_8);
+ applyDefaultCharset();
+ }
+ }
+
+ /**
+ * Apply the configured charset as a default to registered part converters.
+ */
+ private void applyDefaultCharset() {
+ for (HttpMessageConverter> candidate : this.partConverters) {
+ if (candidate instanceof AbstractHttpMessageConverter> converter) {
+ // Only override default charset if the converter operates with a charset to begin with...
+ if (converter.getDefaultCharset() != null) {
+ converter.setDefaultCharset(this.charset);
+ }
+ }
+ }
+ }
+
+ /**
+ * Set the character set to use when writing multipart data to encode file
+ * names. Encoding is based on the {@code encoded-word} syntax defined in
+ * RFC 2047 and relies on {@code MimeUtility} from {@code jakarta.mail}.
+ *
As of 5.0 by default part headers, including {@code Content-Disposition}
+ * (and its filename parameter) will be encoded based on the setting of
+ * {@link #setCharset(Charset)} or {@code UTF-8} by default.
+ * @see Encoded-Word
+ */
+ public void setMultipartCharset(Charset charset) {
+ this.multipartCharset = charset;
+ }
+
+
+ /**
+ * Configure the maximum amount of memory that is allowed per headers section of each part.
+ *
By default, this is set to 10K.
+ * @param byteCount the maximum amount of memory for headers
+ */
+ public void setMaxHeadersSize(int byteCount) {
+ this.maxHeadersSize = byteCount;
+ }
+
+ /**
+ * Configure the maximum amount of memory allowed per part.
+ * When the limit is exceeded:
+ *
+ *
File parts are written to a temporary file.
+ *
Non-file parts are rejected with {@link DataBufferLimitException}.
+ *
+ *
By default, this is set to 256K.
+ * @param maxInMemorySize the in-memory limit in bytes; if set to -1 the entire
+ * contents will be stored in memory
+ */
+ public void setMaxInMemorySize(int maxInMemorySize) {
+ this.maxInMemorySize = maxInMemorySize;
+ }
+
+ /**
+ * Configure the maximum amount of disk space allowed for file parts.
+ *
By default, this is set to -1, meaning that there is no maximum.
+ *
Note that this property is ignored when
+ * {@link #setMaxInMemorySize(int) maxInMemorySize} is set to -1.
+ */
+ public void setMaxDiskUsagePerPart(long maxDiskUsagePerPart) {
+ this.maxDiskUsagePerPart = maxDiskUsagePerPart;
+ }
+
+ /**
+ * Specify the maximum number of parts allowed in a given multipart request.
+ *
By default, this is set to -1, meaning that there is no maximum.
+ */
+ public void setMaxParts(int maxParts) {
+ this.maxParts = maxParts;
+ }
+
+ @Override
+ public boolean canRead(ResolvableType elementType, @Nullable MediaType mediaType) {
+ if (!supportsMediaType(mediaType)) {
+ return false;
+ }
+ if (!MultiValueMap.class.isAssignableFrom(elementType.toClass()) ||
+ (!elementType.hasUnresolvableGenerics() &&
+ !Part.class.isAssignableFrom(elementType.getGeneric(1).toClass()))) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean supportsMediaType(@Nullable MediaType mediaType) {
+ if (mediaType == null) {
+ return true;
+ }
+ for (MediaType supportedMediaType : getSupportedMediaTypes()) {
+ if (supportedMediaType.includes(mediaType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public MultiValueMap read(ResolvableType type, HttpInputMessage message, @Nullable Map hints) throws IOException, HttpMessageNotReadableException {
+
+ Charset headersCharset = MultipartUtils.charset(message.getHeaders());
+ byte[] boundary = boundary(message, headersCharset);
+ if (boundary == null) {
+ throw new HttpMessageNotReadableException("No multipart boundary found in Content-Type: \"" +
+ message.getHeaders().getContentType() + "\"", message);
+ }
+ PartGenerator partListener = new PartGenerator(this.maxInMemorySize, this.maxDiskUsagePerPart, this.maxParts, getTempDirectory());
+ new MultipartParser(this.maxHeadersSize, 2 * 1024).parse(message.getBody(), boundary,
+ headersCharset, partListener);
+ return partListener.getParts();
+ }
+
+
+ private static byte @Nullable [] boundary(HttpInputMessage message, Charset headersCharset) {
+ MediaType contentType = message.getHeaders().getContentType();
+ if (contentType != null) {
+ String boundary = contentType.getParameter("boundary");
+ if (boundary != null) {
+ int len = boundary.length();
+ if (len > 2 && boundary.charAt(0) == '"' && boundary.charAt(len - 1) == '"') {
+ boundary = boundary.substring(1, len - 1);
+ }
+ return boundary.getBytes(headersCharset);
+ }
+ }
+ return null;
+ }
+
+ private Path getTempDirectory() throws IOException {
+ if (this.tempDirectory == null || !this.tempDirectory.toFile().exists()) {
+ this.tempDirectory = Files.createTempDirectory("spring-multipart-");
+ }
+ return this.tempDirectory;
+ }
+
+ @Override
+ public boolean canWrite(ResolvableType targetType, Class> valueClass, @Nullable MediaType mediaType) {
+ if (!MultiValueMap.class.isAssignableFrom(targetType.toClass())) {
+ return false;
+ }
+ if (mediaType == null || MediaType.ALL.equals(mediaType)) {
+ return true;
+ }
+ for (MediaType supportedMediaType : getSupportedMediaTypes()) {
+ if (supportedMediaType.isCompatibleWith(mediaType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void write(MultiValueMap map, ResolvableType type, @Nullable MediaType contentType, HttpOutputMessage outputMessage, @Nullable Map hints) throws IOException, HttpMessageNotWritableException {
+ MultiValueMap parts = (MultiValueMap) map;
+
+ // If the supplied content type is null, fall back to multipart/form-data.
+ // Otherwise, rely on the fact that isMultipart() already verified the
+ // supplied content type is multipart.
+ if (contentType == null) {
+ contentType = MediaType.MULTIPART_FORM_DATA;
+ }
+
+ Map parameters = new LinkedHashMap<>(contentType.getParameters().size() + 2);
+ parameters.putAll(contentType.getParameters());
+
+ byte[] boundary = MimeTypeUtils.generateMultipartBoundary();
+ if (!isFilenameCharsetSet()) {
+ if (!this.charset.equals(StandardCharsets.UTF_8) &&
+ !this.charset.equals(StandardCharsets.US_ASCII)) {
+ parameters.put("charset", this.charset.name());
+ }
+ }
+ parameters.put("boundary", new String(boundary, StandardCharsets.US_ASCII));
+
+ // Add parameters to output content type
+ contentType = new MediaType(contentType, parameters);
+ outputMessage.getHeaders().setContentType(contentType);
+
+ if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
+ boolean repeatable = checkPartsRepeatable(parts, contentType);
+ streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
+ @Override
+ public void writeTo(OutputStream outputStream) throws IOException {
+ MultipartHttpMessageConverter.this.writeParts(outputStream, parts, boundary);
+ writeEnd(outputStream, boundary);
+ }
+
+ @Override
+ public boolean repeatable() {
+ return repeatable;
+ }
+ });
+ }
+ else {
+ writeParts(outputMessage.getBody(), parts, boundary);
+ writeEnd(outputMessage.getBody(), boundary);
+ }
+ }
+
+
+ @SuppressWarnings({"unchecked", "ConstantValue"})
+ private boolean checkPartsRepeatable(MultiValueMap map, MediaType contentType) {
+ return map.entrySet().stream().allMatch(e -> e.getValue().stream().filter(Objects::nonNull).allMatch(part -> {
+ HttpHeaders headers = null;
+ Object body = part;
+ if (part instanceof HttpEntity> entity) {
+ headers = entity.getHeaders();
+ body = entity.getBody();
+ Assert.state(body != null, "Empty body for part '" + e.getKey() + "': " + part);
+ }
+ HttpMessageConverter converter = (HttpMessageConverter) findConverterFor(e.getKey(), headers, body);
+ return converter != null && converter.canWriteRepeatedly((T) body, contentType);
+ }));
+ }
+
+ private @Nullable HttpMessageConverter> findConverterFor(
+ String name, @Nullable HttpHeaders headers, Object body) {
+
+ Class> partType = body.getClass();
+ MediaType contentType = (headers != null ? headers.getContentType() : null);
+ for (HttpMessageConverter> converter : this.partConverters) {
+ if (converter.canWrite(partType, contentType)) {
+ return converter;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * When {@link #setMultipartCharset(Charset)} is configured (i.e. RFC 2047,
+ * {@code encoded-word} syntax) we need to use ASCII for part headers, or
+ * otherwise we encode directly using the configured {@link #setCharset(Charset)}.
+ */
+ private boolean isFilenameCharsetSet() {
+ return (this.multipartCharset != null);
+ }
+
+ private void writeParts(OutputStream os, MultiValueMap parts, byte[] boundary) throws IOException {
+ for (Map.Entry> entry : parts.entrySet()) {
+ String name = entry.getKey();
+ for (Object part : entry.getValue()) {
+ if (part != null) {
+ writeBoundary(os, boundary);
+ writePart(name, getHttpEntity(part), os);
+ writeNewLine(os);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void writePart(String name, HttpEntity> partEntity, OutputStream os) throws IOException {
+ Object partBody = partEntity.getBody();
+ Assert.state(partBody != null, "Empty body for part '" + name + "': " + partEntity);
+ HttpHeaders partHeaders = partEntity.getHeaders();
+ MediaType partContentType = partHeaders.getContentType();
+ HttpMessageConverter> converter = findConverterFor(name, partHeaders, partBody);
+ if (converter != null) {
+ Charset charset = isFilenameCharsetSet() ? StandardCharsets.US_ASCII : this.charset;
+ HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os, charset);
+ String filename = getFilename(partBody);
+ ContentDisposition.Builder cd = ContentDisposition.formData().name(name);
+ if (filename != null) {
+ cd.filename(filename, this.multipartCharset);
+ }
+ multipartMessage.getHeaders().setContentDisposition(cd.build());
+ if (!partHeaders.isEmpty()) {
+ multipartMessage.getHeaders().putAll(partHeaders);
+ }
+ ((HttpMessageConverter