Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

WithOpenApi IFormFile support is not generating valid swagger #47692

Closed
1 task done
cleftheris opened this issue Apr 13, 2023 · 8 comments
Closed
1 task done

WithOpenApi IFormFile support is not generating valid swagger #47692

cleftheris opened this issue Apr 13, 2023 · 8 comments
Labels
✔️ Resolution: Duplicate Resolved as a duplicate of another issue old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels Status: Resolved

Comments

@cleftheris
Copy link

cleftheris commented Apr 13, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Following up on #47644 @captainsafia
When mapping any operation that has an IFormFile or IFormFileCollection parameter the OpenApi configuration generated is not valid. The content type is correctly described as multipart/form-data but there are no 'in' parameters describing what the request should look like. Therefore the swagger ui client throws an exception when trying out the operation.

So trying the example from the official docs

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

using WithOpenApi() and swagger-ui it fails with the the following exception

Microsoft.AspNetCore.Http.BadHttpRequestException: Required parameter "IFormFile file" was not provided from form file.

Although this actually works fine for calling the endpoint with postman (configured as form-data) the OpenApi spec is not accordingly describing it (Same goes for IFormFileCollection).

This is the fragment generated for single file upload

    "/upload": {
            "post": {
                "tags": [
                    "WebApplication1"
                ],
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "string",
                                "format": "binary"
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }

This is for the form file collection

       "/upload_many": {
            "post": {
                "tags": [
                    "WebApplication1"
                ],
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "type": "array",
                                "items": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },

So for the following

Expected Behavior

For this single file operation

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

the generated swagger should be the following

        "/upload": {
            "post": {
                "tags": [
                    "WebApplication1"
                ],
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "required": [
                                    "file"
                                ],
                                "type": "object",
                                "properties": {
                                    "file": {
                                        "type": "string",
                                        "format": "binary"
                                    }
                                },
                                "additionalProperties": false
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },

For this multiple files operation

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

the generated swagger should be the following

        "/upload_many": {
            "post": {
                "tags": [
                    "WebApplication1"
                ],
                "summary": "IFormFileCollection invalid OpenAPI operation parameters.",
                "requestBody": {
                    "content": {
                        "multipart/form-data": {
                            "schema": {
                                "required": [
                                    "myFiles"
                                ],
                                "type": "object",
                                "properties": {
                                    "myFiles": {
                                        "type": "array",
                                        "items": {
                                            "type": "string",
                                            "format": "binary"
                                        }
                                    }
                                },
                                "additionalProperties": false
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },

Steps To Reproduce

I have prepared a project here https://github.com/cleftheris/WithOpenApiBug
The relevant code is located in the UploadsApi.cs file

There are also some workarounds we have come up with in there as well but they also have shortcomings.

Just a footnote regarding our workarounds.

We use a request class with the Accepts<UploadModel>("multiple/form-data") and a custom BindAsync method.
This works majestically and swagger ui behaves itself and the open api is valid (yey!!).
But client code generators like NSwag choke on schema references when it comes to multipart form data requestBody types.
So for anyone who is interested in NSwag this is something to tackle with a swashbuckle operation filter (remove schema reference and copy schema parameters directly to operation request body for contentType "multipart/form-data").

Exceptions (if any)

No response

.NET Version

7.0.4

Anything else?

.NET SDK:
Version: 7.0.202
Commit: 6c74320bc3

Runtime Environment:
OS Name: Windows
OS Version: 10.0.22621
OS Platform: Windows
RID: win10-x64
Base Path: C:\Program Files\dotnet\sdk\7.0.202\

Host:
Version: 7.0.4
Architecture: x64
Commit: 0a396acafe

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates label Apr 13, 2023
@MackinnonBuck MackinnonBuck added old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels and removed area-mvc Includes: MVC, Actions and Controllers, Localization, CORS, most templates labels Apr 13, 2023
@captainsafia
Copy link
Member

@cleftheris I believe that the issue that you're reporting is a dupe of #47526 which I ended up root causing to a problem with the way schemas are merged in Swashbuckle. You can read that analysis in #47526 (comment).

The Swashbuckle issue is logged in domaindrivendev/Swashbuckle.AspNetCore#2625. I'm hoping to open a PR on the Swashbuckle repo to address this soon.

@captainsafia captainsafia added the ✔️ Resolution: Duplicate Resolved as a duplicate of another issue label Apr 13, 2023
@ghost ghost added the Status: Resolved label Apr 13, 2023
@cleftheris
Copy link
Author

cleftheris commented Apr 13, 2023

Oh my, sorry for that I was so invested in reporting I did not do a proper search.

What is amazing here is I had not realized that it worked without WithOpenApi because our code base relies on it in order to customize the operation security info. So I thought it failed regardless. I will check on the Swashbuckle front for this one.

One lesser last thing I have stumbled across is the docs regarding Stream or pipereader that it is covered here #45265 but has been silent for a while.

It generates "valid-ish" swagger.json only if I use the .Accepts<Stream>("application/octet-stream") you can use the swagger ui client but cannot possibly generate a client for any other language.

        "/stream-without-accept": {
            "post": {
                "tags": [
                    "Issues - Stream"
                ],
                "requestBody": {
                    "content": {},
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "Success"
                    }
                }
            }
        },
        "/stream-withaccept": {
            "post": {
                "tags": [
                    "Issues - Stream"
                ],
                "requestBody": {
                    "content": {
                        "application/octet-stream": {
                            "schema": {
                                "$ref": "#/components/schemas/Stream"
                            }
                        }
                    },
                    "required": true
                },
                "responses": {
                    "200": {
                        "description": "Success"
                    }
                }
            }
        },

Thanks again u rule!

@captainsafia
Copy link
Member

One lesser last thing I have stumbled across is the docs regarding Stream or pipereader that it is covered here #45265 but has been silent for a while.

Yep, you might've noticed from my comment on the issue that you linked that this is also an issue on the SB side. It looks like it's not producing the correct schema for Stream types. The bug I am tracking on the Swashbuckle side is domaindrivendev/Swashbuckle.AspNetCore#2563 which would allow using WithOpenApi to override incorrect schemas like this.

What is amazing here is I had not realized that it worked without WithOpenApi because our code base relies on it in order to customize the operation security info.

I'd be curious to learn about what you're doing in this front. You might be interested in upvoting #39761 or chiming in on that issue.

@ghost
Copy link

ghost commented Apr 15, 2023

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

@ghost ghost closed this as completed Apr 15, 2023
@davidfowl
Copy link
Member

It generates "valid-ish" swagger.json only if I use the .Accepts("application/octet-stream") you can use the swagger ui client but cannot possibly generate a client for any other language.

You're not trying to represent a stream in the swagger. That's purely a consumption time decision not one exposed to clients. The incoming data could be a Person, json formatted POCO and I could accept that body as a stream.

@cleftheris
Copy link
Author

@davidfowl I understand,

but open api client generators prefer explicit specs otherwise they usually do not do the expected thing. It seemed to me as a preferable default for the OpenApi description to generate a file upload input in swagger ui. On the other hand it would limit the actual runtime behavior.

Still it would be nice to use WithOpenApi extension to make it work as I intent without needing to go to Swashbuckle operation filters.

@davidfowl
Copy link
Member

I think we agree, I was mainly commenting on this:

It generates "valid-ish" swagger.json only if I use the .Accepts("application/octet-stream") you can use the swagger ui client but cannot possibly generate a client for any other language.

That code snippet isn't the right thing to do.

@cleftheris
Copy link
Author

cleftheris commented Apr 18, 2023

That code snippet isn't the right thing to do.

Yep it is not - I mean I agree (I am not a native speaker).

I also re-read my comment above because I did not understand either why I phrased it like that😀. I was referring to the schema ref part $ref": "#/components/schemas/Stream" (ref inception) that popped there after me adding the Accepts method to force the swagger-ui to a file input.

Thanks for taking the time to check this out @davidfowl, @captainsafia

@ghost ghost locked as resolved and limited conversation to collaborators May 18, 2023
This issue was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
✔️ Resolution: Duplicate Resolved as a duplicate of another issue old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels Status: Resolved
Projects
None yet
Development

No branches or pull requests

4 participants