From 1b0422253c5c6dc86501e6de21046478b8fed80f Mon Sep 17 00:00:00 2001 From: Jannik Stehle Date: Tue, 22 Oct 2024 10:20:33 +0200 Subject: [PATCH] fix: preview image retry Adds a retry mechanism to preview loading when the server responds with `429` status codes. --- changelog/unreleased/bugfix-preview-retries | 6 +++ .../src/services/preview/previewService.ts | 50 ++++++++++++++----- .../unit/services/previewService.spec.ts | 24 +++++++++ 3 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 changelog/unreleased/bugfix-preview-retries diff --git a/changelog/unreleased/bugfix-preview-retries b/changelog/unreleased/bugfix-preview-retries new file mode 100644 index 00000000000..d03f0791bd2 --- /dev/null +++ b/changelog/unreleased/bugfix-preview-retries @@ -0,0 +1,6 @@ +Bugfix: Preview image retries + +We've added a retry mechanism to preview loading in case the server is being overrun with too many requests. + +https://github.com/owncloud/web/pull/11813 +https://github.com/owncloud/web/issues/11798 diff --git a/packages/web-pkg/src/services/preview/previewService.ts b/packages/web-pkg/src/services/preview/previewService.ts index e6e08b6fae2..fd833cf38aa 100644 --- a/packages/web-pkg/src/services/preview/previewService.ts +++ b/packages/web-pkg/src/services/preview/previewService.ts @@ -104,7 +104,7 @@ export class PreviewService { } if (isPublic) { - return this.publicPreviewUrl(options) + return this.publicPreviewUrl(options, signal) } try { return await this.privatePreviewBlob(options, cached, silenceErrors, signal) @@ -116,7 +116,11 @@ export class PreviewService { } } - private async cacheFactory(options: LoadPreviewOptions, silenceErrors: boolean): Promise { + private async cacheFactory( + options: LoadPreviewOptions, + silenceErrors: boolean, + signal?: AbortSignal + ): Promise { const { resource, dimensions } = options const hit = cacheService.filePreview.get(resource.id.toString()) @@ -124,7 +128,7 @@ export class PreviewService { return hit.src } try { - const src = await this.privatePreviewBlob(options) + const src = await this.privatePreviewBlob(options, false, true, signal) return cacheService.filePreview.set( resource.id.toString(), { src, etag: resource.etag, dimensions }, @@ -158,7 +162,7 @@ export class PreviewService { ): Promise { const { resource, dimensions, processor } = options if (cached) { - return this.cacheFactory(options, silenceErrors) + return this.cacheFactory(options, silenceErrors, signal) } const url = [ @@ -168,11 +172,22 @@ export class PreviewService { '?', this.buildQueryString({ etag: resource.etag, dimensions, processor }) ].join('') - const { data } = await this.clientService.httpAuthenticated.get(url, { - responseType: 'blob', - signal - }) - return window.URL.createObjectURL(data) + + try { + const { data } = await this.clientService.httpAuthenticated.get(url, { + responseType: 'blob', + signal + }) + return window.URL.createObjectURL(data) + } catch (e) { + if (e.status === 429) { + const retryAfter = e.response?.headers?.['retry-after'] || 5 + await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000)) + return this.privatePreviewBlob(options, cached, silenceErrors, signal) + } + + throw e + } } private async publicPreviewUrl( @@ -194,10 +209,21 @@ export class PreviewService { .join('&') const previewUrl = [url, combinedQuery].filter(Boolean).join('?') - const { status } = await this.clientService.httpUnAuthenticated.head(previewUrl, { signal }) - if (status !== 404) { - return previewUrl + try { + const { status } = await this.clientService.httpUnAuthenticated.head(previewUrl, { signal }) + + if (status !== 404) { + return previewUrl + } + } catch (e) { + if (e.status === 429) { + const retryAfter = e.response?.headers?.['retry-after'] || 5 + await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000)) + return this.publicPreviewUrl(options, signal) + } + + throw e } } } diff --git a/packages/web-pkg/tests/unit/services/previewService.spec.ts b/packages/web-pkg/tests/unit/services/previewService.spec.ts index 961e483955b..81dccdd2065 100644 --- a/packages/web-pkg/tests/unit/services/previewService.spec.ts +++ b/packages/web-pkg/tests/unit/services/previewService.spec.ts @@ -87,6 +87,30 @@ describe('PreviewService', () => { }) expect(preview).toEqual(undefined) }) + it('retries when the server returns a 429 status code', async () => { + const supportedMimeTypes = ['image/png'] + const { previewService, clientService } = getWrapper({ + supportedMimeTypes, + version: '1' + }) + + clientService.httpAuthenticated.get.mockRejectedValueOnce({ + response: { headers: { 'retry-after': 0.1 } }, + status: 429 + }) + clientService.httpAuthenticated.get.mockResolvedValueOnce(undefined) + + await previewService.loadPreview({ + space: mock(), + resource: mock({ + mimeType: supportedMimeTypes[0], + webDavPath: '/', + etag: '', + canDownload: () => true + }) + }) + expect(clientService.httpAuthenticated.get).toHaveBeenCalledTimes(2) + }) describe('private files', () => { it('loads preview', async () => { const objectUrl = 'objectUrl'