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
This commit is contained in:
Admin
2026-03-04 15:21:17 +05:00
parent 53af7515a3
commit 0d7b985469
5 changed files with 26 additions and 0 deletions

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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 ─────────────────────────────────────────────────────

View File

@@ -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 });

View File

@@ -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}`);
}