88 lines
4.1 KiB
Markdown
88 lines
4.1 KiB
Markdown
# Validate `fetch()` Responses Before Parsing JSON
|
|
|
|
Priority: **Medium** · Effort: **Low** · Risk: **Low**
|
|
|
|
## Problem
|
|
|
|
Several client `fetch` call sites parse the response body as JSON **without checking `res.ok`** (or `res.status`). When the server returns an error envelope (e.g. `{ error: "Authentication required" }` with 401) or a non-JSON body (proxy 502, HTML error page), the parsed object is then **assigned directly to application state** and immediately rendered — silently corrupting the UI.
|
|
|
|
## Affected Locations
|
|
|
|
| File:Line | Call | Current behavior on error |
|
|
|-----------|------|---------------------------|
|
|
| `public/queue.js:382` | `M.library = await res.json();` then `M.renderLibrary()` | The entire library silently becomes the error object; render runs on garbage. |
|
|
| `public/auth.js:11` | guest session bootstrap | Error body used as the user object. |
|
|
| `public/auth.js:101` | `/api/auth/me` | Error body used as the current user. |
|
|
| `public/channelSync.js:11` | `/api/channels` list | Error body assigned to `M.channels`; `channels.length === 0` check then behaves confusingly. |
|
|
|
|
(Other call sites in `playlists.js`, `upload.js`, etc. already do `if (!res.ok)` correctly — follow those as the model.)
|
|
|
|
## Implementation Plan
|
|
|
|
### Step 1 — Add a shared helper in `public/utils.js`
|
|
|
|
Centralize the check so it can't be forgotten again:
|
|
|
|
```js
|
|
M.apiJson = async function (res) {
|
|
if (!res.ok) {
|
|
let detail = "";
|
|
try { detail = (await res.clone().json()).error ?? ""; } catch { /* non-JSON body */ }
|
|
const err = new Error(`Request failed (${res.status})${detail ? ": " + detail : ""}`);
|
|
err.status = res.status;
|
|
err.detail = detail;
|
|
throw err;
|
|
}
|
|
return res.json();
|
|
};
|
|
```
|
|
|
|
(`res.clone()` so the original body remains readable if the caller wants it.)
|
|
|
|
### Step 2 — Update each affected call site
|
|
|
|
**`queue.js:~380`** (library load):
|
|
|
|
```js
|
|
const res = await fetch("/api/library", { credentials: "include" });
|
|
if (res.status === 401) { M.handleAuthRequired?.(); return; } // or surface login UI
|
|
M.library = await M.apiJson(res);
|
|
M.renderLibrary();
|
|
```
|
|
|
|
Wrap the surrounding logic in `try/catch` and show a toast on failure (e.g. `M.showToast("Couldn't load library", "error")`) rather than leaving the UI empty/silent. Keep `M.library` as its previous value on failure rather than overwriting with garbage.
|
|
|
|
**`auth.js:11`** (guest bootstrap) and **`auth.js:101`** (`/api/auth/me`):
|
|
|
|
```js
|
|
const res = await fetch("/api/auth/me", { credentials: "include" });
|
|
if (res.status === 401) { /* not logged in / guest expired — drive login UI */ return; }
|
|
const user = await M.apiJson(res);
|
|
```
|
|
|
|
**`channelSync.js:11`** (channel list):
|
|
|
|
```js
|
|
const res = await fetch("/api/channels", { credentials: "include" });
|
|
if (!res.ok) { M.showToast("Couldn't load channels", "error"); return; }
|
|
const channels = await M.apiJson(res);
|
|
M.channels = channels;
|
|
```
|
|
|
|
### Step 3 — Audit remaining call sites
|
|
|
|
Grep for `await res.json()` and `await response.json()` across `public/`. Convert any that lack a preceding `res.ok` / status check to use `M.apiJson`. The ones already guarded can be left or migrated for consistency.
|
|
|
|
## Validation
|
|
|
|
- **Simulate 401**: clear the session cookie in DevTools, reload. Confirm the library view does **not** render an error object as tracks, and a login prompt / toast appears instead.
|
|
- **Simulate 500/502**: stop the server mid-session and trigger a library reload (e.g. via the refresh path). Confirm a toast shows and the previous library remains visible.
|
|
- **Happy path**: confirm normal load of library, channels, and `/api/auth/me` still works after the change.
|
|
- **Guest expiry**: let a guest session lapse (or delete it from the DB) and reload; confirm graceful handling rather than a broken render.
|
|
|
|
## Risk / Rollback
|
|
|
|
- Behavior change only on the error path; success path is identical.
|
|
- One subtlety: previously-silent failures will now surface as toasts. That is the desired behavior, but verify the messages are user-friendly and not noisy in normal flaky-network conditions.
|
|
- Rollback = revert the helper and the four call sites; no data or schema impact.
|