Skip to content

fix(materials): stop losing uploaded files on redeploy (502 on file view)#16

Merged
veniplex merged 1 commit into
mainfrom
claude/fix-material-file-502
Jul 16, 2026
Merged

fix(materials): stop losing uploaded files on redeploy (502 on file view)#16
veniplex merged 1 commit into
mainfrom
claude/fix-material-file-502

Conversation

@veniplex

Copy link
Copy Markdown
Owner

Summary

  • Root cause: the production container never set UPLOAD_DIR, so src/lib/storage.ts fell back to its default, path.join(process.cwd(), "data", "uploads"). Inside the runner image (WORKDIR /app), that resolves to /app/data/uploadsnot the /data/uploads path that docker-compose.yml actually mounts as a volume and that the Dockerfile mkdirs/chowns. Uploaded files were silently written outside the persisted volume.
  • Every container restart or redeploy wiped /app/data/uploads, so any material row created before that point pointed at a file that no longer existed on disk. Opening it triggered an error mid-stream (the response had already started with a 200 + Content-Length), which the reverse proxy in front of the app surfaces to the client as a 502 — matching the reported error: Unexpected server response (502) while retrieving PDF "https://study-helper.hesa.dev/api/materials/.../file".
  • Fix: set ENV UPLOAD_DIR=/data/uploads in the Dockerfile so it matches the volume mount regardless of orchestrator (Compose, k8s, plain docker run, …).
  • Defense in depth: src/app/api/materials/[id]/file/route.ts now checks the file actually exists on disk before starting to stream it, returning a clean 404 instead of an aborted connection if it's ever missing again (misconfiguration, manual deletion, etc.) — rather than a confusing 502-style failure.

Note: files that were already lost from prior redeploys before this fix can't be recovered by this change — going forward, uploads will persist correctly across restarts.

Test plan

  • npx tsc --noEmit
  • npx eslint on changed files
  • npx next build
  • After deploy, upload a material, restart/redeploy the container, and confirm the file still opens

Generated by Claude Code

UPLOAD_DIR was never set for the production container, so storage.ts
fell back to `$cwd/data/uploads` — /app/data/uploads inside the
runner image — instead of the /data/uploads volume that
docker-compose.yml actually mounts. Every restart/redeploy wiped
that directory, leaving material rows pointing at files that no
longer exist on disk. Reading one then errored mid-stream, which the
reverse proxy in front of the app turns into a 502 for the client
("Unexpected server response (502)" in the PDF viewer).

Set UPLOAD_DIR=/data/uploads in the Dockerfile to match the mkdir'd,
chown'd, volume-mounted path. Also make the file route check the
file exists before streaming, returning a clean 404 instead of an
aborted connection if it's ever missing again.
@veniplex veniplex added the release-candidate Creates a new release and deployment of docker image, once merged. label Jul 16, 2026
@veniplex
veniplex merged commit b86c23f into main Jul 16, 2026
1 of 2 checks passed
@veniplex
veniplex deleted the claude/fix-material-file-502 branch July 16, 2026 08:34
github-actions Bot added a commit that referenced this pull request Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release-candidate Creates a new release and deployment of docker image, once merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants