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:
@@ -40,6 +40,64 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn an arbitrary thrown value from an API mutation into a human-readable,
|
||||
* actionable toast description. Deployment QA kept reporting generic
|
||||
* "Submit failed" / "Upload failed" toasts — this helper surfaces the HTTP
|
||||
* status and, when we recognise the code, a hint about what to do next
|
||||
* (payload too big → ask ops to raise nginx, timeout → retry, etc.).
|
||||
*
|
||||
* @param err the error thrown by an `api.*` call (usually an ApiError)
|
||||
* @param fallback text to show if we can't classify the error
|
||||
* @param context short context word used in the 413/504 hints
|
||||
* ("file", "request", "payload" …)
|
||||
*/
|
||||
export function describeApiError(
|
||||
err: unknown,
|
||||
fallback = "Something went wrong",
|
||||
context = "request",
|
||||
): string {
|
||||
const e = err as { status?: number; message?: string; data?: unknown } | null;
|
||||
const status = e?.status;
|
||||
// Try to extract a server-side error body even if the generic ApiError
|
||||
// message wasn't populated (nginx 413/504 respond with an HTML page so
|
||||
// `data.error` is empty and `message` looks like "413 ").
|
||||
const serverMsg =
|
||||
e?.data && typeof e.data === "object" && "error" in (e.data as object)
|
||||
? String((e.data as { error: unknown }).error || "").trim()
|
||||
: "";
|
||||
|
||||
if (status === 413) {
|
||||
return `The ${context} 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\`.`;
|
||||
}
|
||||
if (status === 504) {
|
||||
return `The server took too long to respond (HTTP 504). The ${context} may still be processing — wait a minute and reload before retrying.`;
|
||||
}
|
||||
if (status === 502) {
|
||||
return "The backend is unreachable (HTTP 502). Odoo may be restarting — try again in a moment.";
|
||||
}
|
||||
if (status === 401) {
|
||||
return "Your session expired — please sign in again.";
|
||||
}
|
||||
if (status === 403) {
|
||||
return serverMsg || "You don't have permission to do that (HTTP 403).";
|
||||
}
|
||||
if (status === 404) {
|
||||
return serverMsg || "The requested item no longer exists (HTTP 404).";
|
||||
}
|
||||
if (status === 422 || status === 400) {
|
||||
return serverMsg || `The server rejected the ${context} as invalid (HTTP ${status}).`;
|
||||
}
|
||||
if (status && status >= 500) {
|
||||
return `${serverMsg || "The server returned an error"} (HTTP ${status}). Check the Odoo logs for a stack trace.`;
|
||||
}
|
||||
|
||||
// Network failure / fetch aborted / CORS — ApiError not thrown at all.
|
||||
if (!status && e?.message) return e.message;
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const ACCESS_KEY = "encoach_token";
|
||||
const REFRESH_KEY = "encoach_refresh_token";
|
||||
const EXP_KEY = "encoach_token_exp";
|
||||
|
||||
Reference in New Issue
Block a user