Affects: 5.2.4.RELEASE
I was previously using the following versions of Spring + Spring Boot:
<springVersion>5.0.16.RELEASE</springVersion>
<springBootVersion>2.0.5.RELEASE</springBootVersion>
And the following classes, which handled a multipart/mixed upload. This is all working fine using the Spring versions above:
StagingApi.java
import io.swagger.annotations.*;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.Valid;
import javax.validation.constraints.*;
@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2022-05-10T11:14:13.587+01:00[Europe/London]")
@Api(value = "Staging", description = "API")
public interface StagingApi {
@ApiOperation(value = "", nickname = "createOrReplaceBatch", notes = "", tags={ })
@ApiResponses(value = {
@ApiResponse(code = 200, message = ""),
@ApiResponse(code = 400, message = ""),
@ApiResponse(code = 500, message = "") })
@RequestMapping(value = "/batches/{batchId}",
consumes = { "multipart/mixed" },
method = RequestMethod.PUT)
ResponseEntity<Void> createOrReplaceBatch(
@ApiParam(value = "" ,required=true) @RequestHeader(value="X-TENANT-ID", required=true) String X_TENANT_ID,
@Size(min=1) @ApiParam(value = "",required=true) @PathVariable("batchId") String batchId,
@ApiParam(value = "" ) @Valid @RequestBody Object body);
}
StagingApiExtension.java
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.stream.Stream;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.Util;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
public class StaginApiExtension {
// This method is used to build a RequestBody from the Stream<MultiPart> param, then sends a HTTP request to the API (i.e. this code will call StagingController.java)
public void createOrReplaceBatch(final String tenantId, final String batchId, final Stream<MultiPart> uploadData)
throws ApiException
{
final MultipartBuilder mpBuilder = new MultipartBuilder().type(MultipartBuilder.MIXED);
final Iterator<MultiPart> uploadDataIterator = uploadData.iterator();
while (uploadDataIterator.hasNext()) {
final MultiPart fileToStage = uploadDataIterator.next();
mpBuilder.addFormDataPart(fileToStage.getName(),
null,
new StreamingBody(MediaType.parse(fileToStage.getContentType()),
fileToStage::openInputStream)
);
}
final RequestBody requestBody = mpBuilder.build();
final String apiPath = getApiClient().getBasePath() + PUT_API_PATH + batchId;
final Map<String, String> stagingHeaders = new HashMap<>();
stagingHeaders.put(TENANT_HEADER_NAME, tenantId);
final Request.Builder reqBuilder = new Request.Builder();
getApiClient().processHeaderParams(stagingHeaders, reqBuilder);
final Request request = reqBuilder
.url(apiPath)
.put(requestBody)
.build();
try {
final Response response = getApiClient().getHttpClient().newCall(request).execute();
if (!response.isSuccessful()) {
throw new ApiException("Error uploading documents for tenant " + tenantId + " batch: " + batchId,
response.code(),
null,
response.message());
}
} catch (IOException e) {
throw new ApiException(e);
}
}
}
StagingController.java
public ResponseEntity<Void> createOrReplaceBatch(
@ApiParam(value = "Identifies the tenant making the request.", required = true)
@RequestHeader(value = "X-TENANT-ID", required = true) String X_TENANT_ID,
@Size(min = 1) @ApiParam(value = "Identifies the batch.", required = true)
@PathVariable("batchId") String batchId,
Object body)
{
final ServletFileUpload fileUpload = new ServletFileUpload();
final FileItemIterator fileItemIterator;
try {
fileItemIterator = fileUpload.getItemIterator(request);
} catch (final FileUploadException | IOException ex) {
LOGGER.error("Error getting FileItemIterator", ex);
throw new WebMvcHandledRuntimeException(HttpStatus.BAD_REQUEST, ex.getMessage());
}
try {
batchDao.saveFiles(new TenantId(X_TENANT_ID), new BatchId(batchId), fileItemIterator);
return new ResponseEntity<>(HttpStatus.OK);
} catch (final InvalidTenantIdException | InvalidBatchIdException | IncompleteBatchException | InvalidBatchException ex) {
throw new WebMvcHandledRuntimeException(HttpStatus.BAD_REQUEST, ex.getMessage());
} catch (final StagingException ex) {
throw new WebMvcHandledRuntimeException(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
}
}
Log of successfully handled request:
[2022-05-10 11:12:14.861Z #bc7.042 DEBUG - - ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Looking up handler method for path /batches/test-batch
[2022-05-10 11:12:14.861Z #bc7.042 DEBUG - - ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.861Z #bc7.042 DEBUG - - ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.862Z #bc7.042 DEBUG - - ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.863Z #bc7.042 DEBUG - - ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Returning handler method [public org.springframework.http.ResponseEntity<java.lang.Void> com.acme.corp.staging.StagingController.createOrReplaceBatch(java.lang.String,java.lang.String,java.lang.Object)]
[2022-05-10 11:12:14.863Z #bc7.042 DEBUG - - ] o.s.b.f.s.DefaultListableBeanFactory: Returning cached instance of singleton bean 'stagingController'
[2022-05-10 11:12:14.866Z #bc7.042 DEBUG - - ] o.s.b.w.s.f.OrderedRequestContextFilter: Bound request context to thread: org.apache.catalina.connector.RequestFacade@37f2254a
[2022-05-10 11:12:14.870Z #bc7.042 DEBUG - - ] o.s.w.s.DispatcherServlet: DispatcherServlet with name 'dispatcherServlet' processing PUT request for [/batches/test-batch]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG - - ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Looking up handler method for path /batches/test-batch
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG - - ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG - - ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG - - ] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG - - ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Returning handler method [public org.springframework.http.ResponseEntity<java.lang.Void> com.acme.corp.staging.StagingController.createOrReplaceBatch(java.lang.String,java.lang.String,java.lang.Object)]
[2022-05-10 11:12:14.871Z #bc7.042 DEBUG - - ] o.s.b.f.s.DefaultListableBeanFactory: Returning cached instance of singleton bean 'stagingController'
[2022-05-10 11:12:14.932Z #bc7.042 DEBUG - 10c9] o.s.w.a.FixedContentNegotiationStrategy: Requested media types: [application/json, */*]
[2022-05-10 11:12:14.933Z #bc7.042 DEBUG - - ] o.s.w.s.DispatcherServlet: Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
[2022-05-10 11:12:14.933Z #bc7.042 DEBUG - - ] o.s.w.s.DispatcherServlet: Successfully completed request
However, when I try to update the versions of Spring and Spring Boot to:
<springVersion>5.2.4.RELEASE</springVersion>
<springBootVersion>2.2.6.RELEASE</springBootVersion>
the same request fails with a 415 UNSUPPORTED_MEDIA_TYPE response.
Log of failed request:
[2022-05-10 11:31:52.158Z #dc2.024 INFO - - ] o.a.c.c.C..localhost.: Initializing Spring DispatcherServlet 'dispatcherServlet'
[2022-05-10 11:31:52.158Z #dc2.024 INFO - - ] o.s.w.s.DispatcherServlet: Initializing Servlet 'dispatcherServlet'
[2022-05-10 11:31:52.158Z #dc2.024 DEBUG - - ] o.s.w.s.DispatcherServlet: Detected StandardServletMultipartResolver
[2022-05-10 11:31:52.162Z #dc2.024 DEBUG - - ] o.s.w.s.DispatcherServlet: enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data
[2022-05-10 11:31:52.162Z #dc2.024 INFO - - ] o.s.w.s.DispatcherServlet: Completed initialization in 4 ms
[2022-05-10 11:31:52.165Z #dc2.024 DEBUG - - ] o.s.w.s.DispatcherServlet: PUT "/batches/test-batch", parameters={}
[2022-05-10 11:31:52.192Z #dc2.024 DEBUG - - ] o.s.w.s.m.m.a.RequestMappingHandlerMapping: Mapped to com.acme.corp.staging.StagingController#createOrReplaceBatch(String, String, Object)
[2022-05-10 11:31:52.202Z #dc2.024 DEBUG - a17e] o.s.w.s.m.m.a.ServletInvocableHandlerMethod: Could not resolve parameter [2] in public org.springframework.http.ResponseEntity<java.lang.Void> com.acme.corp.staging.StagingController.createOrReplaceBatch(java.lang.String,java.lang.String,java.lang.Object): Content type 'multipart/mixed;boundary=efb8369b-607b-4dcf-9f92-e6cd8244db1e;charset=UTF-8' not supported
[2022-05-10 11:31:52.204Z #dc2.024 DEBUG - a17e] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Using @ExceptionHandler acme.corp.staging.exceptions.WebMvcExceptionHandler#handleException(Exception, WebRequest)
[2022-05-10 11:31:52.206Z #dc2.024 DEBUG - a17e] o.s.w.s.m.m.a.HttpEntityMethodProcessor: No match for [application/json, */*], supported: []
[2022-05-10 11:31:52.206Z #dc2.024 DEBUG - a17e] o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/mixed;boundary=efb8369b-607b-4dcf-9f92-e6cd8244db1e;charset=UTF-8' not supported]
[2022-05-10 11:31:52.206Z #dc2.024 DEBUG - a17e] o.s.w.s.DispatcherServlet: Completed 415 UNSUPPORTED_MEDIA_TYPE
Based on this StackOverflow post:
https://stackoverflow.com/questions/48051177/content-type-multipart-form-databoundary-charset-utf-8-not-supported
I tried to change:
@Valid @RequestBody Object body
to:
@Valid @ModelAttribute Object body
This seemed to work, but I think it caused a new problem due to the order of the parts in the request body not being the same as they were before. i.e I have code that will throw an Exception if the client sends a request where the parts of the body are not in a specific order, and with this change to from RequestBody
to ModelAttribute
, the code is no longer throwing an Exception, so I'm not sure how the request is coming in, but its not exactly the same as it was when using RequestBody
.
I'm also not sure this is the right change here, since RequestBody
was working fine using the Spring versions at the top of this post?
Any help is much appreciated!
Comment From: bclozel
Closing as a duplicate of spring-projects/spring-boot#30971 - we can migrate the issue to this repository if it turns out this is a Spring Framework bug. Please avoid creating duplicates.