Transcription block saves silently lost when navigating away in production #204

Closed
opened 2026-04-07 11:09:01 +02:00 by marcel · 0 comments
Owner

Context

TranscriptionEditView.svelte uses navigator.sendBeacon() in flushViaBeacon() (line 231-238) to flush pending transcription block edits on beforeunload. The beacon sends a PUT request to /api/documents/${documentId}/transcription-blocks/${blockId}.

This works in dev mode but silently fails in production.

Impact

Users lose unsaved transcription text when navigating away or closing the tab. No error is shown -- the data is silently dropped.

Root Cause

The app has two different /api routing paths depending on the environment:

Dev mode (works): Vite's dev server proxy (vite.config.ts:16-33) intercepts all /api requests -- including sendBeacon -- and forwards them to Spring Boot, injecting the Authorization header from the auth_token cookie.

Production (broken): SvelteKit serves the app. Client-side fetch() calls to /api/... are intercepted by SvelteKit's handleFetch hook (hooks.server.ts:63-101), which reads the auth_token cookie and adds the Authorization header before proxying to the backend. However, navigator.sendBeacon() is a raw browser API -- it does not go through SvelteKit's handleFetch hook. It sends the request directly to the SvelteKit server as a regular HTTP request. Since there is no +server.ts route matching /api/documents/[id]/transcription-blocks/[blockId], SvelteKit returns a 404.

The existing API route files confirm the gap -- only these proxy routes exist:

  • frontend/src/routes/api/documents/[id]/file/+server.ts
  • frontend/src/routes/api/persons/+server.ts
  • frontend/src/routes/api/tags/+server.ts

There is no +server.ts for transcription-blocks.

Relevant Files

  • frontend/src/lib/components/TranscriptionEditView.svelte:231-238 -- flushViaBeacon()
  • frontend/src/hooks.server.ts:63-101 -- handleFetch (only applies to SvelteKit-managed fetch)
  • frontend/vite.config.ts:16-33 -- Vite dev proxy (masks the bug in dev)
  • frontend/src/routes/documents/[id]/+page.svelte:79-85 -- regular saveBlock() via fetch (works correctly)

Reproduction Steps

  1. Build and run the frontend in production mode (npm run build && npm run preview)
  2. Open a document with transcription blocks
  3. Edit a block's text
  4. Navigate away or close the tab before the debounce timer fires
  5. Return to the document -- the edit is lost

Proposed Fix

Add a SvelteKit API proxy route at frontend/src/routes/api/documents/[id]/transcription-blocks/[blockId]/+server.ts that forwards PUT requests to the backend with the Authorization header, matching the pattern used by the existing proxy routes.

Alternatively, consider whether all /api paths need a catch-all proxy route (frontend/src/routes/api/[...path]/+server.ts) to avoid this class of bug recurring for other client-side fetch or beacon calls.

## Context `TranscriptionEditView.svelte` uses `navigator.sendBeacon()` in `flushViaBeacon()` (line 231-238) to flush pending transcription block edits on `beforeunload`. The beacon sends a PUT request to `/api/documents/${documentId}/transcription-blocks/${blockId}`. This works in **dev mode** but **silently fails in production**. ## Impact Users lose unsaved transcription text when navigating away or closing the tab. No error is shown -- the data is silently dropped. ## Root Cause The app has two different `/api` routing paths depending on the environment: **Dev mode (works):** Vite's dev server proxy (`vite.config.ts:16-33`) intercepts all `/api` requests -- including `sendBeacon` -- and forwards them to Spring Boot, injecting the `Authorization` header from the `auth_token` cookie. **Production (broken):** SvelteKit serves the app. Client-side `fetch()` calls to `/api/...` are intercepted by SvelteKit's `handleFetch` hook (`hooks.server.ts:63-101`), which reads the `auth_token` cookie and adds the `Authorization` header before proxying to the backend. However, `navigator.sendBeacon()` is a raw browser API -- it does **not** go through SvelteKit's `handleFetch` hook. It sends the request directly to the SvelteKit server as a regular HTTP request. Since there is no `+server.ts` route matching `/api/documents/[id]/transcription-blocks/[blockId]`, SvelteKit returns a **404**. The existing API route files confirm the gap -- only these proxy routes exist: - `frontend/src/routes/api/documents/[id]/file/+server.ts` - `frontend/src/routes/api/persons/+server.ts` - `frontend/src/routes/api/tags/+server.ts` There is no `+server.ts` for transcription-blocks. ## Relevant Files - `frontend/src/lib/components/TranscriptionEditView.svelte:231-238` -- `flushViaBeacon()` - `frontend/src/hooks.server.ts:63-101` -- `handleFetch` (only applies to SvelteKit-managed fetch) - `frontend/vite.config.ts:16-33` -- Vite dev proxy (masks the bug in dev) - `frontend/src/routes/documents/[id]/+page.svelte:79-85` -- regular `saveBlock()` via fetch (works correctly) ## Reproduction Steps 1. Build and run the frontend in production mode (`npm run build && npm run preview`) 2. Open a document with transcription blocks 3. Edit a block's text 4. Navigate away or close the tab before the debounce timer fires 5. Return to the document -- the edit is lost ## Proposed Fix Add a SvelteKit API proxy route at `frontend/src/routes/api/documents/[id]/transcription-blocks/[blockId]/+server.ts` that forwards PUT requests to the backend with the `Authorization` header, matching the pattern used by the existing proxy routes. Alternatively, consider whether all `/api` paths need a catch-all proxy route (`frontend/src/routes/api/[...path]/+server.ts`) to avoid this class of bug recurring for other client-side fetch or beacon calls.
marcel added the bug label 2026-04-07 11:09:07 +02:00
Sign in to join this conversation.
No Label bug
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#204