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

[BUG] JSON payload not parsed when using upload with extra fields #1306

Open
matt0x6F opened this issue Sep 27, 2024 · 8 comments
Open

[BUG] JSON payload not parsed when using upload with extra fields #1306

matt0x6F opened this issue Sep 27, 2024 · 8 comments

Comments

@matt0x6F
Copy link

Describe the bug
Ninja fails to parse the request body correctly and throws a validation error, contrary to the instructions under Upload files with extra fields.

Versions (please complete the following information):

  • Python version: 3.11.7
  • Django version: 5.1.1
  • Django-Ninja version: 1.3.0
  • Pydantic version: 2.9.2

Relevant schema

class FileMetadata(Schema):
    posts: List[int] = []
    visibility: str = "public"

API Code

@files_router.post(
    "/", response={200: FileDetails}, tags=["files"], auth=JWTAuth(permissions=StaffOnly)
)
def create_file(request: HttpRequest, metadata: FileMetadata, upload: NinjaFile[UploadedFile]):
    """
    Creates a file with or without post associations.
    """
    try:
        if metadata.visibility == "public":
            stored_name = PublicStorage().save(upload.name, upload.file)

            url = PublicStorage().url(stored_name)
        else:
            stored_name = PrivateStorage().save(upload.name, upload.file)

            url = PrivateStorage().url(stored_name)

        upload = File.objects.create(
            location=url,
            name=stored_name,
            content_type=upload.content_type,
            charset=upload.charset,
            size=upload.size,
            visibility=metadata.visibility,
        )

        if metadata:
            upload.posts.set(metadata.posts)

        return upload
    except Exception as err:
        logger.error("Error creating file", error=err)

        raise HttpError(500, "Fail to create file") from err

Request body

-----------------------------291645760626718691221248293984
Content-Disposition: form-data; name="upload"; filename="business card front.pdf"
Content-Type: application/pdf

file stuff
%%EOF

-----------------------------291645760626718691221248293984
Content-Disposition: form-data; name="metadata"; filename="blob"
Content-Type: application/json

{"posts":[2,4],"visibility":"public"}
-----------------------------291645760626718691221248293984--

Content-Type on the request is set to multipart/form-data; boundary=---------------------------291645760626718691221248293984

Response

{
	"detail": [
		{
			"type": "missing",
			"loc": [
				"body",
				"metadata"
			],
			"msg": "Field required"
		}
	]
}
@matt0x6F
Copy link
Author

To disambiguate some code here:

  • File is a db model
  • NinjaFile is from ninja import File as NinjaFile

@matt0x6F
Copy link
Author

For some extra context I use openapi-generator-cli to generate my TypeScript SDK using typescript-fetch. I can supply the code around that if it helps at all.

@matt0x6F
Copy link
Author

matt0x6F commented Sep 28, 2024

Just to double check I console.log'd what I'm sending

image

I changed it to use Form around the schema and that got me a little bit farther but it still doesn't parse correctly.

@matt0x6F
Copy link
Author

matt0x6F commented Sep 28, 2024

I was able to successfully make the request go through via the OpenAPI docs page. The payload looks a lot different:

-----------------------------35115467084627556252850040527
Content-Disposition: form-data; name="upload"; filename="business card back.pdf"
Content-Type: application/pdf

file stuff
%%EOF

-----------------------------35115467084627556252850040527
Content-Disposition: form-data; name="metadata"

{
  "posts": [
    4, 2
  ],
  "visibility": "public"
}
-----------------------------35115467084627556252850040527--

I think that means that the typescript-fetch plugin for openapi-generator-cli may be the problem. It sends an extra Content-Type: application/json and filename="blob"

@matt0x6F
Copy link
Author

Yeap, and found the bug on openapi-generator that's been open since 2020

@matt0x6F
Copy link
Author

so, they do this in every generator plugin. I think that's what's messing up the parsing and typing here.

@0nliner
Copy link

0nliner commented Oct 3, 2024

I have same problem, but I am not using openapi-generator

@0nliner
Copy link

0nliner commented Oct 3, 2024

I tried sending the request not through my application, but through Postman. I noticed that in the request that is processed correctly, the Content-Type header contains a boundary (an arbitrary string that defines the boundaries of the content).

b'-----------------------------409722013720178429992086406357\r\nContent-Disposition: form-data; name="file"; filename="\xd0\x9a\xd0\xbe\xd0\xbd\xd1\x82\xd0\xb8\xd0\xbd\xd0\xb5\xd0\xbd\xd1\x82\xd0\xb0\xd0\xbb\xd1\x8c (2).xls"\r\nContent-Type: application/vnd.ms-excel\r\n\r\n\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00...

In this case, -----------------------------409722013720178429992086406357 is our boundary, which can be found at the beginning and end of the raw request.

The method django.http.multipartparser.MultiPartParser.init looks for the boundary in the Content-Type and expects the following format in the request headers:
'Content-Type': 'multipart/form-data; boundary=---------------------------409722013720178429992086406357'

When explicitly setting a value for Content-Type, using fetch does not automatically substitute the boundary value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants