From 7956aaac9e84d8f868b3aeb513068100c91cb9f0 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Tue, 2 Jun 2026 16:02:26 -0700 Subject: [PATCH] Fix #772: manual import progress bar stuck at 0 / 'Failed' on slow imports Per-track import does heavy synchronous server-side enrichment (metadata, art, lyrics) that can take 60-90s/track, far longer when external sources are degraded. The React apiClient (ky) had no timeout, so ky's default 10s aborted the import-process request client-side even though the server completed the import (200) and moved the files. The import loop then counted the aborted call as an error, so the bar stayed at 0 and flipped to 'Failed' while files imported fine. Give the two import-process calls (album/process, singles/process) an explicit 5-min timeout. Scoped to import only -- every other endpoint keeps the 10s default; bounded, not disabled. Server behavior unchanged. Adds a test asserting both calls pass the long timeout. --- webui/src/routes/import/-import.api.test.ts | 27 +++++++++++++++++++-- webui/src/routes/import/-import.api.ts | 11 +++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/webui/src/routes/import/-import.api.test.ts b/webui/src/routes/import/-import.api.test.ts index 1963b1f7..c8aaa83d 100644 --- a/webui/src/routes/import/-import.api.test.ts +++ b/webui/src/routes/import/-import.api.test.ts @@ -1,8 +1,14 @@ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; +import { apiClient } from '@/app/api-client'; import { HttpResponse, http, server } from '@/test/msw'; -import { approveAutoImportResult, rejectAutoImportResult } from './-import.api'; +import { + approveAutoImportResult, + processImportAlbumTrack, + processImportSingleFile, + rejectAutoImportResult, +} from './-import.api'; const softFailureMessage = 'Item not found or not pending review'; @@ -26,4 +32,21 @@ describe('import api', () => { await expect(approveAutoImportResult(17)).rejects.toThrow(softFailureMessage); await expect(rejectAutoImportResult(18)).rejects.toThrow(softFailureMessage); }); + + it('#772: import-process calls use a long timeout, not ky default 10s', async () => { + // Per-track import does heavy server-side enrichment (60-90s+); the default + // 10s timeout aborted it client-side -> progress bar stuck + "Failed" while + // files imported. These calls must pass an explicit long timeout. + const ok = { json: async () => ({ success: true, processed: 1, total: 1, errors: [] }) }; + const spy = vi.spyOn(apiClient, 'post').mockReturnValue(ok as never); + + await processImportAlbumTrack({ album: {} as never, match: {} as never }); + await processImportSingleFile({}); + + expect(spy).toHaveBeenCalledTimes(2); + for (const call of spy.mock.calls) { + expect(call[1]).toMatchObject({ timeout: 300_000 }); + } + spy.mockRestore(); + }); }); diff --git a/webui/src/routes/import/-import.api.ts b/webui/src/routes/import/-import.api.ts index 8de7234d..00de11e2 100644 --- a/webui/src/routes/import/-import.api.ts +++ b/webui/src/routes/import/-import.api.ts @@ -18,6 +18,15 @@ import type { export const IMPORT_QUERY_KEY = ['import'] as const; +// Per-track import does heavy synchronous enrichment server-side (metadata +// lookups, art, lyrics) and can legitimately take 60-90s/track — much longer +// when external sources are degraded. ky's default 10s timeout aborts those +// requests client-side even though the server completes the import (200), +// which left the progress bar stuck at 0 and showing "Failed" while files +// imported fine (#772). Give the import-process calls a generous bound so the +// responses actually arrive and the bar advances. Scoped to import only. +const IMPORT_REQUEST_TIMEOUT_MS = 300_000; // 5 min/track + export async function fetchImportStagingFiles(): Promise { return readJson(apiClient.get('import/staging/files')); } @@ -71,6 +80,7 @@ export async function processImportAlbumTrack(input: { album: input.album, matches: [input.match], }, + timeout: IMPORT_REQUEST_TIMEOUT_MS, }), ); } @@ -92,6 +102,7 @@ export async function processImportSingleFile(file: unknown): Promise