From 0d7b985469df611022750deaf6d2d471e0f13fe0 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 4 Mar 2026 15:21:17 +0500 Subject: [PATCH] fix(audio): return 404 from presign when audio object not yet uploaded handlePresignAudio now checks AudioExists before presigning; previously it would generate a valid-looking signed URL for a non-existent object, causing the browser to get 404 from MinIO directly. - Add AudioExists to Store interface and HybridStore - Guard handlePresignAudio with AudioExists check, return 404 if missing - Propagate 404 through presignAudio() in minio.ts (typed error.status=404) - SvelteKit presign route forwards 404 to the browser instead of 500 --- scraper/internal/server/server.go | 8 ++++++++ scraper/internal/storage/hybrid.go | 4 ++++ scraper/internal/storage/store.go | 2 ++ ui/src/lib/server/minio.ts | 7 +++++++ ui/src/routes/api/presign/audio/+server.ts | 5 +++++ 5 files changed, 26 insertions(+) diff --git a/scraper/internal/server/server.go b/scraper/internal/server/server.go index 29c2611..9e6e173 100644 --- a/scraper/internal/server/server.go +++ b/scraper/internal/server/server.go @@ -719,6 +719,14 @@ func (s *Server) handlePresignAudio(w http.ResponseWriter, r *http.Request) { } key := s.store.AudioObjectKey(slug, n, voice, speed) + + // Return 404 when the object hasn't been uploaded yet — the client treats + // this as "audio not ready" and will either poll or trigger generation. + if !s.store.AudioExists(r.Context(), key) { + http.NotFound(w, r) + return + } + url, err := s.store.PresignAudio(r.Context(), key, 1*time.Hour) if err != nil { s.log.Error("presign audio failed", "slug", slug, "n", n, "err", err) diff --git a/scraper/internal/storage/hybrid.go b/scraper/internal/storage/hybrid.go index f7d7345..19a3ada 100644 --- a/scraper/internal/storage/hybrid.go +++ b/scraper/internal/storage/hybrid.go @@ -291,6 +291,10 @@ func (h *HybridStore) AudioObjectKey(slug string, n int, voice string, speed flo return AudioObjectKey(slug, n, voice, speed) } +func (h *HybridStore) AudioExists(ctx context.Context, key string) bool { + return h.minio.AudioExists(ctx, key) +} + // ─── PutAudio ───────────────────────────────────────────────────────────────── func (h *HybridStore) PutAudio(ctx context.Context, key string, data []byte) error { diff --git a/scraper/internal/storage/store.go b/scraper/internal/storage/store.go index 090bc65..d78a7ee 100644 --- a/scraper/internal/storage/store.go +++ b/scraper/internal/storage/store.go @@ -130,6 +130,8 @@ type Store interface { // AudioObjectKey returns the MinIO object key for a cached audio file. AudioObjectKey(slug string, n int, voice string, speed float64) string + // AudioExists returns true when the audio object is present in the bucket. + AudioExists(ctx context.Context, key string) bool // ── Presigned URLs ───────────────────────────────────────────────────── diff --git a/ui/src/lib/server/minio.ts b/ui/src/lib/server/minio.ts index 87de996..99d6480 100644 --- a/ui/src/lib/server/minio.ts +++ b/ui/src/lib/server/minio.ts @@ -64,6 +64,7 @@ export async function presignChapter(slug: string, n: number, rewrite = false): /** * Returns a presigned URL for an audio file. * URL is valid for ~1 hour. The URL is returned to the browser for direct streaming. + * Throws with { status: 404 } when the audio object has not been generated yet. */ export async function presignAudio( slug: string, @@ -83,6 +84,12 @@ export async function presignAudio( log.error('minio', 'presign audio network error', { slug, n, err: String(e) }); throw new Error(`presign audio ${slug}/${n}: network error`); } + if (res.status === 404) { + // Audio hasn't been generated / uploaded yet — caller should surface this as 404. + const err = new Error(`presign audio ${slug}/${n}: not found`) as Error & { status: number }; + err.status = 404; + throw err; + } if (!res.ok) { const body = await res.text().catch(() => ''); log.error('minio', 'presign audio failed', { slug, n, status: res.status, body }); diff --git a/ui/src/routes/api/presign/audio/+server.ts b/ui/src/routes/api/presign/audio/+server.ts index 5cb3897..d31a7aa 100644 --- a/ui/src/routes/api/presign/audio/+server.ts +++ b/ui/src/routes/api/presign/audio/+server.ts @@ -7,6 +7,7 @@ import { log } from '$lib/server/logger'; * GET /api/presign/audio?slug=...&n=...&voice=...&speed=... * Returns a presigned MinIO URL for the audio file so the browser * can stream it directly without going through the server. + * Returns 404 when the audio has not been generated yet. */ export const GET: RequestHandler = async ({ url }) => { const slug = url.searchParams.get('slug'); @@ -22,6 +23,10 @@ export const GET: RequestHandler = async ({ url }) => { const presignedUrl = await presignAudio(slug, n, voice, speed); return json({ url: presignedUrl }); } catch (e) { + const status = (e as { status?: number }).status; + if (status === 404) { + error(404, 'Audio not found'); + } log.error('presign', 'presign audio failed', { slug, n, err: String(e) }); error(500, `Could not get presigned URL: ${e}`); }