{"record":{"id":"b0a3b1c61657f6e6","repo":"coleam00/Archon","slug":"invalid-multipart-form-data","errorCode":null,"errorMessage":"Invalid multipart form data","messagePattern":"Invalid multipart form data","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"packages/server/src/routes/api.ts","lineNumber":2893,"sourceCode":"    // Reject conversation IDs that could be used for path traversal when building\n    // the upload directory. Web conversation IDs are alphanumeric with hyphens only.\n    if (!/^[\\w-]+$/.test(conversationId)) {\n      return c.json({ error: 'Invalid conversation ID' }, 400);\n    }\n\n    let message: string;\n    let savedFiles: AttachedFile[] = [];\n    let uploadDir = '';\n\n    const contentType = c.req.header('content-type') ?? '';\n\n    if (contentType.includes('multipart/form-data')) {\n      let body: Record<string, string | File | (string | File)[]>;\n      try {\n        body = await c.req.parseBody({ all: true });\n      } catch (parseErr: unknown) {\n        getLog().warn({ err: parseErr, conversationId }, 'upload.parse_failed');\n        return c.json({ error: 'Invalid multipart form data' }, 400);\n      }\n\n      const rawMessage = body.message;\n      if (typeof rawMessage !== 'string' || !rawMessage) {\n        return c.json({ error: 'message must be a non-empty string' }, 400);\n      }\n      message = rawMessage;\n\n      const rawFiles = body.files;\n      let fileList: (string | File)[];\n      if (Array.isArray(rawFiles)) {\n        fileList = rawFiles;\n      } else if (rawFiles !== undefined) {\n        fileList = [rawFiles];\n      } else {\n        fileList = [];\n      }\n","sourceCodeStart":2875,"sourceCodeEnd":2911,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/server/src/routes/api.ts#L2875-L2911","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: hand-built multipart\nconst res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'multipart/form-data; boundary=xyz' }, body: str });\n// after: let the encoder set the boundary\nconst form = new FormData();\nform.append('message', 'hi');\nform.append('file', new File([buf], 'a.txt'));\nconst res = await fetch(url, { method: 'POST', body: form });","handlingStrategy":"validation","validationCode":"// client-side: build a real multipart body, never hand-write it\nconst form = new FormData();\nform.append('message', message);\nfor (const f of files) form.append('files', f, f.name);\nif (!message || typeof message !== 'string') throw new Error('message must be a non-empty string');\n// do NOT set Content-Type manually; fetch adds the correct boundary","typeGuard":"function looksMultipart(header: string | null): boolean {\n  return !!header && /multipart\\/form-data;\\s*boundary=/.test(header);\n}\n// verify the request you are about to send:\n// looksMultipart(req.headers.get('content-type'))","tryCatchPattern":"try {\n  const res = await sendUpload(form);\n  if (res.status === 400 && (await res.json()).error === 'Invalid multipart form data') {\n    throw new Error('Client produced a malformed multipart body — fix the encoder, not the payload');\n  }\n} catch (err) { /* log full request headers + body length for diagnosis */ }","preventionTips":["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."],"tags":["http-400","multipart","request-validation","client-error"],"backgroundTag":"invalid-multipart-form-data","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}