Skip to content

Commit

Permalink
Content-type for POST endpoints with multipart/form-data does not wor…
Browse files Browse the repository at this point in the history
…k since v2.4.0. Fixes #2621
  • Loading branch information
bnasslahsen committed Jun 19, 2024
1 parent 6c24eb6 commit cf12547
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ else if (methodParameter.getParameterAnnotation(BackendId.class) != null) {
parameterInfo.setParameterModel(parameter);
}
if (!ArrayUtils.isEmpty(methodParameter.getParameterAnnotations()))
parameter = requestBuilder.buildParams(parameterInfo, openAPI.getComponents(), requestMethod, null,
parameter = requestBuilder.buildParams(parameterInfo, openAPI.getComponents(), requestMethod, methodAttributes,
openAPI.getOpenapi());
addParameters(openAPI, requestMethod, methodAttributes, operation, methodParameter, parameterInfo, parameter);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
package org.springdoc.core.service;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayList;
Expand Down Expand Up @@ -88,12 +89,14 @@
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.UriComponentsBuilder;

import static org.springdoc.core.converters.SchemaPropertyDeprecatingConverter.containsDeprecatedAnnotation;
import static org.springdoc.core.service.GenericParameterService.isFile;
import static org.springdoc.core.utils.Constants.OPENAPI_ARRAY_TYPE;
import static org.springdoc.core.utils.Constants.OPENAPI_STRING_TYPE;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

/**
* The type Abstract request builder.
Expand Down Expand Up @@ -323,7 +326,7 @@ public Operation build(HandlerMethod handlerMethod, RequestMethod requestMethod,
}

if (!isParamToIgnore(methodParameter)) {
parameter = buildParams(parameterInfo, components, requestMethod, methodAttributes.getJsonViewAnnotation(), openAPI.getOpenapi());
parameter = buildParams(parameterInfo, components, requestMethod, methodAttributes, openAPI.getOpenapi());
// Merge with the operation parameters
parameter = GenericParameterService.mergeParameter(operationParameters, parameter);
List<Annotation> parameterAnnotations = Arrays.asList(methodParameter.getParameterAnnotations());
Expand Down Expand Up @@ -353,7 +356,7 @@ else if (!RequestMethod.GET.equals(requestMethod) || OpenApiVersion.OPENAPI_3_1.
// support form-data
if (defaultSupportFormData && requestBody != null
&& requestBody.getContent() != null
&& requestBody.getContent().containsKey(org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)) {
&& requestBody.getContent().containsKey(MULTIPART_FORM_DATA_VALUE)) {
Iterator<Entry<ParameterId, Parameter>> it = map.entrySet().iterator();
while (it.hasNext()) {
Entry<ParameterId, Parameter> entry = it.next();
Expand Down Expand Up @@ -496,28 +499,28 @@ public boolean isValidParameter(Parameter parameter) {
/**
* Build params parameter.
*
* @param parameterInfo the parameter info
* @param components the components
* @param requestMethod the request method
* @param jsonView the json view
* @param openApiVersion the open api version
* @param parameterInfo the parameter info
* @param components the components
* @param requestMethod the request method
* @param methodAttributes the method attributes
* @param openApiVersion the open api version
* @return the parameter
*/
public Parameter buildParams(ParameterInfo parameterInfo, Components components,
RequestMethod requestMethod, JsonView jsonView, String openApiVersion) {
RequestMethod requestMethod, MethodAttributes methodAttributes, String openApiVersion) {
MethodParameter methodParameter = parameterInfo.getMethodParameter();
if (parameterInfo.getParamType() != null) {
if (!ValueConstants.DEFAULT_NONE.equals(parameterInfo.getDefaultValue()))
parameterInfo.setRequired(false);
else
parameterInfo.setDefaultValue(null);
return this.buildParam(parameterInfo, components, jsonView);
return this.buildParam(parameterInfo, components, methodAttributes.getJsonViewAnnotation());
}
// By default
if (!isRequestBodyParam(requestMethod, parameterInfo, openApiVersion)) {
if (!isRequestBodyParam(requestMethod, parameterInfo, openApiVersion, methodAttributes)) {
parameterInfo.setRequired(!((DelegatingMethodParameter) methodParameter).isNotRequired() && !methodParameter.isOptional());
parameterInfo.setDefaultValue(null);
return this.buildParam(parameterInfo, components, jsonView);
return this.buildParam(parameterInfo, components, methodAttributes.getJsonViewAnnotation());
}
return null;
}
Expand Down Expand Up @@ -631,7 +634,7 @@ public RequestBodyService getRequestBodyBuilder() {
public boolean isDefaultFlatParamObject() {
return defaultFlatParamObject;
}

/**
* Calculate size.
*
Expand Down Expand Up @@ -722,12 +725,13 @@ private void applyValidationsToSchema(Map<String, Annotation> annos, Schema<?> s
/**
* Is RequestBody param boolean.
*
* @param requestMethod the request method
* @param parameterInfo the parameter info
* @param openApiVersion the open api version
* @param requestMethod the request method
* @param parameterInfo the parameter info
* @param openApiVersion the open api version
* @param methodAttributes the method attributes
* @return the boolean
*/
private boolean isRequestBodyParam(RequestMethod requestMethod, ParameterInfo parameterInfo, String openApiVersion) {
private boolean isRequestBodyParam(RequestMethod requestMethod, ParameterInfo parameterInfo, String openApiVersion, MethodAttributes methodAttributes) {
MethodParameter methodParameter = parameterInfo.getMethodParameter();
DelegatingMethodParameter delegatingMethodParameter = (DelegatingMethodParameter) methodParameter;
boolean isBodyAllowed = !RequestMethod.GET.equals(requestMethod) || OpenApiVersion.OPENAPI_3_1.getVersion().equals(openApiVersion);
Expand All @@ -739,8 +743,7 @@ private boolean isRequestBodyParam(RequestMethod requestMethod, ParameterInfo pa
|| AnnotatedElementUtils.findMergedAnnotation(Objects.requireNonNull(methodParameter.getMethod()), io.swagger.v3.oas.annotations.parameters.RequestBody.class) != null)
|| checkOperationRequestBody(methodParameter)
|| checkFile(methodParameter)

);
|| Arrays.asList(methodAttributes.getMethodConsumes()).contains(MULTIPART_FORM_DATA_VALUE));
}

/**
Expand All @@ -767,7 +770,7 @@ else if (methodParameter.getParameterAnnotation(org.springframework.web.bind.ann
private boolean checkOperationRequestBody(MethodParameter methodParameter) {
if (AnnotatedElementUtils.findMergedAnnotation(Objects.requireNonNull(methodParameter.getMethod()), io.swagger.v3.oas.annotations.Operation.class) != null) {
io.swagger.v3.oas.annotations.Operation operation = AnnotatedElementUtils.findMergedAnnotation(Objects.requireNonNull(methodParameter.getMethod()), io.swagger.v3.oas.annotations.Operation.class);
if(operation!=null){
if (operation != null) {
io.swagger.v3.oas.annotations.parameters.RequestBody requestBody = operation.requestBody();
if (StringUtils.isNotBlank(requestBody.description()))
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package test.org.springdoc.api.v30.app221;

import java.io.IOException;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import test.org.springdoc.api.v30.app221.HomeController.HelloDto;
import test.org.springdoc.api.v30.app221.HomeController.HelloUploadDto;

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

@Tag(name = "Hello World Api", description = "This is a test api")
@RequestMapping("api/hello")
public interface HomeApi {

@Operation(summary = "Upload new content", description = "Upload test content")
@PostMapping(produces = APPLICATION_JSON_VALUE, consumes = MULTIPART_FORM_DATA_VALUE)
HelloDto uploadContent(HelloUploadDto contentUploadDto) throws IOException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package test.org.springdoc.api.v30.app221;

import java.io.IOException;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class HomeController implements HomeApi {

@Override
public HelloDto uploadContent(@Valid HelloUploadDto uploadDto)
throws IOException {
var fileBytes = uploadDto.file.getBytes();
return new HelloDto(uploadDto.title, fileBytes);
}

public record HelloDto(String title, byte[] file) {}

public record HelloUploadDto(@NotNull String title, MultipartFile file) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
*
* *
* * *
* * * *
* * * * * Copyright 2019-2022 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 test.org.springdoc.api.v30.app221;

import test.org.springdoc.api.v30.AbstractSpringDocV30Test;

import org.springframework.boot.autoconfigure.SpringBootApplication;

public class SpringDocApp221Test extends AbstractSpringDocV30Test {

@SpringBootApplication
static class SpringDocTestApp {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"openapi": "3.0.1",
"info": {
"title": "OpenAPI definition",
"version": "v0"
},
"servers": [
{
"url": "http://localhost",
"description": "Generated server url"
}
],
"tags": [
{
"name": "Hello World Api",
"description": "This is a test api"
}
],
"paths": {
"/api/hello": {
"post": {
"tags": [
"Hello World Api"
],
"summary": "Upload new content",
"description": "Upload test content",
"operationId": "uploadContent",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/HelloUploadDto"
}
}
}
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HelloDto"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"HelloUploadDto": {
"required": [
"title"
],
"type": "object",
"properties": {
"title": {
"type": "string"
},
"file": {
"type": "string",
"format": "binary"
}
}
},
"HelloDto": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"file": {
"type": "string",
"format": "byte"
}
}
}
}
}
}

0 comments on commit cf12547

Please sign in to comment.