Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onShouldRetry as option to @uppy/tus #3720

Merged
merged 18 commits into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion e2e/clients/dashboard-tus/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
24 changes: 1 addition & 23 deletions e2e/cypress/integration/dashboard-tus.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -57,6 +34,7 @@ describe('Dashboard with Tus', () => {
{ statusCode: 429, body: {} },
).as('patch')

cy.wait('@patch')
cy.wait('@patch')

cy.window().then(({ uppy }) => {
Expand Down
9 changes: 8 additions & 1 deletion packages/@uppy/tus/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
24 changes: 14 additions & 10 deletions packages/@uppy/tus/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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
aduh95 marked this conversation as resolved.
Show resolved Hide resolved
}

declare class Tus extends BasePlugin<TusOptions> {}
Expand Down
35 changes: 35 additions & 0 deletions website/src/docs/tus.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'`.
Expand Down Expand Up @@ -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
}
Murderlon marked this conversation as resolved.
Show resolved Hide resolved
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).
Expand Down