@uppy/xhr-upload: allow passing getter as endpoint option (#5836)

This adds the option to pass a getter to the `endpoint` option of the
`@uppy/xhr-upload` plugin:

```ts
.use(XHRUpload, {
  endpoint: (file) => createSignedUrl(file),
})
```

This is especially useful when the backend uses short-lived signed urls
for uploads.

---------

Co-authored-by: Merlijn Vos <merlijn@soverin.net>
This commit is contained in:
Louis Haftmann 2025-07-29 14:36:26 +02:00 committed by GitHub
parent f7eba01529
commit 49e98ab30f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 85 additions and 3 deletions

View file

@ -0,0 +1,6 @@
---
"@uppy/xhr-upload": minor
---
The `endpoint` option now also accepts a callback to dynamically set it (`endpoint: (fileOrFiles) => '<url>'`).
If `bundle` is `true`, you get `UppyFile[]` otherwise `UppyFile`.

View file

@ -151,4 +151,68 @@ describe('XHRUpload', () => {
expect(scope.isDone()).toBe(true)
})
})
describe('endpoint', () => {
it('can be a function', async () => {
const scope = nock('https://fake-endpoint.uppy.io').defaultReplyHeaders({
'access-control-allow-origin': '*',
})
scope.options('/upload/test.jpg').reply(200, {})
scope.post('/upload/test.jpg').reply(200, {})
const core = new Core()
core.use(XHRUpload, {
id: 'XHRUpload',
endpoint: (file) =>
!Array.isArray(file)
? `https://fake-endpoint.uppy.io/upload/${file.name}`
: '',
bundle: false,
})
core.addFile({
type: 'image/png',
source: 'test',
name: 'test.jpg',
data: new Blob([new Uint8Array(8192)]),
})
await core.upload()
expect(scope.isDone()).toBe(true)
})
it('can be a function (bundle)', async () => {
const scope = nock('https://fake-endpoint.uppy.io').defaultReplyHeaders({
'access-control-allow-origin': '*',
})
scope.options('/upload-bundle/test.jpg,test2.jpg').reply(200, {})
scope.post('/upload-bundle/test.jpg,test2.jpg').reply(200, {})
const core = new Core()
core.use(XHRUpload, {
id: 'XHRUpload',
endpoint: (file) =>
Array.isArray(file)
? `https://fake-endpoint.uppy.io/upload-bundle/${file.map((f) => f.name).join(',')}`
: '',
bundle: true,
})
core.addFile({
type: 'image/png',
source: 'test',
name: 'test.jpg',
data: new Blob([new Uint8Array(8192)]),
})
core.addFile({
type: 'image/png',
source: 'test',
name: 'test2.jpg',
data: new Blob([new Uint8Array(8192)]),
})
await core.upload()
expect(scope.isDone()).toBe(true)
})
})
})

View file

@ -28,7 +28,11 @@ import locale from './locale.js'
export interface XhrUploadOpts<M extends Meta, B extends Body>
extends PluginOpts {
endpoint: string
endpoint:
| string
| ((
fileOrBundle: UppyFile<M, B> | UppyFile<M, B>[],
) => string | Promise<string>)
method?:
| 'GET'
| 'HEAD'
@ -371,7 +375,11 @@ export default class XHRUpload<
const body = opts.formData
? this.createFormDataUpload(file, opts)
: file.data
return fetch(opts.endpoint, {
const endpoint =
typeof opts.endpoint === 'string'
? opts.endpoint
: await opts.endpoint(file)
return fetch(endpoint, {
...opts,
body,
signal: controller.signal,
@ -404,7 +412,11 @@ export default class XHRUpload<
...this.opts,
...optsFromState,
})
return fetch(this.opts.endpoint, {
const endpoint =
typeof this.opts.endpoint === 'string'
? this.opts.endpoint
: await this.opts.endpoint(files)
return fetch(endpoint, {
// headers can't be a function with bundle: true
...(this.opts as OptsWithHeaders<M, B>),
body,