From 6e866979965491d47a013213138ba3550930777f Mon Sep 17 00:00:00 2001 From: juliancarrivick Date: Tue, 13 Jan 2026 15:15:54 +0000 Subject: [PATCH] fix(web): Handle upload failures from public users (#24826) --- web/src/lib/utils/file-uploader.spec.ts | 73 +++++++++++++++++++++++++ web/src/lib/utils/file-uploader.ts | 6 +- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 web/src/lib/utils/file-uploader.spec.ts diff --git a/web/src/lib/utils/file-uploader.spec.ts b/web/src/lib/utils/file-uploader.spec.ts new file mode 100644 index 0000000000..cc42b2e141 --- /dev/null +++ b/web/src/lib/utils/file-uploader.spec.ts @@ -0,0 +1,73 @@ +import { uploadManager } from '$lib/managers/upload-manager.svelte'; +import { uploadAssetsStore } from '$lib/stores/upload'; +import { resetSavedUser, user } from '$lib/stores/user.store'; +import { UploadState } from '$lib/types'; +import * as utils from '$lib/utils'; +import { AssetMediaStatus, type AssetMediaResponseDto, type UserAdminResponseDto } from '@immich/sdk'; +import { get } from 'svelte/store'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { fileUploadHandler } from './file-uploader'; + +describe('fileUploader error handling', () => { + const mockFile = new File(['content'], 'test.jpg', { type: 'image/jpeg' }); + const mockUserObject = { id: 'user-123', email: 'test@example.com' } as UserAdminResponseDto; + const mockError = new Error('Upload failed'); + const mockUploadResponse = { id: 'mock-id', status: AssetMediaStatus.Created } as AssetMediaResponseDto; + + beforeEach(() => { + vi.clearAllMocks(); + vi.spyOn(uploadManager, 'getExtensions').mockReturnValue(['.jpg']); + uploadAssetsStore.reset(); + resetSavedUser(); + + // Stub out crypto to avoid that branch + vi.stubGlobal('crypto', undefined); + }); + + for (const [name, mockUser] of [ + ['logged-in users', true], + ['anonymous users', false], + ] as const) { + describe(`for ${name}`, () => { + beforeEach(() => { + if (mockUser) { + user.set(mockUserObject); + } + }); + + it(`should transition successful uploads to done`, async () => { + vi.spyOn(utils, 'uploadRequest').mockResolvedValue({ status: 200, data: mockUploadResponse }); + + await fileUploadHandler({ files: [mockFile] }); + + const items = get(uploadAssetsStore); + expect(items.length).toBe(1); + expect(items[0].state).toBe(UploadState.DONE); + }); + + it('should capture errors', async () => { + vi.spyOn(utils, 'uploadRequest').mockRejectedValue(mockError); + + await fileUploadHandler({ files: [mockFile] }); + + const items = get(uploadAssetsStore); + expect(items.length).toBe(1); + expect(items[0].state).toBe(UploadState.ERROR); + }); + }); + } + + it('should suppress errors on logout', async () => { + user.set(mockUserObject); + vi.spyOn(utils, 'uploadRequest').mockImplementationOnce(() => { + resetSavedUser(); + return Promise.reject(mockError); + }); + + await fileUploadHandler({ files: [mockFile] }); + + const items = get(uploadAssetsStore); + expect(items.length).toBe(1); + expect(items[0].state).toBe(UploadState.STARTED); + }); +}); diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 62e81762a7..042bbaaa2e 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -125,6 +125,7 @@ async function fileUploader({ }: FileUploaderParams): Promise { const fileCreatedAt = new Date(assetFile.lastModified).toISOString(); const $t = get(t); + const wasInitiallyLoggedIn = !!get(user); uploadAssetsStore.markStarted(deviceAssetId); @@ -215,8 +216,9 @@ async function fileUploader({ return responseData.id; } catch (error) { - // ignore errors if the user logs out during uploads - if (!get(user)) { + // If the user store no longer holds a user, it means they have logged out + // In this case don't bother reporting any errors. + if (wasInitiallyLoggedIn && !get(user)) { return; }