mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
A failed Turbo response used to be rendered: the error page drawn as the page (or merged under it — a picked PDF avatar left plMail's 500 hanging off the settings screen), or "Content missing" in a frame. - request_errors.js catches every Turbo visit, form and frame. A failed response is not rendered; the page stays as it was and an error toast says what happened. 422 (a form with its errors), 401/403 (the session guard) and elements marked data-turbo-errors="self" (the compose dock) are left alone. A network failure gets its own words; a frame response of the wrong shape is visited as a page. - requestFailed() gives controllers' own fetch() calls the same toast. An audit of the 60 calls fixed the 22 files whose user-initiated requests failed silently (star, archive, labels, drag and drop, bulk actions, reorders, appearance save, undo/cancel send, and more), and left background polling and preferences quiet on purpose. - Every request gets an 8-hex id: X-Request-Id on the response, `id` on its log entries, "Ref. …" in the toast with a copy button, and a Reference filter in Admin → Logs. A proxy's own id is kept. - toast.js clones the server's toast partial, so a script's toast is the same pane and theme as a streamed one. The sync button uses it, and its hardcoded English is now translated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
4.6 KiB
JavaScript
133 lines
4.6 KiB
JavaScript
/**
|
|
* A request that fails says so in a toast, and leaves the page alone.
|
|
*
|
|
* WHY THIS EXISTS
|
|
* ───────────────
|
|
* Turbo's answer to a failed response is to RENDER it. For a page visit or a
|
|
* form that is the error page drawn as the new page — and when the page Turbo
|
|
* was on and the error page it drew got merged, the settings screen came back
|
|
* with plMail's 500 page hanging off the bottom of it, which is how a picked
|
|
* PDF avatar was reported. A frame whose response is an error gets Turbo's
|
|
* "Content missing" written into it instead. Neither says what happened, and
|
|
* both break the screen the user was using.
|
|
*
|
|
* So every Turbo fetch passes through here first. A response that failed is
|
|
* not rendered: the page stays exactly as it was, and a toast says what went
|
|
* wrong, with the request's reference (RequestIdSubscriber) so the entry can
|
|
* be found in the admin log browser.
|
|
*
|
|
* WHAT IS LEFT ALONE
|
|
* ──────────────────
|
|
* - 422 — "here is the form again, with what is wrong on it". Turbo renders it
|
|
* and should; it is how every form in the app answers a refusal.
|
|
* - 401 and 403 — the session guard's (core--session-guard), and the
|
|
* controllers that asked have something more specific to say.
|
|
* - anything inside `[data-turbo-errors="self"]` — an element whose own
|
|
* controller explains its failures in place (the compose dock does). The
|
|
* attribute is the opt-out, and it is spelled out on the element rather than
|
|
* decided here, so the next one is a line of markup.
|
|
*
|
|
* `fetch()` calls a controller makes itself do not pass through Turbo; those
|
|
* report failures with requestFailed() below.
|
|
*/
|
|
import { showToast } from "./toast.js";
|
|
|
|
const LEFT_TO_RENDER = new Set([401, 403, 422]);
|
|
|
|
function words() {
|
|
return document.getElementById("toast-template-error")?.dataset ?? {};
|
|
}
|
|
|
|
function ownsItsErrors(target) {
|
|
return target instanceof Element && null !== target.closest('[data-turbo-errors="self"]');
|
|
}
|
|
|
|
/** The sentence for a status, or for no response at all (status 0). */
|
|
function messageFor(status) {
|
|
const w = words();
|
|
|
|
if (0 === status) {
|
|
return w.requestErrorOffline ?? "";
|
|
}
|
|
if (404 === status || 410 === status) {
|
|
return w.requestErrorNotFound ?? "";
|
|
}
|
|
if (status >= 500) {
|
|
return w.requestErrorServer ?? "";
|
|
}
|
|
return w.requestErrorRefused ?? "";
|
|
}
|
|
|
|
/**
|
|
* Tell the user a request failed.
|
|
*
|
|
* For a controller's own `fetch()`: pass the Response (or nothing, when the
|
|
* network never answered). Answers true when there was a failure to report.
|
|
*/
|
|
export function requestFailed(response = null) {
|
|
if (null !== response && true === response.ok) {
|
|
return false;
|
|
}
|
|
|
|
const status = null === response ? 0 : response.status;
|
|
const id = response?.headers?.get("X-Request-Id") ?? null;
|
|
|
|
showToast(messageFor(status), {
|
|
type: "error",
|
|
reference: null === id ? null : {
|
|
id,
|
|
label: words().requestErrorReference ?? "%id%",
|
|
copyLabel: words().requestErrorCopy ?? "",
|
|
},
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
document.addEventListener("turbo:before-fetch-response", (event) => {
|
|
const fetchResponse = event.detail?.fetchResponse;
|
|
|
|
if (!fetchResponse || true === fetchResponse.succeeded || true === event.defaultPrevented) {
|
|
return;
|
|
}
|
|
if (LEFT_TO_RENDER.has(fetchResponse.statusCode) || ownsItsErrors(event.target)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
requestFailed(fetchResponse.response);
|
|
});
|
|
|
|
// The network never answered: no response to render, and Turbo's own reaction
|
|
// is a console error and a page that simply did not change.
|
|
document.addEventListener("turbo:fetch-request-error", (event) => {
|
|
if (ownsItsErrors(event.target)) {
|
|
return;
|
|
}
|
|
|
|
requestFailed(null);
|
|
});
|
|
|
|
// A frame whose response did not carry it. Turbo would write "Content
|
|
// missing" into the frame and throw. After the guard above, a response that
|
|
// arrives here and succeeded is a page of the wrong shape — a redirect to
|
|
// somewhere else — and is visited as the page it is, which is Turbo's own
|
|
// advice; one that failed is reported like any other.
|
|
document.addEventListener("turbo:frame-missing", (event) => {
|
|
if (ownsItsErrors(event.target)) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
const response = event.detail?.response ?? null;
|
|
|
|
if (null !== response && true === response.ok && "function" === typeof event.detail?.visit) {
|
|
event.detail.visit(response);
|
|
|
|
return;
|
|
}
|
|
|
|
requestFailed(response);
|
|
});
|