Compare commits

..

2 Commits

Author SHA1 Message Date
ruinivist 7e02863022 fix(api): gracefully handle aborted requests
When a client closes a connection early (e.g. while `await request.text()` is parsing), it throws a `DOMException` with `name: "AbortError"`. Previously, this hit our catch block, was logged as an unhandled exception to the console via `console.error`, and returned a `500` status. This resulted in an alarming error message which looked like a server crash, but the server actually stayed up.

This commit updates `errorResponse` in `src/api.ts` to intercept `AbortError` and handle it properly. If we detect an `AbortError`, we now log a simple `499 Client Closed Request` instead, without writing out the `DOMException` stack trace.
2026-05-31 11:16:01 +00:00
ruinivist 90c2134bbe fix(api): gracefully handle aborted requests
When a client closes a connection early (e.g. while `await request.text()` is parsing), it throws a `DOMException` with `name: "AbortError"`. Previously, this hit our catch block, was logged as an unhandled exception to the console via `console.error`, and returned a `500` status. This resulted in an alarming error message which looked like a server crash, but the server actually stayed up.

This commit updates `errorResponse` in `src/api.ts` to intercept `AbortError` and handle it properly. If we detect an `AbortError`, we now log a simple `400 Bad Request` instead, without writing out the `DOMException` stack trace.
2026-05-31 02:32:08 +00:00
2 changed files with 12 additions and 42 deletions
+8 -42
View File
@@ -108,8 +108,6 @@ export function App() {
const loadVersionRef = useRef(0);
const themeSyncFrameRef = useRef<number | null>(null);
const toastTimeoutRef = useRef<number | null>(null);
const loadedUpdatedAtRef = useRef<string | null>(null);
const inFlightTitleSaveRef = useRef<Promise<void> | null>(null);
const activeDrawing = useMemo(
() => drawings.find((drawing) => drawing.id === activeId) ?? null,
@@ -143,7 +141,6 @@ export function App() {
body: JSON.stringify(nextScene),
})
.then((body) => {
loadedUpdatedAtRef.current = body.drawing.updatedAt;
setDrawings((current) => replaceMeta(current, body.drawing));
if (isManual) {
setToastMessage("Saved");
@@ -233,7 +230,6 @@ export function App() {
currentIdRef.current = drawing.id;
currentTitleRef.current = drawing.title;
latestSceneRef.current = nextScene;
loadedUpdatedAtRef.current = drawing.updatedAt;
setDrawings(sortDrawings(list));
setActiveId(drawing.id);
@@ -327,22 +323,18 @@ export function App() {
return;
}
const promise = requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
method: "PUT",
body: JSON.stringify({ title: currentTitleRef.current }),
}).then((body) => {
try {
const body = await requestJson<{ ok: true; drawing: DrawingMeta }>(`/api/drawings/${drawingId}`, {
method: "PUT",
body: JSON.stringify({ title: currentTitleRef.current }),
});
currentTitleRef.current = body.drawing.title;
loadedUpdatedAtRef.current = body.drawing.updatedAt;
setActiveTitle(body.drawing.title);
setDrawings((current) => replaceMeta(current, body.drawing));
}).catch((renameError) => {
} catch (renameError) {
setError(renameError instanceof Error ? renameError.message : "Rename failed");
}).finally(() => {
inFlightTitleSaveRef.current = null;
});
inFlightTitleSaveRef.current = promise;
await promise;
}
}, []);
const syncThemeTokens = useCallback(() => {
@@ -419,32 +411,6 @@ export function App() {
};
}, []);
useEffect(() => {
const interval = window.setInterval(() => {
void refreshList();
}, 5000);
return () => {
window.clearInterval(interval);
};
}, [refreshList]);
useEffect(() => {
if (
activeDrawing &&
loadedUpdatedAtRef.current &&
activeDrawing.updatedAt !== loadedUpdatedAtRef.current &&
!inFlightSaveRef.current &&
!inFlightTitleSaveRef.current
) {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
void loadDrawing(activeDrawing.id);
}
}, [activeDrawing, loadDrawing]);
useEffect(() => {
if (!scene || loading) {
return;
+4
View File
@@ -14,6 +14,10 @@ function errorResponse(error: unknown): Response {
return json({ ok: false, error: error.message }, { status: error.status });
}
if (error instanceof Error && error.name === "AbortError") {
return json({ ok: false, error: "Request aborted" }, { status: 499 });
}
console.error(error);
return json({ ok: false, error: "Internal server error" }, { status: 500 });
}