fix(ui): actionable 413/504/401 toasts + VPS nginx deploy template
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
This commit is contained in:
@@ -17,6 +17,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { resourcesService } from "@/services/resources.service";
|
||||
import { coursewareService } from "@/services/courseware.service";
|
||||
import { taxonomyService } from "@/services/taxonomy.service";
|
||||
import { describeApiError } from "@/lib/api-client";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { TaxonomyCascade } from "@/components/TaxonomyCascade";
|
||||
import type { Resource, ResourceTag } from "@/types";
|
||||
@@ -274,17 +275,16 @@ export default function ResourceManager() {
|
||||
const uploadMutation = useMutation({
|
||||
mutationFn: (formData: FormData) => resourcesService.upload(formData),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ["resources"] }); toast({ title: "Resource uploaded" }); setShowUpload(false); setUploadTagIds([]); setUploadSubjectId("none"); setUploadDomainId("none"); setUploadTopicIds([]); setUploadObjectiveIds([]); },
|
||||
onError: (err: unknown) => {
|
||||
// Surface status / server message so users can tell 413 (file too big)
|
||||
// from 401 (session expired) from 500 (server error) without opening
|
||||
// devtools. Default "Upload failed" hid real failures during QA.
|
||||
const e = err as { status?: number; message?: string };
|
||||
const status = e?.status;
|
||||
let description = e?.message || "Upload failed";
|
||||
if (status === 413) description = "File is too large for the server (HTTP 413). Ask an admin to raise nginx/odoo upload limits.";
|
||||
else if (status === 401) description = "Session expired — please sign in again.";
|
||||
else if (status === 504) description = "Server timed out while saving the file (HTTP 504). Try a smaller file or retry.";
|
||||
toast({ title: "Upload failed", description, variant: "destructive" });
|
||||
onError: (err) => {
|
||||
// describeApiError surfaces status code + actionable next step so QA /
|
||||
// ops can tell 413 (payload limit) from 504 (timeout) from 401 (session
|
||||
// expired) without opening DevTools. Default "Upload failed" was
|
||||
// reported as non-actionable during the Apr-2026 deployment.
|
||||
toast({
|
||||
title: "Upload failed",
|
||||
description: describeApiError(err, "Upload failed", "file"),
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user