coleam00/Archon · warning
Invalid multipart form data
Error message
Invalid multipart form data
What it means
The conversation-message route received a multipart/form-data request but Hono's c.req.parseBody({ all: true }) threw while parsing it. The server logs upload.parse_failed (with the parse error and conversationId) and returns 400, meaning the request body is not well-formed multipart data the framework can decode.
Source
Thrown at packages/server/src/routes/api.ts:2893
// Reject conversation IDs that could be used for path traversal when building
// the upload directory. Web conversation IDs are alphanumeric with hyphens only.
if (!/^[\w-]+$/.test(conversationId)) {
return c.json({ error: 'Invalid conversation ID' }, 400);
}
let message: string;
let savedFiles: AttachedFile[] = [];
let uploadDir = '';
const contentType = c.req.header('content-type') ?? '';
if (contentType.includes('multipart/form-data')) {
let body: Record<string, string | File | (string | File)[]>;
try {
body = await c.req.parseBody({ all: true });
} catch (parseErr: unknown) {
getLog().warn({ err: parseErr, conversationId }, 'upload.parse_failed');
return c.json({ error: 'Invalid multipart form data' }, 400);
}
const rawMessage = body.message;
if (typeof rawMessage !== 'string' || !rawMessage) {
return c.json({ error: 'message must be a non-empty string' }, 400);
}
message = rawMessage;
const rawFiles = body.files;
let fileList: (string | File)[];
if (Array.isArray(rawFiles)) {
fileList = rawFiles;
} else if (rawFiles !== undefined) {
fileList = [rawFiles];
} else {
fileList = [];
}
View on GitHub (pinned to 0773b97458)
Solutions
- Rebuild the request with a proper multipart encoder (FormData in fetch/undici, -F in curl) instead of hand-writing the body.
- Ensure the Content-Type header includes the exact boundary generated by the encoder (do not set Content-Type manually with fetch + FormData).
- Verify the body is fully transmitted (Content-Length correct, no proxy truncation) and the file fields are valid.
- Log the raw body once in a test to confirm it is valid multipart before blaming the server.
Example fix
// before: hand-built multipart
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'multipart/form-data; boundary=xyz' }, body: str });
// after: let the encoder set the boundary
const form = new FormData();
form.append('message', 'hi');
form.append('file', new File([buf], 'a.txt'));
const res = await fetch(url, { method: 'POST', body: form }); Defensive patterns
Strategy: validation
Validate before calling
// client-side: build a real multipart body, never hand-write it
const form = new FormData();
form.append('message', message);
for (const f of files) form.append('files', f, f.name);
if (!message || typeof message !== 'string') throw new Error('message must be a non-empty string');
// do NOT set Content-Type manually; fetch adds the correct boundary Type guard
function looksMultipart(header: string | null): boolean {
return !!header && /multipart\/form-data;\s*boundary=/.test(header);
}
// verify the request you are about to send:
// looksMultipart(req.headers.get('content-type')) Try / catch
try {
const res = await sendUpload(form);
if (res.status === 400 && (await res.json()).error === 'Invalid multipart form data') {
throw new Error('Client produced a malformed multipart body — fix the encoder, not the payload');
}
} catch (err) { /* log full request headers + body length for diagnosis */ } Prevention
- Always build multipart with FormData or an equivalent encoder; never concatenate strings by hand.
- Never manually set the multipart Content-Type boundary with fetch/FormData.
- Test uploads end-to-end with curl -F once when adding file support to a client.
- Check proxies/gateways for body size limits that truncate large multipart payloads.
When it happens
Trigger: Sending multipart/form-data to the conversation message endpoint with a malformed body: missing/mismatched boundary, truncated body, hand-rolled multipart string instead of a real encoder, wrong Content-Length, or a Content-Type header whose boundary does not match the body.
Common situations: Custom HTTP clients/curl scripts constructing multipart manually; proxies or gateways truncating large uploads; SDK version mismatch producing a body/boundary mismatch; testing with an incomplete fetch where the File field was never appended.
Related errors
- Invalid JSON in request body
- inputs must be a JSON-encoded object of string values
- ${label} must be a JSON-encoded object
- config must be a JSON-encoded object
- Request body is not valid JSON — send {"comment": "..."} or
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/b0a3b1c61657f6e6.
Report an issue: GitHub.