fix: pass explicit filename in multipart upload to prevent 422 in server runtimes#197
Conversation
…unctions When a File object is passed to files.upload(), audioTranscriptions, or betaLibrariesDocuments.upload(), the isBlobLike branch was calling appendForm without a fileName argument. Some server-side runtimes (Bun, Next.js App Router) do not implicitly include the filename in the multipart Content-Disposition header when fd.append is called without an explicit filename, causing the Mistral API to return 422 with "file field missing". Fix: extract the name property from the Blob-like value and pass it explicitly to appendForm in all four affected upload functions. Fixes mistralai#196
There was a problem hiding this comment.
Pull request overview
This PR addresses multipart upload failures in certain server runtimes by ensuring an explicit filename is provided when appending Blob/File values to FormData, preventing 422 responses caused by missing filename in multipart Content-Disposition.
Changes:
- Extract
namefrom Blob-likefileinputs (when present) and pass it as the explicitfileNametoappendFormin affected upload/transcription functions. - Add a new Vitest suite covering
appendFormbehavior withBlob/Fileinputs and explicit vs implicit filenames.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/funcs/filesUpload.ts |
Pass explicit filename when payload.file is Blob-like and exposes name. |
src/funcs/audioTranscriptionsComplete.ts |
Same explicit-filename handling for transcription (complete). |
src/funcs/audioTranscriptionsStream.ts |
Same explicit-filename handling for transcription (stream). |
src/funcs/betaLibrariesDocumentsUpload.ts |
Same explicit-filename handling for beta library document uploads. |
tests/v2/append-form-blob.test.ts |
Adds tests for appendForm behavior with Blob/File and filename overrides. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (isBlobLike(payload.file)) { | ||
| appendForm(body, "file", payload.file); | ||
| const fileName = "name" in payload.file | ||
| && typeof (payload.file as { name: unknown }).name === "string" | ||
| ? (payload.file as { name: string }).name | ||
| : undefined; | ||
| appendForm(body, "file", payload.file, fileName); |
There was a problem hiding this comment.
appendForm only uses the fileName argument when it’s truthy (value instanceof Blob && fileName). If a runtime produces an empty-string name for a File-like object, this logic will pass "" and appendForm will silently fall back to fd.append(key, value)—reintroducing the missing-filename issue. Consider normalizing empty/whitespace-only names to undefined or providing a default filename (or update appendForm to check fileName !== undefined).
| const fileName = "name" in payload.RequestBody.file | ||
| && typeof (payload.RequestBody.file as { name: unknown }).name === "string" | ||
| ? (payload.RequestBody.file as { name: string }).name | ||
| : undefined; |
There was a problem hiding this comment.
appendForm only applies fileName when it’s truthy. If payload.RequestBody.file.name can be "", the current extraction will pass an empty string and appendForm will ignore it, falling back to an append-without-filename call. Consider guarding against empty names (or update appendForm to treat empty string as a valid filename / check fileName !== undefined).
| const fileName = "name" in payload.RequestBody.file | |
| && typeof (payload.RequestBody.file as { name: unknown }).name === "string" | |
| ? (payload.RequestBody.file as { name: string }).name | |
| : undefined; | |
| const fileName = | |
| "name" in payload.RequestBody.file && | |
| typeof (payload.RequestBody.file as { name: unknown }).name === "string" && | |
| (payload.RequestBody.file as { name: string }).name.trim() !== "" | |
| ? (payload.RequestBody.file as { name: string }).name | |
| : undefined; |
| appendForm(body, "file", payload.file); | ||
| const fileName = "name" in payload.file | ||
| && typeof (payload.file as { name: unknown }).name === "string" | ||
| ? (payload.file as { name: string }).name |
There was a problem hiding this comment.
appendForm only uses fileName when it’s truthy; if payload.file.name is an empty string, the filename override won’t be applied and the multipart part may again be missing a filename in some runtimes. Consider normalizing empty names or adjusting appendForm’s fileName check to fileName !== undefined.
| ? (payload.file as { name: string }).name | |
| ? ((payload.file as { name: string }).name || undefined) |
| if (isBlobLike(payload.file)) { | ||
| appendForm(body, "file", payload.file); | ||
| const fileName = "name" in payload.file | ||
| && typeof (payload.file as { name: unknown }).name === "string" |
There was a problem hiding this comment.
Because appendForm checks value instanceof Blob && fileName (truthy), an empty-string payload.file.name would result in appending without an explicit filename, which is the scenario this PR is trying to avoid. Consider guarding against empty names here (or changing appendForm to check fileName !== undefined).
| && typeof (payload.file as { name: unknown }).name === "string" | |
| && typeof (payload.file as { name: unknown }).name === "string" | |
| && (payload.file as { name: string }).name !== "" |
| it("appends a real File without an explicit fileName, preserving the file's own name", () => { | ||
| const fd = new FormData(); | ||
| const file = new File(["pdf content"], "document.pdf", { type: "application/pdf" }); | ||
|
|
||
| appendForm(fd, "file", file); | ||
|
|
||
| const value = fd.get("file"); | ||
| expect(value).toBeInstanceOf(File); | ||
| expect((value as File).name).toBe("document.pdf"); | ||
| }); | ||
|
|
||
| it("appends a real File with an explicit fileName, using the explicit name", () => { | ||
| const fd = new FormData(); | ||
| const file = new File(["pdf content"], "original.pdf", { type: "application/pdf" }); | ||
|
|
||
| appendForm(fd, "file", file, "override.pdf"); | ||
|
|
||
| const value = fd.get("file"); | ||
| expect(value).toBeInstanceOf(Blob); | ||
| expect((value as File).name).toBe("override.pdf"); | ||
| }); | ||
|
|
||
| it("appends a bare Blob with explicit fileName", () => { | ||
| const fd = new FormData(); | ||
| const blob = new Blob(["data"], { type: "application/pdf" }); | ||
|
|
||
| appendForm(fd, "file", blob, "upload.pdf"); | ||
|
|
||
| const value = fd.get("file"); | ||
| expect(value).toBeInstanceOf(Blob); | ||
| expect((value as File).name).toBe("upload.pdf"); | ||
| }); |
There was a problem hiding this comment.
These tests assert the in-memory FormData.get() value/name, but the reported 422 is caused by how multipart is serialized on the wire (missing filename in Content-Disposition). Consider adding an assertion that serializes the FormData (e.g., via new Response(fd).text() in Node/undici) and checks the resulting multipart contains Content-Disposition: form-data; name="file"; filename="...", so the test actually guards the regression being fixed.
…lename Two improvements over the initial filename-passing fix: 1. Cross-realm Blob normalization: in bundled environments (Next.js/ webpack), File objects from request.formData() fail instanceof Blob checks due to module realm isolation. The isBlobLike fast path then fell through to fd.append(key, String(value)), sending "[object File]" as the field value and causing Mistral's API to return 422. Convert cross-realm Blob-like objects to native Blob via arrayBuffer() before passing to appendForm so FormData.append always receives a real Blob. 2. Empty-string filename guard: appendForm checks fileName as truthy, so an empty-string name would silently fall back to no-filename append, reintroducing the missing Content-Disposition filename. Normalize empty strings to undefined in all four upload functions. Adds wire-level tests that assert filename appears in the serialized multipart Content-Disposition header, not just in the in-memory FormData entry. Fixes mistralai#196, related to mistralai#190
|
@louis-sanna-dev Hey, could you take a look at this PR when you get a chance? It's been open for a bit and hasn't been reviewed yet. Would appreciate a review and merge if everything looks good. Thanks! |
|
Hello @ossaidqadri , this is auto-generated code so it can't be fixed directly. We are working on a fix at the generation level. |
Summary
Fixes #196, related to #190
Root Cause
When Next.js (webpack/turbopack) bundles the SDK, it runs in a separate module realm from the
Fileobjects produced byrequest.formData(). This causesvalue instanceof BlobinsideappendFormto returnfalsefor legitimateFileinstances. The code then falls through to:The file field is sent as a literal string instead of binary data, causing Mistral's API to return
422 {"type": "missing", "loc": ["file", "file"], "msg": "Field required"}.Fix
Two changes applied to all four affected upload functions (
filesUpload,audioTranscriptionsComplete,audioTranscriptionsStream,betaLibrariesDocumentsUpload):1. Cross-realm Blob normalization
Convert Blob-like objects that fail
instanceof Blobto a nativeBlobviaarrayBuffer()before passing toappendForm:Same-realm
Blob/Fileinstances are passed through unchanged — no performance impact for the common case.2. Explicit filename (with empty-string guard)
Extract and pass
file.nameexplicitly toappendForm, ensuringContent-Dispositionalways includesfilename=regardless of runtime behavior. Empty strings are normalized toundefinedsoappendForm's truthy check doesn't silently drop the filename:Test plan
bunx vitest run tests/v2/append-form-blob.test.ts— 9 tests passappendFormhandles realFile/Blobwith implicit and explicit filenamesContent-Disposition: form-data; name="file"; filename="..."instanceof Blobis correctly converted to native Blobbunx vitest run— no regressions (2 pre-existing failures inrealtime.test.tsandstructChat.test.tsare unrelated)form.append("file", file, file.name)) resolves the 422 — reference fix in ossaidqadri/otherdev-web-v2@5fc4328