Skip to content

fix: stream uploads to disk instead of buffering them in memory - #9

Merged
netomi merged 3 commits into
mainfrom
fix/stream-upload
Oct 27, 2026
Merged

netomi merged 3 commits into
mainfrom
fix/stream-upload

Conversation

@netomi

@netomi netomi commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

handleScan called r.ParseMultipartForm(config.MaxUploadSize). That argument is maxMemory, not a size cap, so with MAX_UPLOAD_SIZE_MB=1024 any upload up to 1 GiB was held entirely in the Go heap (with extra transient allocation from buffer growth) before being copied to disk. Several concurrent large extensions can exhaust the container memory limit and get the pod OOM-killed.

Change

  • Read the file part with r.MultipartReader() and io.Copy it directly into the temp file; memory use no longer depends on upload size.
  • Cap the request body with http.MaxBytesReader(MaxUploadSize), which the old code never enforced. Oversized uploads now return 413 with a JSON error.
  • The "Received file" log line no longer includes a byte count, since the size is not known up front.

Behavior is otherwise unchanged: same status codes for non-multipart (400) and missing-file (400) requests. Non-file fields in the form are skipped rather than parsed.

Tests

  • TestSaveUploadStreamsToDisk: streams a 64 MiB upload and asserts allocations stay below a quarter of its size. With the upload buffered (io.ReadAll) the same test fails (about 165 MB allocated).
  • TestSaveUploadTooLarge, TestHandleScanTooLarge: 413 and no temp file left behind.
  • Existing tests pass. The ones needing the yr binary are skipped locally, as before.

Not included

No concurrency limit, request-context cancellation or GOMEMLIMIT yet; those are separate follow-ups. clamav-rest-new has the same ParseMultipartForm pattern and would need the same change.

🤖 Generated with Claude Code

netomi and others added 3 commits October 27, 2026 07:08
ParseMultipartForm was given the upload size limit as its maxMemory
argument, so uploads up to MAX_UPLOAD_SIZE_MB were held entirely in
the Go heap. Concurrent large uploads could exhaust the container
memory limit.

Read the file part with MultipartReader and copy it straight to the
temp file, capping the body with http.MaxBytesReader. Oversized
uploads now return 413.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
handleScan writes uploads to /tmp/scans, which only exists in the
container image, so TestHandleScanTooLarge failed on a clean machine.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
io.Copy errors from the temp file (disk full, I/O error) were wrapped
as a malformed request and returned as 400. Classify only failures
reading the request body; write failures return 500 again.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@netomi
netomi merged commit ed88d00 into main Oct 27, 2026
2 checks passed
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.

1 participant