Files
coolify/resources/js/livewire-request-failure.js
Andras Bacsai c1219576cf feat(livewire): handle infrastructure request failures with toast
Add a local preview route and test coverage for proxy-style failures, suppressing raw Livewire error responses and showing a user-friendly toast after gestures.
2026-08-23 13:51:22 +02:00

69 lines
2.6 KiB
JavaScript

export const INFRASTRUCTURE_FAILURE_STATUSES = new Set([502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 527, 530]);
const USER_GESTURE_EVENTS = ['click', 'submit', 'keydown', 'input', 'change'];
// A request sent within this window of a trusted user gesture is treated as
// user-initiated. It must cover Alpine $nextTick deferrals and wire:model
// debounces, while staying short enough to exclude most wire:poll requests.
export const GESTURE_WINDOW_MS = 2_000;
const WARN_COOLDOWN_MS = 10_000;
const WARN_CONTENT_MAX_LENGTH = 2_000;
export function createLivewireRequestFailureHandler({ now = Date.now } = {}) {
let lastWarnAt = Number.NEGATIVE_INFINITY;
let lastToastGestureAt = Number.NEGATIVE_INFINITY;
return ({ status, content, preventDefault, gestureAt = Number.NEGATIVE_INFINITY }) => {
if (!INFRASTRUCTURE_FAILURE_STATUSES.has(status)) {
return;
}
preventDefault();
const currentTime = now();
if (currentTime - lastWarnAt >= WARN_COOLDOWN_MS) {
lastWarnAt = currentTime;
console.warn('Livewire request failed', {
status,
content: typeof content === 'string' ? content.slice(0, WARN_CONTENT_MAX_LENGTH) : content,
});
}
// One toast per user gesture: a single click that fails several
// component requests toasts once, while a retry (a new gesture)
// always toasts again. Background requests carry no gesture.
if (gestureAt > lastToastGestureAt) {
lastToastGestureAt = gestureAt;
window.toast?.('Action could not be completed', {
type: 'danger',
description: 'Coolify did not receive a response. Please try again.',
});
}
};
}
export function registerLivewireRequestFailureHandler(Livewire, documentObject = document, { now = Date.now } = {}) {
let lastGestureAt = Number.NEGATIVE_INFINITY;
const markUserGesture = (event) => {
if (event.isTrusted) {
lastGestureAt = now();
}
};
USER_GESTURE_EVENTS.forEach((eventName) => {
documentObject.addEventListener(eventName, markUserGesture, true);
});
const handleFailure = createLivewireRequestFailureHandler({ now });
Livewire.hook('request', ({ fail }) => {
// Classify at send time: infrastructure failures (522/524) can arrive
// long after the gesture, so the failure timestamp is meaningless.
const gestureAt = now() - lastGestureAt <= GESTURE_WINDOW_MS ? lastGestureAt : Number.NEGATIVE_INFINITY;
fail((failure) => handleFailure({ ...failure, gestureAt }));
});
}