Skip to content

fix(uploads): clear JSON Content-Type so multipart uploads set a boundary (#32)#39

Open
ClaydeCode wants to merge 3 commits into
mainfrom
fix/clayde/multipart-upload-content-type
Open

fix(uploads): clear JSON Content-Type so multipart uploads set a boundary (#32)#39
ClaydeCode wants to merge 3 commits into
mainfrom
fix/clayde/multipart-upload-content-type

Conversation

@ClaydeCode

Copy link
Copy Markdown
Contributor

Summary

Fixes #32 — uploading an image on the Public View page (and uploading a custom app on the Apps page) failed with a 422:

{"detail":[{"type":"missing","loc":["body","file"],"msg":"Field required","input":null}]}

Root cause

$http is the shared axios instance created in src/main.js with a default Content-Type: application/json. When a FormData body is sent under that default, axios' transformRequest serializes the FormData to a JSON string (lib/defaults/index.js: hasJSONContentType ? JSON.stringify(formDataToJSON(data)) : data). The body is no longer a FormData, so the browser never sets a multipart/form-data; boundary=... header and the backend finds no file part.

  • src/views/Public.vue uploadAvatar never overrode the JSON default.
  • src/views/Apps.vue uploadCustomApp set 'Content-Type': 'multipart/form-data' with no boundary — same failure mode.

Fix

Clear the per-request Content-Type on both upload calls ({ headers: { 'Content-Type': undefined } }). With no JSON content-type, axios keeps the raw FormData; in a standard browser env axios' xhr adapter then drops the header entirely (lib/helpers/resolveConfig.js: headers.setContentType(undefined) // Let the browser set it) so the browser sets the multipart boundary. axios normalizes header case, so Content-Type cancels the Content-type default.

Tests / docs

  • New tests/unit/upload-content-type.spec.js drives the real uploadAvatar / uploadCustomApp methods with an injected $http mock and asserts they clear the Content-Type; a third test pins the axios behavior that makes undefined the correct value. Verified by mutation: reverting the override makes the tests fail.
  • agents.md documents the upload gotcha under API Communication.

Local verification: test:unit (22 passing), lint clean, build succeeds.

Recommended reading order

  1. src/views/Public.vue, src/views/Apps.vue — the fix
  2. tests/unit/upload-content-type.spec.js — the guard
  3. agents.md — the documented gotcha

Review panel

Ran three reviewers (adversarial always-on, plus test-adversary and DevEx since the diff adds real logic and tests). Security / DB / API-contract / UX did not fire — this is a client-side request-header change with no auth, schema, route, or visual surface.

  • Adversarial (correctness): No blocking findings. Verified against axios 1.8.4 source that Content-Type: undefined drops the JSON default (case-insensitively) and that no other FormData/upload call sites exist. Raised the same test-teeth advisory as below.
  • Test adversary: BLOCKING — the original test reconstructed axios inline and re-inlined the override, so deleting the fix from Public.vue/Apps.vue left all tests green (proven by mutation). Resolved in 7aa7e24: the tests now invoke the real component methods and assert on the config passed to $http; a mutation run confirms they fail when the override is removed. Advisory edge cases (multi-file loop, failure branch) — the multi-file path is now covered; the failure/doneCallback branches were left uncovered as out of scope for this fix.
  • DevEx / readability: BLOCKING — same test-teeth issue, independently found; resolved by the same rewrite. Advisories: duplicate tests (now distinct — real methods, URLs, verbs), brittle deep axios import (confined to the one mechanism test with an explanatory comment), header-casing invisibility (fixed — noted in agents.md). Praised the naming, restraint on source comments, and doc placement.

🤖 Generated with Claude Code

ClaydeCode and others added 3 commits July 22, 2026 06:26
The global $http axios instance sets a default Content-Type:
application/json. Sending a FormData body under that default makes axios
serialize the FormData to JSON, so the browser never sets the
multipart/form-data boundary and the backend can't find the file part,
returning 422.

Clear the per-request Content-Type on the avatar upload (Public.vue) and
the custom-app upload (Apps.vue) so the browser sets the multipart
boundary itself. Apps.vue previously set 'multipart/form-data' with no
boundary, which fails the same way.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Add a unit test that drives a main.js-shaped axios instance and asserts
the FormData body reaches the adapter intact when Content-Type is
cleared; a control case proves the JSON default alone serializes it to a
string. Note the requirement in agents.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The first version reconstructed an inline axios instance and re-inlined
the Content-Type override, so it verified axios behavior rather than the
app: deleting the override from Public.vue/Apps.vue left every test
green. Invoke the real uploadAvatar / uploadCustomApp methods with an
injected $http mock and assert on the config they pass, so the override
regression actually fails a test (confirmed by mutation). Keep one
axios-level test pinning why 'undefined' is the correct value to send.

Also note in agents.md that axios normalizes header case, which is the
load-bearing reason the override cancels the differently-cased default.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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.

Public View avatar upload fails with 422 (missing multipart boundary)

1 participant