hermes-agent/web/src
Ben Barclay c661634537
fix(dashboard): stream file uploads via multipart instead of base64 JSON (NS-501) (#47663)
* fix(dashboard): stream file uploads via multipart instead of base64 JSON

The dashboard file manager uploaded files (including backup/restore zip
archives) by reading them client-side with FileReader.readAsDataURL and
POSTing a base64 data URL inside a JSON body to /api/files/upload. For a
large backup this (a) inflates the payload ~33%, (b) buffers the whole
file plus its decoded copy in memory, and (c) reliably trips an upstream
proxy body-size/timeout limit, surfacing as a 502 with the upload
appearing to hang indefinitely (NS-501). Dashboard-only hosted users have
no shell fallback to place the archive, so backup restore was unusable.

Add a streaming multipart endpoint POST /api/files/upload-stream
(UploadFile + Form) that reads the request body in 1 MiB chunks straight
to a sibling temp file, enforces the existing 100 MB size cap as it
streams (413 on overflow, before buffering the whole file), and
atomically renames into place so a partial/aborted/over-limit upload
never clobbers an existing file. The frontend api.uploadFile now sends
multipart/form-data (raw bytes, no base64, browser-set boundary) and
FilesPage passes the File object directly; the dead readAsDataUrl helper
is removed. The legacy base64 JSON endpoint stays for backward compat.

FastAPI's UploadFile/Form require python-multipart, which is NOT pulled in
by fastapi itself, so it is added to the base deps, the [web] extra, and
the tool.dashboard lazy-install set (kept in sync).

Validated: 5 new endpoint tests (roundtrip, multi-chunk >1 MiB,
over-limit 413 without clobbering + no temp-file leak, overwrite=false
conflict, forced-root traversal containment); existing base64 tests still
pass; web typecheck + vite build clean; and a real uvicorn server E2E
(5 MB multipart upload -> HTTP 200 in 0.21s, exact byte match) plus a
30 MB TestClient roundtrip confirm constant-memory streaming end to end.

Reported via beta (NS-501).

* build(deps): regenerate uv.lock for python-multipart (NS-501)

CI ran uv lock --check / uv sync --locked which failed because the
python-multipart dependency add was not reflected in uv.lock. Regenerate
the lockfile (resolves to 0.0.20, matching the [web] extra pin) after
merging current main.
2026-06-18 15:54:32 +10:00
..
components fix(dashboard): scope chat sidebar model card to selected profile (#46665) 2026-06-15 12:50:19 -04:00
contexts Hide hosted dashboard update controls 2026-06-15 20:08:39 -07:00
hooks Merge remote-tracking branch 'origin/main' into refactor/use-ds-primitives 2026-05-28 14:20:49 -04:00
i18n feat(dashboard): clone profiles from any source 2026-06-13 07:33:58 -07:00
lib fix(dashboard): stream file uploads via multipart instead of base64 JSON (NS-501) (#47663) 2026-06-18 15:54:32 +10:00
pages fix(dashboard): stream file uploads via multipart instead of base64 JSON (NS-501) (#47663) 2026-06-18 15:54:32 +10:00
plugins fix(dashboard): sanction plugin WS/upload auth via SDK helpers (gated mode) 2026-06-03 16:59:36 -07:00
themes feat(dashboard): change UI font from the theme picker, independent of theme (#41145) 2026-06-07 03:39:01 -07:00
App.tsx Hide hosted dashboard update controls 2026-06-15 20:08:39 -07:00
index.css feat(dashboard): nous-blue theme, bulk sessions, schedule picker (#37383) 2026-06-02 12:37:40 -04:00
main.tsx fix(dashboard): remove country flags from language picker (#29997) 2026-05-21 13:10:52 -07:00