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
86 lines
4.0 KiB
Plaintext
86 lines
4.0 KiB
Plaintext
# EnCoach — example VPS reverse-proxy config
|
||
#
|
||
# Sits in front of the docker-compose stack (frontend:80 + backend:8069).
|
||
# Copy to /etc/nginx/sites-available/encoach, edit server_name / TLS paths,
|
||
# symlink to sites-enabled, then `nginx -t && systemctl reload nginx`.
|
||
#
|
||
# The inner frontend container already raises its own nginx limits
|
||
# (see frontend/Dockerfile). If THIS outer proxy isn't raised too, the
|
||
# outer layer returns 413/504 before traffic ever reaches Odoo, and users
|
||
# see "Submit failed 413" or "Upload failed" with no server-side log entry.
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# HTTP → HTTPS redirect (remove if you're not terminating TLS here)
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name encoach.example.com;
|
||
return 301 https://$host$request_uri;
|
||
}
|
||
|
||
# ─────────────────────────────────────────────────────────────────
|
||
# Main TLS vhost
|
||
server {
|
||
listen 443 ssl http2;
|
||
listen [::]:443 ssl http2;
|
||
server_name encoach.example.com;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/encoach.example.com/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/encoach.example.com/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
# ── CRITICAL ────────────────────────────────────────────────
|
||
# Default nginx caps request bodies at 1 MB, which triggers
|
||
# HTTP 413 on:
|
||
# * /api/resources (PDF / audio / image uploads)
|
||
# * /api/exam/generation/submit (full module payload — can
|
||
# easily be 2–10 MB when all tasks are filled)
|
||
# * /api/approval-requests (attachments on approval flow)
|
||
# Match this value to Odoo's `limit_request` (128 MB).
|
||
client_max_body_size 128m;
|
||
client_body_buffer_size 1m;
|
||
client_body_timeout 900s;
|
||
large_client_header_buffers 4 16k;
|
||
|
||
# ── Static / SPA served by the frontend container ───────────
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8080; # docker-compose: frontend:80
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
|
||
# ── Odoo API: AI calls can take minutes, needs long timeouts ─
|
||
# Must exceed Odoo's limit_time_real (900s in odoo-docker.conf).
|
||
# proxy_request_buffering off streams the body to Odoo instead of
|
||
# buffering it entirely on disk → less chance of hitting the 413
|
||
# when uploading big resources.
|
||
location /api/ {
|
||
proxy_pass http://127.0.0.1:8069/api/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_http_version 1.1;
|
||
proxy_read_timeout 900s;
|
||
proxy_send_timeout 900s;
|
||
proxy_connect_timeout 60s;
|
||
proxy_request_buffering off;
|
||
proxy_buffering off; # stream AI responses to the client
|
||
}
|
||
|
||
# ── Odoo web client + longpolling (optional, for /web/* URLs) ─
|
||
location /longpolling/ {
|
||
proxy_pass http://127.0.0.1:8069/longpolling/;
|
||
proxy_http_version 1.1;
|
||
proxy_read_timeout 3600s;
|
||
proxy_send_timeout 3600s;
|
||
}
|
||
|
||
# Access / error logs — keep them separate so 413s are easy to spot
|
||
access_log /var/log/nginx/encoach.access.log;
|
||
error_log /var/log/nginx/encoach.error.log warn;
|
||
}
|