Skip to content

fix: pass explicit filename in multipart upload to prevent 422 in server runtimes#197

Open
ossaidqadri wants to merge 2 commits intomistralai:mainfrom
ossaidqadri:fix/files-upload-blob-multipart-encoding
Open

fix: pass explicit filename in multipart upload to prevent 422 in server runtimes#197
ossaidqadri wants to merge 2 commits intomistralai:mainfrom
ossaidqadri:fix/files-upload-blob-multipart-encoding

Conversation

@ossaidqadri
Copy link
Copy Markdown

@ossaidqadri ossaidqadri commented Mar 23, 2026

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 File objects produced by request.formData(). This causes value instanceof Blob inside appendForm to return false for legitimate File instances. The code then falls through to:

fd.append(key, String(value)); // → "[object File]"

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 Blob to a native Blob via arrayBuffer() before passing to appendForm:

const nativeBlob = payload.file instanceof Blob
  ? payload.file
  : new Blob([await (payload.file as Blob).arrayBuffer()], {
      type: (payload.file as Blob).type,
    });

Same-realm Blob/File instances are passed through unchanged — no performance impact for the common case.

2. Explicit filename (with empty-string guard)

Extract and pass file.name explicitly to appendForm, ensuring Content-Disposition always includes filename= regardless of runtime behavior. Empty strings are normalized to undefined so appendForm's truthy check doesn't silently drop the filename:

const fileName = "name" in payload.file
    && typeof (payload.file as { name: unknown }).name === "string"
    && (payload.file as { name: string }).name !== ""
  ? (payload.file as { name: string }).name
  : undefined;

Test plan

  • bunx vitest run tests/v2/append-form-blob.test.ts — 9 tests pass
    • appendForm handles real File/Blob with implicit and explicit filenames
    • Wire-level: serialized multipart body contains Content-Disposition: form-data; name="file"; filename="..."
    • Cross-realm: Blob-like that fails instanceof Blob is correctly converted to native Blob
  • Full suite: bunx vitest run — no regressions (2 pre-existing failures in realtime.test.ts and structChat.test.ts are unrelated)
  • Verified against Mistral API: raw equivalent (form.append("file", file, file.name)) resolves the 422 — reference fix in ossaidqadri/otherdev-web-v2@5fc4328

…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
Copilot AI review requested due to automatic review settings March 23, 2026 10:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 name from Blob-like file inputs (when present) and pass it as the explicit fileName to appendForm in affected upload/transcription functions.
  • Add a new Vitest suite covering appendForm behavior with Blob/File inputs 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.

Comment thread src/funcs/filesUpload.ts Outdated
Comment on lines +100 to +105
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);
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +104
const fileName = "name" in payload.RequestBody.file
&& typeof (payload.RequestBody.file as { name: unknown }).name === "string"
? (payload.RequestBody.file as { name: string }).name
: undefined;
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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;

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
? (payload.file as { name: string }).name
? ((payload.file as { name: string }).name || undefined)

Copilot uses AI. Check for mistakes.
if (isBlobLike(payload.file)) {
appendForm(body, "file", payload.file);
const fileName = "name" in payload.file
&& typeof (payload.file as { name: unknown }).name === "string"
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
&& typeof (payload.file as { name: unknown }).name === "string"
&& typeof (payload.file as { name: unknown }).name === "string"
&& (payload.file as { name: string }).name !== ""

Copilot uses AI. Check for mistakes.
Comment thread tests/v2/append-form-blob.test.ts Outdated
Comment on lines +5 to +36
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");
});
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
…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
@ossaidqadri
Copy link
Copy Markdown
Author

@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!

@louis-sanna-dev
Copy link
Copy Markdown
Contributor

Hello @ossaidqadri , this is auto-generated code so it can't be fixed directly. We are working on a fix at the generation level.

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

Successfully merging this pull request may close these issues.

files.upload() sends empty file field in Node.js/Bun server environments (422 from API)

3 participants