Production QA reported "Submit failed 413" on /generation and plain "Upload failed" (no detail) on resource upload. Root cause is the outer reverse-proxy nginx on the VPS — still on default `client_max_body_size 1m` — rejecting requests before they reach Odoo. The inner docker-nginx and Odoo limits were already raised in earlier commits; only the VPS proxy was left unpatched. UI side - Add `describeApiError(err, fallback, context)` helper in lib/api-client.ts. Maps common HTTP codes to human-readable, actionable toast descriptions (413 → "ask ops to raise nginx client_max_body_size", 504 → "server took too long, retry", 502 → "Odoo is restarting", 401 → "sign in again", 4xx/5xx → server error body + status code). - Use it in GenerationPage submit, ResourceManager upload, TeacherLibrary upload, CustomExamCreate save/publish. Replaces four slightly-different ad-hoc mappings with one source of truth. Deploy side - Add deploy/nginx-vps.conf.example: full TLS reverse-proxy template with the body/timeout knobs that must match Odoo's limit_request (128 MB) and limit_time_real (900 s). - Add docs/DEPLOY_413_504_FIX.md: step-by-step remediation guide for the team lead covering the 3 layers that can block large bodies (outer nginx, Cloudflare, Odoo worker) and how to verify each. Made-with: Cursor
4.2 KiB
Why "Submit failed 413" and "Upload failed" still happen in production
QA keeps reporting that the following two actions fail on the deployed VPS while working locally:
- Resource Manager → Upload: toast shows "Upload failed" (no details).
- Generation → Submit module as exam for approval: toast shows "Submit failed — 413".
Both are the same root cause: the outer nginx reverse proxy on the VPS
(the one terminating TLS in front of the docker stack) is still using its
default client_max_body_size 1m and short proxy timeouts. We already
raised the inner limits in:
odoo.conf/odoo-docker.conf→limit_request 134217728(128 MB) +limit_time_real 900sfrontend/Dockerfile→client_max_body_size 128m+proxy_read_timeout 900s+proxy_request_buffering off
But the outer nginx wasn't changed, so it still rejects the request
(413) or times the proxied connection (504) before traffic ever reaches
Odoo. Because the refusal happens at the proxy, nothing shows up in the
Odoo logs — only in /var/log/nginx/*.error.log.
Fix on the VPS (one-time, 2 minutes)
-
SSH to the VPS and back up the existing config:
sudo cp /etc/nginx/sites-available/encoach /etc/nginx/sites-available/encoach.bak -
Copy the template from the repo and edit the
server_name+ TLS cert paths to match the real hostnames:sudo cp deploy/nginx-vps.conf.example /etc/nginx/sites-available/encoach sudo ln -sf /etc/nginx/sites-available/encoach /etc/nginx/sites-enabled/encoach sudo nginx -t sudo systemctl reload nginxThe critical lines added are:
client_max_body_size 128m; # matches Odoo limit_request client_body_timeout 900s; proxy_read_timeout 900s; # matches Odoo limit_time_real proxy_send_timeout 900s; proxy_request_buffering off; # stream uploads, don't buffer 100% on disk -
Smoke-test:
# Upload a ~30 MB PDF through /api/resources — should return 200, not 413. # Submit a module from /generation — should return 201, not 413. tail -f /var/log/nginx/encoach.error.log # watch for "client intended to send too large body" tail -f /var/log/odoo/odoo-server.log # watch for the actual request hitting Odoo
If the 413 persists after the nginx change
The body is getting rejected by something upstream. Check in order:
- Cloudflare / WAF — Cloudflare's default free-plan body cap is 100 MB
for authenticated endpoints and as low as 1 MB on some enterprise
rules. In the dashboard → Rules → Transform / Firewall, look for
request.body.sizerestrictions. - Odoo worker (
limit_request) — verifygrep limit_request /etc/odoo/odoo.confshows at least134217728. If the config file was baked into the docker image and never rebuilt,docker compose build backend && docker compose up -d backend. ModSecurity/ CRL — some VPS providers ship ModSecurity enabled by default (SecRequestBodyLimit, default 13 MB).
If the 504 persists
504 = the upstream didn't respond in time.
- Confirm
proxy_read_timeout 900sis actually in the running config (nginx -T | grep proxy_read_timeout). - Confirm
limit_time_realinodoo.confis at least 900 (default is 120; a single AI generation with 4 modules can exceed 3 minutes). - For first-time submissions, Odoo may need to warm up its AI clients. Retry once after a minute.
What the UI now tells the user
The frontend toast has been upgraded (see lib/api-client.ts::describeApiError).
Users will now see, for example:
- 413: "The module is too large for the server to accept (HTTP 413). Try a smaller file / fewer tasks, or ask an admin to raise the nginx client_max_body_size and Odoo limit_request."
- 504: "The server took too long to respond (HTTP 504). The module may still be processing — wait a minute and reload before retrying."
- 502: "The backend is unreachable (HTTP 502). Odoo may be restarting — try again in a moment."
- 401: "Your session expired — please sign in again."
This applies to:
- Resource upload (admin + teacher library)
- Exam generation submit
- Custom-exam publish / save draft