Explorar el Código

Add `onShouldRetry` as option to @uppy/tus (#3720)

- Add `onShouldRetry` as option to @uppy/tus
  - Docs
  - Types
  - E2E test
- Remove bad flaky test

Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
Merlijn Vos hace 2 años
padre
commit
7d7c4dd5ad

+ 8 - 1
e2e/clients/dashboard-tus/app.js

@@ -10,9 +10,16 @@ import '@uppy/dashboard/dist/style.css'
 const companionUrl = 'http://localhost:3020'
 const uppy = new Uppy()
   .use(Dashboard, { target: '#app', inline: true })
-  .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files' })
+  .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files', onShouldRetry })
   .use(Url, { target: Dashboard, companionUrl })
   .use(Unsplash, { target: Dashboard, companionUrl })
 
+function onShouldRetry (err, retryAttempt, options, next) {
+  if (err?.originalResponse?.getStatus() === 418) {
+    return true
+  }
+  return next(err)
+}
+
 // Keep this here to access uppy in tests
 window.uppy = uppy

+ 1 - 23
e2e/cypress/integration/dashboard-tus.spec.ts

@@ -16,29 +16,6 @@ describe('Dashboard with Tus', () => {
     cy.intercept('http://localhost:3020/search/unsplash/*').as('unsplash')
   })
 
-  it('should emit `error` and `upload-error` events on failed POST request', () => {
-    cy.get('@file-input').attachFile(['images/traffic.jpg'])
-
-    const error = cy.spy()
-    const uploadError = cy.spy()
-    cy.window().then(({ uppy }) => {
-      uppy.on('upload-error', uploadError)
-      uppy.on('error', error)
-    })
-
-    cy.get('.uppy-StatusBar-actionBtn--upload').click()
-
-    cy.intercept(
-      { method: 'POST', url: 'https://tusd.tusdemo.net/*', times: 1 },
-      { statusCode: 401, body: { code: 401, message: 'Expired JWT Token' } },
-    ).as('post')
-
-    cy.wait('@post').then(() =>  {
-      expect(error).to.be.called
-      expect(uploadError).to.be.called
-    })
-  })
-
   it('should upload cat image successfully', () => {
     cy.get('@file-input').attachFile('images/cat.jpg')
     cy.get('.uppy-StatusBar-actionBtn--upload').click()
@@ -57,6 +34,7 @@ describe('Dashboard with Tus', () => {
       { statusCode: 429, body: {} },
     ).as('patch')
 
+    cy.wait('@patch')
     cy.wait('@patch')
 
     cy.window().then(({ uppy }) => {

+ 8 - 1
packages/@uppy/tus/src/index.js

@@ -277,8 +277,9 @@ export default class Tus extends BasePlugin {
         resolve(upload)
       }
 
-      uploadOptions.onShouldRetry = (err) => {
+      const defaultOnShouldRetry = (err) => {
         const status = err?.originalResponse?.getStatus()
+
         if (status === 429) {
           // HTTP 429 Too Many Requests => to avoid the whole download to fail, pause all requests.
           if (!this.requests.isPaused) {
@@ -316,6 +317,12 @@ export default class Tus extends BasePlugin {
         return true
       }
 
+      if (opts.onShouldRetry != null) {
+        uploadOptions.onShouldRetry = (...args) => opts.onShouldRetry(...args, defaultOnShouldRetry)
+      } else {
+        uploadOptions.onShouldRetry = defaultOnShouldRetry
+      }
+
       const copyProp = (obj, srcProp, destProp) => {
         if (hasProperty(obj, srcProp) && !hasProperty(obj, destProp)) {
           // eslint-disable-next-line no-param-reassign

+ 14 - 10
packages/@uppy/tus/types/index.d.ts

@@ -1,22 +1,26 @@
 import type { PluginOptions, BasePlugin } from '@uppy/core'
 import type { UploadOptions } from 'tus-js-client'
 
-  type TusUploadOptions = Pick<UploadOptions, Exclude<keyof UploadOptions,
-    | 'fingerprint'
-    | 'metadata'
-    | 'onProgress'
-    | 'onChunkComplete'
-    | 'onSuccess'
-    | 'onError'
-    | 'uploadUrl'
-    | 'uploadSize'
-  >>
+type TusUploadOptions = Pick<UploadOptions, Exclude<keyof UploadOptions,
+  | 'fingerprint'
+  | 'metadata'
+  | 'onProgress'
+  | 'onChunkComplete'
+  | 'onShouldRetry'
+  | 'onSuccess'
+  | 'onError'
+  | 'uploadUrl'
+  | 'uploadSize'
+>>
+
+type Next = (err: Error | undefined, retryAttempt?: number, options?: TusOptions) => boolean
 
 export interface TusOptions extends PluginOptions, TusUploadOptions {
     metaFields?: string[] | null
     limit?: number
     useFastRemoteRetry?: boolean
     withCredentials?: boolean
+    onShouldRetry: (err: Error | undefined, retryAttempt: number, options: TusOptions, next: Next) => boolean
   }
 
 declare class Tus extends BasePlugin<TusOptions> {}

+ 35 - 0
website/src/docs/tus.md

@@ -37,6 +37,10 @@ const { Tus } = Uppy
 
 ## Options
 
+**Note**: all options are passed to `tus-js-client` and we document the ones here that we added or changed. This means you can also pass functions like [`onBeforeRequest`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#onbeforerequest) and [`onAfterResponse`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#onafterresponse).
+
+We recommended taking a look at the [API reference](https://github.com/tus/tus-js-client/blob/master/docs/api.md) from `tus-js-client` to know what is supported.
+
 ### `id: 'Tus'`
 
 A unique identifier for this plugin. It defaults to `'Tus'`.
@@ -87,6 +91,37 @@ When uploading a chunk fails, automatically try again after the millisecond inte
 
 Set to `null` to disable automatic retries, and fail instantly if any chunk fails to upload.
 
+### `onShouldRetry: (err, retryAttempt, options, next) => next(err)`
+
+When an upload fails `onShouldRetry` is called with the error and the default retry logic as the second argument. The default retry logic is an [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) algorithm triggered on HTTP 429 (Too Many Requests) errors. Meaning if your server (or proxy) returns HTTP 429 because it’s being overloaded, @uppy/tus will find the ideal sweet spot to keep uploading without overloading.
+
+If you want to extend this functionality, for instance to retry on unauthorized requests (to retrieve a new authentication token):
+
+```js
+import Uppy from '@uppy/core'
+import Tus from '@uppy/tus'
+
+new Uppy().use(Tus, { endpoint: '', onBeforeRequest, onShouldRetry, onAfterResponse })
+
+async function onBeforeRequest (req) {
+  const token = await getAuthToken()
+  req.setHeader('Authorization', `Bearer ${token}`)
+}
+
+function onShouldRetry (err, retryAttempt, options, next) {
+  if (err?.originalResponse?.getStatus() === 401) {
+    return true
+  }
+  return next(err)
+}
+
+async function onAfterResponse (req, res) {
+  if (res.getStatus() === 401) {
+    await refreshAuthToken()
+  }
+}
+```
+
 ### `metaFields: null`
 
 Pass an array of field names to limit the metadata fields that will be added to uploads as [Tus Metadata](https://tus.io/protocols/resumable-upload.html#upload-metadata).