Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

multipart/form-data with mixed @RequestParts fails to provide correct Content-Type in HTTP request #396

Closed
stefan-huettemann opened this issue Jan 31, 2020 · 8 comments
Labels
enhancement New feature or request

Comments

@stefan-huettemann
Copy link

Hi. Great work with springdoc-openapi!

PROBLEM

With sprindoc 1.2.29 and spring-boot 2.2.4 I run into a problem with a rest controller:

@RequestMapping(
            method = PUT,
            consumes = {MediaType.MULTIPART_FORM_DATA_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE}
    )
public ResponseEntity<?> put(
            @PathVariable("config") final String config,
            @RequestPart(value = "configuration") final Configuration configuration,
            @RequestPart(value = "file") final MultipartFile aFile) {
        // -> Configuration is any data object that will be uploaded as JSON
    }

spring-doc produces the following swagger config:

"requestBody": {
      "content": {
        "multipart/form-data": {
          "schema": {
            "type": "object",
            "properties": {
              "configuration": {
                "$ref": "#/components/schemas/Configuration"
              },
              "file": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        }
      }
    }

which results in a HTTP multipart request of the form:

PUT /sasmd/api/v1/configurations/266f8aa5-1a78-4a95-afae-2f5774cda90a HTTP/1.1

<-- redacted-->

Content-Type: multipart/form-data; boundary=---------------------------13787589471453071099581927209

<-- redacted-->

-----------------------------13787589471453071099581927209
Content-Disposition: form-data; name="configuration"

{
  "date": "2019-12-17T10:45:52.727+0100",
  "some": "json"
}
-----------------------------13787589471453071099581927209
Content-Disposition: form-data; name="file"; filename="UKF-121.zip"
Content-Type: application/zip

<file-contents-base64-removed>

-----------------------------13787589471453071099581927209--

This fails with spring boot 415: HttpMediaTypeNotSupportedException: Content type 'application/octet-stream'

The correct HTTP request would be to have a Content-Type: application/json declaration for the first part:

PUT /sasmd/api/v1/configurations/266f8aa5-1a78-4a95-afae-2f5774cda90a HTTP/1.1

<-- redacted-->

Content-Type: multipart/form-data; boundary=---------------------------13787589471453071099581927209

<-- redacted-->

-----------------------------13787589471453071099581927209
Content-Disposition: form-data; name="configuration"
Content-Type: application/json

{
  "date": "2019-12-17T10:45:52.727+0100",
  "some": "json"
}
-----------------------------13787589471453071099581927209
Content-Disposition: form-data; name="file"; filename="UKF-121.zip"
Content-Type: application/zip

<file-contents-base64-removed>

-----------------------------13787589471453071099581927209--

Questions:

  1. I have no idea what the correct swagger config would look like to provide a Content-Type: application/jsonfor the first part
  2. I fail to overwrite swagger config using swaggers own @RequestBody annotation...

Working Solution in swagger config

The following swagger config would provide a possible solution

"requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "configuration": {
                    "type": "string",
                    "format": "binary"
                  },
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        }

This result in a swagger-ui with two file-upload buttons. Using a file-upload button the content-type of the mutlipart-request gets set (if the user uploads a .JSON file for parameter "configuration").

BUT: as said above - I fail to produce any such swagger configuration using swagger annotations.

Well ... any idea how to resolve this?

Best,
-Stefan

@bnasslahsen
Copy link
Collaborator

Hi @stefan-huettemann,

It should work without any extra configuration. You just need to remove @RequestPart(value = "configuration").

@RequestMapping(
            method = PUT,
            consumes = {MediaType.MULTIPART_FORM_DATA_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE}
    )
public ResponseEntity<?> put(
            @PathVariable("config") final String config,
            final Configuration configuration,
            @RequestPart(value = "file") final MultipartFile aFile) {
        // -> Configuration is any data object that will be uploaded as JSON
    }

@stefan-huettemann
Copy link
Author

stefan-huettemann commented Feb 1, 2020

Hi @bnasslahsen

tlrd; removing the @RequestPart annotation does not work. :(

You are telling me to change my program code in order to make swagger work?

Well .. removing the spring-web annotation @RequestPart breaks the API contract:

  1. a client sending a valid multipart request fails, because springs HttpMessageConverter will not parse the JSON to a (here: "Configuration") object.

  2. it is now not possible to mark a method parameter with @RequestPart(name="some_name_different_from_method_parameter", required=false)

Your solution would work for a API like this:

public ResponseEntity<?> put(@PathVariable("config") final String config,
                                 final String configuration,
                                 @RequestPart(value = "file") final MultipartFile aFile) {

But that is not what is intended.

Somehow swagger must be able to handle a multipart request with each request-part having a defined Content-Type declaration.

I do not see any way to make spring handle a (broken?) http multipart request that fails to declare a content-type for each request part.

Looking at swagger config, the only way I can make swagger-ui work is to use two file-upload buttons and upload a .json file for the first part (see my comment above).

But then: I don't know how produce such a swagger config using swagger-annotations - which would be a good way to solve the issue ... :-(

(-> swagger-api/swagger-core#3433)

Best,
-Stefan

@bnasslahsen
Copy link
Collaborator

Hi @stefan-huettemann,

If you add on the top of your class Configuration the following code, it should produce your expected OpenAPI description:
@Schema(name ="Configuration", type = "string", format = "binary")

Is it working from the swagger-ui in your case?

@stefan-huettemann
Copy link
Author

stefan-huettemann commented Feb 2, 2020

Hi @bnasslahsen

thanks for the fast responses from your side - very helpful!

Adding the @Schema annotation to the Model-Class (Configuration) results in a "valid" swagger config:

"requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "configuration": {
                    "type": "string",
                    "format": "binary"
                  },
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        }

So part of the problem seems solved.

BUT...

This now has a side effect!

Assume we have a second service:

@RequestMapping(
            method = PUT,
            value = {"/configurations/{config}/test"},
            consumes = {MediaType.APPLICATION_JSON_VALUE},
            produces = {MediaType.APPLICATION_JSON_VALUE}
    )
    public ResponseEntity<Void> put2(@PathVariable("config") final String config,
                                     @RequestBody final Configuration configuration) {...}

Now all the occurences of Configuration will be declared as

"configuration": {
                    "type": "string",
                    "format": "binary"
                  }

(The service still works in swagger-ui! But as default model/schema only string is shown instead of the model class).

Also Configuration disapeard from the list of components in swagger configuration/swagger-ui.

Using a @ResponseBody annotation for this service like

 @Operation(
            requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                    content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
                            schema = @Schema(name = "configuration", implementation = Configuration.class))
            )
    )

does not overwrite the values for this service.

Looks like the @Schema annotation on class level is very dominant.

Any idea here? I think it would be appropriate, if each service can be independently declared as needed.

Best,
-Stefan

@bnasslahsen
Copy link
Collaborator

bnasslahsen commented Feb 2, 2020

It really depends if this annotation is valid for all the services or not.
If its not valid, you have another workaround which is to use another wrapper object which can declare all the requestBody structure, because we are not able to access to properties from @Schema swagger annotation.

I think it will be more convenient to allow @Parameter annotation to define the schema of each parameter, and springdoc-openapi will merge all the definitions into the @RequestBody properties.

I guess the following syntax, will be more convinient in the case, you don't want add the @Schema annotation on the class level.

public ResponseEntity<?> put(
		@PathVariable("config") final String config,
		@RequestPart(value = "configuration")  @Parameter(name = "configuration", schema = @Schema(name ="configuration", type = "string", format = "binary")) final Configuration configuration,
		@RequestPart(value = "file") final MultipartFile aFile)

I can add this enhancement for the next release.
Let me know if this solution is ok for you, in case you see other particular cases.

@stefan-huettemann
Copy link
Author

The @Parameter annotation for each service parameter was actually the first thing I tried and this sounds like a good solution to me.

I also thought it would be possible to define the complete swagger configuration for the request-body using the @Operation like:

@Operation(
            requestBody = @RequestBody(
                    content = @Content(
                            mediaType = MediaType.MULTIPART_FORM_DATA_VALUE,
                            schema = @Schema(
                                    type = "object"
                                    // -> properties[] is missing !!!
                            )
                    ))
    )

But somehow there is no way to add the "properties".

Do you have any idea why there is no properties attribute in @Schema annotation?

@akamom
Copy link

akamom commented Oct 25, 2021

As @stefan-huettemann mentioned. The above solution has two downsides:

  • The service still works in swagger-ui! But as the default model/schema only string is shown instead of the model class.

  • Also Configuration disappeared from the list of components in swagger configuration/swagger-ui.

Is there already a solution for those two points?

@bnasslahsen
Copy link
Collaborator

bnasslahsen commented Oct 25, 2021

@akamom,

Read related issue answers #820 ...

@springdoc springdoc locked as too heated and limited conversation to collaborators Oct 25, 2021
@bnasslahsen bnasslahsen added the enhancement New feature or request label Jan 10, 2022
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants