From 88f8c6ebc2e484d690edd30687f9c69f43bbdd65 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 11 Apr 2024 13:19:01 +0200 Subject: [PATCH 01/13] @uppy/utils: add fetcher --- packages/@uppy/utils/src/fetcher.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/@uppy/utils/src/fetcher.ts b/packages/@uppy/utils/src/fetcher.ts index 619e8a501c..618ec228ad 100644 --- a/packages/@uppy/utils/src/fetcher.ts +++ b/packages/@uppy/utils/src/fetcher.ts @@ -3,6 +3,9 @@ import ProgressTimeout from './ProgressTimeout.ts' const noop = (): void => {} +/** + * Optional settings for the fetch operation. + */ export type FetcherOptions = { /** The HTTP method to use for the request. Default is 'GET'. */ method?: string @@ -31,7 +34,7 @@ export type FetcherOptions = { retryCount: number, ) => void | Promise - /** Function for tracking upload progress. */ + /** A callback function for tracking upload progress. */ onUploadProgress?: (event: ProgressEvent) => void /** A function to determine whether to retry the request. */ From ddeb229bb2df06536ae3da563d872f60881e0f8c Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 11 Apr 2024 14:08:18 +0200 Subject: [PATCH 02/13] Improve callbacks --- packages/@uppy/utils/src/fetcher.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/@uppy/utils/src/fetcher.ts b/packages/@uppy/utils/src/fetcher.ts index 618ec228ad..619e8a501c 100644 --- a/packages/@uppy/utils/src/fetcher.ts +++ b/packages/@uppy/utils/src/fetcher.ts @@ -3,9 +3,6 @@ import ProgressTimeout from './ProgressTimeout.ts' const noop = (): void => {} -/** - * Optional settings for the fetch operation. - */ export type FetcherOptions = { /** The HTTP method to use for the request. Default is 'GET'. */ method?: string @@ -34,7 +31,7 @@ export type FetcherOptions = { retryCount: number, ) => void | Promise - /** A callback function for tracking upload progress. */ + /** Function for tracking upload progress. */ onUploadProgress?: (event: ProgressEvent) => void /** A function to determine whether to retry the request. */ From ce16ed4619d341e8b5e2cd73743c42c78000f395 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Tue, 16 Apr 2024 17:37:34 +0200 Subject: [PATCH 03/13] @uppy/xhr-upload: introduce hooks similar to tus --- packages/@uppy/utils/src/fetcher.ts | 6 +- packages/@uppy/xhr-upload/src/index.test.ts | 112 ++++++-------------- packages/@uppy/xhr-upload/src/index.ts | 66 ++++-------- 3 files changed, 55 insertions(+), 129 deletions(-) diff --git a/packages/@uppy/utils/src/fetcher.ts b/packages/@uppy/utils/src/fetcher.ts index 619e8a501c..c600f71552 100644 --- a/packages/@uppy/utils/src/fetcher.ts +++ b/packages/@uppy/utils/src/fetcher.ts @@ -38,7 +38,7 @@ export type FetcherOptions = { shouldRetry?: (xhr: XMLHttpRequest) => boolean /** Called after the response has succeeded or failed but before the promise is resolved. */ - onAfterRequest?: ( + onAfterResponse?: ( xhr: XMLHttpRequest, retryCount: number, ) => void | Promise @@ -67,7 +67,7 @@ export function fetcher( onBeforeRequest = noop, onUploadProgress = noop, shouldRetry = () => true, - onAfterRequest = noop, + onAfterResponse = noop, onTimeout = noop, responseType, retries = 3, @@ -99,7 +99,7 @@ export function fetcher( }) xhr.onload = async () => { - await onAfterRequest(xhr, retryCount) + await onAfterResponse(xhr, retryCount) if (xhr.status >= 200 && xhr.status < 300) { timer.done() diff --git a/packages/@uppy/xhr-upload/src/index.test.ts b/packages/@uppy/xhr-upload/src/index.test.ts index 62271157e6..7ccfd812af 100644 --- a/packages/@uppy/xhr-upload/src/index.test.ts +++ b/packages/@uppy/xhr-upload/src/index.test.ts @@ -4,88 +4,44 @@ import Core from '@uppy/core' import XHRUpload from './index.ts' describe('XHRUpload', () => { - describe('getResponseData', () => { - it('has the XHRUpload options as its `this`', () => { - nock('https://fake-endpoint.uppy.io') - .defaultReplyHeaders({ - 'access-control-allow-method': 'POST', - 'access-control-allow-origin': '*', - }) - .options('/') - .reply(200, {}) - .post('/') - .reply(200, {}) - - const core = new Core() - const getResponseData = vi.fn(function getResponseData() { - // @ts-expect-error TS can't know the type - expect(this.some).toEqual('option') - return {} - }) - core.use(XHRUpload, { - id: 'XHRUpload', - endpoint: 'https://fake-endpoint.uppy.io', - // @ts-expect-error that option does not exist - some: 'option', - getResponseData, - }) - core.addFile({ - type: 'image/png', - source: 'test', - name: 'test.jpg', - data: new Blob([new Uint8Array(8192)]), - }) - - return core.upload().then(() => { - expect(getResponseData).toHaveBeenCalled() + it('should leverage hooks from fetcher', () => { + nock('https://fake-endpoint.uppy.io') + .defaultReplyHeaders({ + 'access-control-allow-method': 'POST', + 'access-control-allow-origin': '*', }) - }) - }) + .options('/') + .reply(204, {}) + .post('/') + .reply(401, {}) + .options('/') + .reply(204, {}) + .post('/') + .reply(200, {}) - describe('validateStatus', () => { - it('emit upload error under status code 200', () => { - nock('https://fake-endpoint.uppy.io') - .defaultReplyHeaders({ - 'access-control-allow-method': 'POST', - 'access-control-allow-origin': '*', - }) - .options('/') - .reply(200, {}) - .post('/') - .reply(200, { - code: 40000, - message: 'custom upload error', - }) + const core = new Core() + const shouldRetry = vi.fn(() => true) + const onBeforeRequest = vi.fn(() => {}) + const onAfterResponse = vi.fn(() => {}) - const core = new Core() - const validateStatus = vi.fn((status, responseText) => { - return JSON.parse(responseText).code !== 40000 - }) - - core.use(XHRUpload, { - id: 'XHRUpload', - endpoint: 'https://fake-endpoint.uppy.io', - // @ts-expect-error that option doesn't exist - some: 'option', - validateStatus, - getResponseError(responseText) { - return JSON.parse(responseText).message - }, - }) - core.addFile({ - type: 'image/png', - source: 'test', - name: 'test.jpg', - data: new Blob([new Uint8Array(8192)]), - }) + core.use(XHRUpload, { + id: 'XHRUpload', + endpoint: 'https://fake-endpoint.uppy.io', + shouldRetry, + onBeforeRequest, + onAfterResponse, + }) + core.addFile({ + type: 'image/png', + source: 'test', + name: 'test.jpg', + data: new Blob([new Uint8Array(8192)]), + }) - return core.upload().then((result) => { - expect(validateStatus).toHaveBeenCalled() - expect(result!.failed!.length).toBeGreaterThan(0) - result!.failed!.forEach((file) => { - expect(file.error).toEqual('custom upload error') - }) - }) + return core.upload().then(() => { + expect(shouldRetry).toHaveBeenCalledTimes(1) + expect(onAfterResponse).toHaveBeenCalledTimes(2) + expect(onBeforeRequest).toHaveBeenCalledTimes(2) }) }) diff --git a/packages/@uppy/xhr-upload/src/index.ts b/packages/@uppy/xhr-upload/src/index.ts index 5799ea2477..9aacda4ed2 100644 --- a/packages/@uppy/xhr-upload/src/index.ts +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -10,7 +10,7 @@ import { } from '@uppy/utils/lib/RateLimitedQueue' import NetworkError from '@uppy/utils/lib/NetworkError' import isNetworkError from '@uppy/utils/lib/isNetworkError' -import { fetcher } from '@uppy/utils/lib/fetcher' +import { fetcher, type FetcherOptions } from '@uppy/utils/lib/fetcher' import { filterNonFailedFiles, filterFilesToEmitUploadStarted, @@ -54,16 +54,11 @@ export interface XhrUploadOpts limit?: number responseType?: XMLHttpRequestResponseType withCredentials?: boolean - validateStatus?: ( - status: number, - body: string, - xhr: XMLHttpRequest, - ) => boolean - getResponseData?: (body: string, xhr: XMLHttpRequest) => B - getResponseError?: (body: string, xhr: XMLHttpRequest) => Error | NetworkError - allowedMetaFields?: string[] | boolean + onBeforeRequest?: FetcherOptions['onBeforeRequest'] + shouldRetry?: FetcherOptions['shouldRetry'] + onAfterResponse?: FetcherOptions['onAfterResponse'] + allowedMetaFields?: boolean | string[] bundle?: boolean - responseUrlFieldName?: string } function buildResponseError( @@ -106,37 +101,12 @@ const defaultOptions = { fieldName: 'file', method: 'post', allowedMetaFields: true, - responseUrlFieldName: 'url', bundle: false, headers: {}, timeout: 30 * 1000, limit: 5, withCredentials: false, responseType: '', - getResponseData(responseText) { - let parsedResponse = {} - try { - parsedResponse = JSON.parse(responseText) - } catch { - // ignore - } - // We don't have access to the B (Body) generic here - // so we have to cast it to any. The user facing types - // remain correct, this is only to please the merging of default options. - return parsedResponse as any - }, - getResponseError(_, response) { - let error = new Error('Upload error') - - if (isNetworkError(response)) { - error = new NetworkError(error, response) - } - - return error - }, - validateStatus(status) { - return status >= 200 && status < 300 - }, } satisfies Partial> type Opts = DefinePluginOpts< @@ -215,6 +185,9 @@ export default class XHRUpload< try { const res = await fetcher(url, { ...options, + onBeforeRequest: this.opts.onBeforeRequest, + shouldRetry: this.opts.shouldRetry, + onAfterResponse: this.opts.onAfterResponse, onTimeout: (timeout) => { const seconds = Math.ceil(timeout / 1000) const error = new Error(this.i18n('uploadStalled', { seconds })) @@ -235,15 +208,11 @@ export default class XHRUpload< }, }) - if (!this.opts.validateStatus(res.status, res.responseText, res)) { - throw new NetworkError(res.statusText, res) - } + const body = JSON.parse(res.responseText) - const body = this.opts.getResponseData(res.responseText, res) - const uploadURL = body[this.opts.responseUrlFieldName] - if (typeof uploadURL !== 'string') { + if (!body?.url) { throw new Error( - `The received response did not include a valid URL for key ${this.opts.responseUrlFieldName}`, + 'Expected body to be JSON and have a `url` property.', ) } @@ -251,7 +220,7 @@ export default class XHRUpload< this.uppy.emit('upload-success', file, { status: res.status, body, - uploadURL, + uploadURL: body.url, }) } @@ -262,12 +231,13 @@ export default class XHRUpload< } if (error instanceof NetworkError) { const request = error.request! - const customError = buildResponseError( - request, - this.opts.getResponseError(request.responseText, request), - ) + for (const file of files) { - this.uppy.emit('upload-error', file, customError) + this.uppy.emit( + 'upload-error', + file, + buildResponseError(request, error), + ) } } From 6b955668d6d0d7502367d11369e6887b46c3054e Mon Sep 17 00:00:00 2001 From: Murderlon Date: Mon, 29 Apr 2024 16:09:41 +0200 Subject: [PATCH 04/13] Apply feedback --- packages/@uppy/xhr-upload/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@uppy/xhr-upload/src/index.ts b/packages/@uppy/xhr-upload/src/index.ts index 9aacda4ed2..25e8ad66de 100644 --- a/packages/@uppy/xhr-upload/src/index.ts +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -208,9 +208,9 @@ export default class XHRUpload< }, }) - const body = JSON.parse(res.responseText) + const body = JSON.parse(res.responseText) as B - if (!body?.url) { + if (typeof body?.url !== 'string') { throw new Error( 'Expected body to be JSON and have a `url` property.', ) From fb379add08d79c9cdf64a65add6c5d0582661b2d Mon Sep 17 00:00:00 2001 From: Murderlon Date: Wed, 1 May 2024 11:54:05 +0200 Subject: [PATCH 05/13] Update docs --- docs/uploader/xhr.mdx | 147 +++++++++++++++--------------------------- 1 file changed, 52 insertions(+), 95 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index 1b1a429ff6..c70ede3b00 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -148,6 +148,14 @@ The function syntax is not available when [`bundle`](#bundle) is set to `true`. ::: +:::note + +Failed requests are retried with the same headers. +If you want to change the headers on retry, [such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), +you can use `onBeforeRequest`. + +::: + #### `bundle` Send all files in a single multipart request (`boolean`, default: `false`). @@ -176,101 +184,6 @@ uppy.setFileState(fileID, { }); ``` -#### `validateStatus` - -Check if the response was successful (`function`, default: -`(status, responseText, response) => boolean`). - -- By default, responses with a 2xx HTTP status code are considered successful. -- When `true`, [`getResponseData()`](#getResponseData-responseText-response) - will be called and the upload will be marked as successful. -- When `false`, both - [`getResponseData()`](#getResponseData-responseText-response) and - [`getResponseError()`](#getResponseError-responseText-response) will be called - and the upload will be marked as unsuccessful. - -##### Parameters - -- The `statusCode` is the numeric HTTP status code returned by the endpoint. -- The `responseText` is the XHR endpoint response as a string. -- `response` is the [XMLHttpRequest][] object. - -:::note - -This option is only used for **local** uploads. Uploads from remote providers -like Google Drive or Instagram do not support this and will always use the -default. - -::: - -#### `getResponseData` - -Extract the response data from the successful upload (`function`, default: -`(responseText, response) => void`). - -- `responseText` is the XHR endpoint response as a string. -- `response` is the [XMLHttpRequest][] object. - -JSON is handled automatically, so you should only use this if the endpoint -responds with a different format. For example, an endpoint that responds with an -XML document: - -```js -function getResponseData(responseText, response) { - const parser = new DOMParser(); - const xmlDoc = parser.parseFromString(responseText, 'text/xml'); - return { - url: xmlDoc.querySelector('Location').textContent, - }; -} -``` - -:::note - -This response data will be available on the file’s `.response` property and will -be emitted in the [`upload-success`][uppy.upload-success] event. - -::: - -:::note - -When uploading files from remote providers such as Dropbox or Instagram, -Companion sends upload response data to the client. This is made available in -the `getResponseData()` function as well. The `response` object from Companion -has some properties named after their [XMLHttpRequest][] counterparts. - -::: - -#### `getResponseError` - -Extract the error from the failed upload (`function`, default: -`(responseText, response) => void`). - -For example, if the endpoint responds with a JSON object containing a -`{ message }` property, this would show that message to the user: - -```js -function getResponseError(responseText, response) { - return new Error(JSON.parse(responseText).message); -} -``` - -#### `responseUrlFieldName` - -The field name containing the location of the uploaded file (`string`, default: -`'url'`). - -This is returned by [`getResponseData()`](#getResponseData). - -#### `timeout: 30 * 1000` - -Abort the connection if no upload progress events have been received for this -milliseconds amount (`number`, default: `30_000`). - -Note that unlike the [`XMLHttpRequest.timeout`][xhr.timeout] property, this is a -timer between progress events: the total upload can take longer than this value. -Set to `0` to disable this check. - #### `limit` The maximum amount of files to upload in parallel (`number`, default: `5`). @@ -291,6 +204,21 @@ by browsers, so it’s recommended to use one of those. Indicates whether cross-site Access-Control requests should be made using credentials (`boolean`, default: `false`). +### `onBeforeRequest` + +An optional function that will be called before a HTTP request is sent out (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). + +### `shouldRetry` + +An optional function called once an error appears and before retrying (`(xhr: XMLHttpRequesT) => boolean`). + +The amount of retries is 3, even if you continue to return `true`. +The default behavior uses [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a maximum of 3 retries. + +### `onAfterResponse` + +An optional function that will be called after a HTTP response has been received (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). + #### `locale: {}` ```js @@ -304,6 +232,35 @@ export default { ## Frequently Asked Questions +### How can I refresh auth tokens after they expire? + +```js +import Uppy from '@uppy/core'; +import XHR from '@uppy/xhr-upload'; + +let token + +async function getAuthToken(refresh = false) { + if (!refresh && token) { + return token; + } + // fetch it +} + +new Uppy().use(XHR, { + endpoint: '', + async onBeforeRequest(xhr) { + const token = await getAuthToken(); + xhr.setRequestHeader('Authorization', `Bearer ${token}`); + }, + async onAfterResponse(xhr) { + if (xhr.status === 401) { + await getAuthToken(true); + } + }, +}); +``` + ### How to send along meta data with the upload? When using XHRUpload with [`formData: true`](#formData-true), file metadata is From 4097ed0d68908d1210576552cca991131b002240 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Wed, 1 May 2024 11:58:13 +0200 Subject: [PATCH 06/13] Fix docs --- docs/uploader/xhr.mdx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index c70ede3b00..a5a05dd623 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -184,6 +184,15 @@ uppy.setFileState(fileID, { }); ``` +#### `timeout: 30 * 1000` + +Abort the connection if no upload progress events have been received for this +milliseconds amount (`number`, default: `30_000`). + +Note that unlike the [`XMLHttpRequest.timeout`][xhr.timeout] property, this is a +timer between progress events: the total upload can take longer than this value. +Set to `0` to disable this check. + #### `limit` The maximum amount of files to upload in parallel (`number`, default: `5`). From 56c6da63665f0613973a1e26bc0f2637f967f0ad Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 2 May 2024 13:02:20 +0200 Subject: [PATCH 07/13] Format again --- docs/uploader/xhr.mdx | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index a5a05dd623..7797e4d0ed 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -150,8 +150,9 @@ The function syntax is not available when [`bundle`](#bundle) is set to `true`. :::note -Failed requests are retried with the same headers. -If you want to change the headers on retry, [such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), +Failed requests are retried with the same headers. If you want to change the +headers on retry, +[such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), you can use `onBeforeRequest`. ::: @@ -215,18 +216,23 @@ credentials (`boolean`, default: `false`). ### `onBeforeRequest` -An optional function that will be called before a HTTP request is sent out (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). +An optional function that will be called before a HTTP request is sent out +(`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). ### `shouldRetry` -An optional function called once an error appears and before retrying (`(xhr: XMLHttpRequesT) => boolean`). +An optional function called once an error appears and before retrying +(`(xhr: XMLHttpRequesT) => boolean`). -The amount of retries is 3, even if you continue to return `true`. -The default behavior uses [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a maximum of 3 retries. +The amount of retries is 3, even if you continue to return `true`. The default +behavior uses +[exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a +maximum of 3 retries. ### `onAfterResponse` -An optional function that will be called after a HTTP response has been received (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). +An optional function that will be called after a HTTP response has been received +(`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). #### `locale: {}` @@ -247,13 +253,13 @@ export default { import Uppy from '@uppy/core'; import XHR from '@uppy/xhr-upload'; -let token +let token; async function getAuthToken(refresh = false) { - if (!refresh && token) { - return token; - } - // fetch it + if (!refresh && token) { + return token; + } + // fetch it } new Uppy().use(XHR, { @@ -349,14 +355,12 @@ $file_name = $_POST['name']; // desired name of the file move_uploaded_file($file_path, $_SERVER['DOCUMENT_ROOT'] . '/img/' . basename($file_name)); // save the file at `img/FILE_NAME` ``` -[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData [xmlhttprequest]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest [xhr.timeout]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout [xhr.responsetype]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType -[uppy.upload-success]: /docs/uppy/#upload-success [uppy file]: /docs/uppy#working-with-uppy-files [php.file-upload]: https://secure.php.net/manual/en/features.file-upload.php [php.multiple]: From a5012c45a5cd048a3043fa1ede8494fa69ac98c7 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 2 May 2024 13:09:06 +0200 Subject: [PATCH 08/13] Fix --- docs/uploader/xhr.mdx | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index 7797e4d0ed..a5a05dd623 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -150,9 +150,8 @@ The function syntax is not available when [`bundle`](#bundle) is set to `true`. :::note -Failed requests are retried with the same headers. If you want to change the -headers on retry, -[such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), +Failed requests are retried with the same headers. +If you want to change the headers on retry, [such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), you can use `onBeforeRequest`. ::: @@ -216,23 +215,18 @@ credentials (`boolean`, default: `false`). ### `onBeforeRequest` -An optional function that will be called before a HTTP request is sent out -(`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). +An optional function that will be called before a HTTP request is sent out (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). ### `shouldRetry` -An optional function called once an error appears and before retrying -(`(xhr: XMLHttpRequesT) => boolean`). +An optional function called once an error appears and before retrying (`(xhr: XMLHttpRequesT) => boolean`). -The amount of retries is 3, even if you continue to return `true`. The default -behavior uses -[exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a -maximum of 3 retries. +The amount of retries is 3, even if you continue to return `true`. +The default behavior uses [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a maximum of 3 retries. ### `onAfterResponse` -An optional function that will be called after a HTTP response has been received -(`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). +An optional function that will be called after a HTTP response has been received (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). #### `locale: {}` @@ -253,13 +247,13 @@ export default { import Uppy from '@uppy/core'; import XHR from '@uppy/xhr-upload'; -let token; +let token async function getAuthToken(refresh = false) { - if (!refresh && token) { - return token; - } - // fetch it + if (!refresh && token) { + return token; + } + // fetch it } new Uppy().use(XHR, { @@ -355,12 +349,14 @@ $file_name = $_POST['name']; // desired name of the file move_uploaded_file($file_path, $_SERVER['DOCUMENT_ROOT'] . '/img/' . basename($file_name)); // save the file at `img/FILE_NAME` ``` +[formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData [xmlhttprequest]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest [xhr.timeout]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout [xhr.responsetype]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType +[uppy.upload-success]: /docs/uppy/#upload-success [uppy file]: /docs/uppy#working-with-uppy-files [php.file-upload]: https://secure.php.net/manual/en/features.file-upload.php [php.multiple]: From 9ae9b3d9ac69dd4500175531e126434497628979 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Mon, 6 May 2024 10:54:56 +0200 Subject: [PATCH 09/13] Format --- docs/uploader/xhr.mdx | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index a5a05dd623..d0d6302333 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -150,8 +150,9 @@ The function syntax is not available when [`bundle`](#bundle) is set to `true`. :::note -Failed requests are retried with the same headers. -If you want to change the headers on retry, [such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), +Failed requests are retried with the same headers. If you want to change the +headers on retry, +[such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), you can use `onBeforeRequest`. ::: @@ -215,18 +216,23 @@ credentials (`boolean`, default: `false`). ### `onBeforeRequest` -An optional function that will be called before a HTTP request is sent out (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). +An optional function that will be called before a HTTP request is sent out +(`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). ### `shouldRetry` -An optional function called once an error appears and before retrying (`(xhr: XMLHttpRequesT) => boolean`). +An optional function called once an error appears and before retrying +(`(xhr: XMLHttpRequesT) => boolean`). -The amount of retries is 3, even if you continue to return `true`. -The default behavior uses [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a maximum of 3 retries. +The amount of retries is 3, even if you continue to return `true`. The default +behavior uses +[exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) with a +maximum of 3 retries. ### `onAfterResponse` -An optional function that will be called after a HTTP response has been received (`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). +An optional function that will be called after a HTTP response has been received +(`(xhr: XMLHttpRequest, retryCount: number) => void | Promise`). #### `locale: {}` @@ -247,13 +253,13 @@ export default { import Uppy from '@uppy/core'; import XHR from '@uppy/xhr-upload'; -let token +let token; async function getAuthToken(refresh = false) { - if (!refresh && token) { - return token; - } - // fetch it + if (!refresh && token) { + return token; + } + // fetch it } new Uppy().use(XHR, { From bdeba84464b9fadf176da04c479fc4d8da635152 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Mon, 6 May 2024 10:58:55 +0200 Subject: [PATCH 10/13] Remove unused defs --- docs/uploader/xhr.mdx | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index d0d6302333..17fe7364bd 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -356,13 +356,10 @@ move_uploaded_file($file_path, $_SERVER['DOCUMENT_ROOT'] . '/img/' . basename($f ``` [formdata]: https://developer.mozilla.org/en-US/docs/Web/API/FormData -[xmlhttprequest]: - https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest [xhr.timeout]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout [xhr.responsetype]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType -[uppy.upload-success]: /docs/uppy/#upload-success [uppy file]: /docs/uppy#working-with-uppy-files [php.file-upload]: https://secure.php.net/manual/en/features.file-upload.php [php.multiple]: From 33033fccc7f256dd22623301071bba1c74736f9d Mon Sep 17 00:00:00 2001 From: Merlijn Vos Date: Wed, 8 May 2024 15:52:38 +0200 Subject: [PATCH 11/13] Apply suggestions from code review --- docs/uploader/xhr.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index 17fe7364bd..9c6ea74978 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -152,8 +152,8 @@ The function syntax is not available when [`bundle`](#bundle) is set to `true`. Failed requests are retried with the same headers. If you want to change the headers on retry, -[such as refreshing an auth token](how-can-I-refresh-auth-tokens-after-they-expire), -you can use `onBeforeRequest`. +[such as refreshing an auth token](#how-can-I-refresh-auth-tokens-after-they-expire), +you can use [`onBeforeRequest`](#onbeforerequest). ::: From d44e34a974f497d9bbb88d593434cdbb40e5d8e9 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Wed, 8 May 2024 16:47:43 +0200 Subject: [PATCH 12/13] Simplify example --- docs/uploader/xhr.mdx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index 9c6ea74978..63afffa4e0 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -253,24 +253,24 @@ export default { import Uppy from '@uppy/core'; import XHR from '@uppy/xhr-upload'; -let token; +let token = null; -async function getAuthToken(refresh = false) { - if (!refresh && token) { - return token; - } - // fetch it +async function getAuthToken() { + token = await fetch('/auth/token').then((res) => res.json()); } new Uppy().use(XHR, { endpoint: '', + // Called again for every retry too. async onBeforeRequest(xhr) { - const token = await getAuthToken(); + if (!token) { + token = await getAuthToken(); + } xhr.setRequestHeader('Authorization', `Bearer ${token}`); }, async onAfterResponse(xhr) { if (xhr.status === 401) { - await getAuthToken(true); + token = null; } }, }); From 62cf6820882d8f52cb35224d2c3dd31f2a5cb83e Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 9 May 2024 09:17:09 +0200 Subject: [PATCH 13/13] Apply feedback --- docs/uploader/xhr.mdx | 6 ++++-- packages/@uppy/xhr-upload/src/index.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/uploader/xhr.mdx b/docs/uploader/xhr.mdx index 63afffa4e0..18f4801da6 100644 --- a/docs/uploader/xhr.mdx +++ b/docs/uploader/xhr.mdx @@ -256,7 +256,9 @@ import XHR from '@uppy/xhr-upload'; let token = null; async function getAuthToken() { - token = await fetch('/auth/token').then((res) => res.json()); + const res = await fetch('/auth/token'); + const json = await res.json(); + return json.token; } new Uppy().use(XHR, { @@ -270,7 +272,7 @@ new Uppy().use(XHR, { }, async onAfterResponse(xhr) { if (xhr.status === 401) { - token = null; + token = await getAuthToken(); } }, }); diff --git a/packages/@uppy/xhr-upload/src/index.ts b/packages/@uppy/xhr-upload/src/index.ts index 2e74caca33..6b3124f813 100644 --- a/packages/@uppy/xhr-upload/src/index.ts +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -209,12 +209,13 @@ export default class XHRUpload< }) const body = JSON.parse(res.responseText) as B + const uploadURL = typeof body?.url === 'string' ? body.url : undefined for (const file of files) { this.uppy.emit('upload-success', file, { status: res.status, body, - uploadURL: body.url as string | undefined, + uploadURL, }) }