diff --git a/examples/tus-min/.gitignore b/examples/tus-min/.gitignore deleted file mode 100644 index 8fc0d8027..000000000 --- a/examples/tus-min/.gitignore +++ /dev/null @@ -1 +0,0 @@ -uploads/ diff --git a/examples/tus-min/README.md b/examples/tus-min/README.md deleted file mode 100644 index 74fce63ae..000000000 --- a/examples/tus-min/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# example-tus-min - -Minimal reproduction for [issue #6287](https://github.com/transloadit/uppy/issues/6287): -`@uppy/core` + `@uppy/tus` (local file uploads only — no Dashboard, no -providers, no Companion) talking to a [`tus-node-server`](https://github.com/tus/tus-node-server) -backend (`@tus/server` + `@tus/file-store`). - -The server rejects any file whose name ends in `.bin` with `HTTP 403` and a JSON -body, mimicking the "BIN content type is disallowed" server in the bug report. -So one running server reproduces both paths: - -- a **normal file** → success path -- a **`*.bin` file** → error path (#6287) - -## Run - -From the repo root, make sure the workspace packages are built so the example -resolves the local `@uppy/tus`: - -```bash -yarn install -yarn workspace @uppy/tus build # or `yarn build` for everything -``` - -Then start the client + server together: - -```bash -yarn workspace example-tus-min start -``` - -- client: -- tus server: - -Uploaded files land in `examples/tus-min/uploads/` (gitignored). - -## What to look for - -Open and watch the on-page log (and the devtools -console). `window.uppy` is exposed for poking around. - -### Error path — pick a `*.bin` file (e.g. rename anything to `test.bin`) - -| | without the fix (`main`) | with the fix | -| --- | --- | --- | -| `upload-error` `response` | `undefined` | `{ status: 403, body: { xhr } }` | -| `response.body.xhr.responseText` | n/a | the server's JSON message | - -### Success path — pick any normal file - -| | without the fix (`main`) | with the fix | -| --- | --- | --- | -| `upload-success` `response.body.xhr.status` | `0` (xhr was reset) | `204` | - -> `response.status` is always `200` on success because the Tus plugin hardcodes -> it — the tell for the success-path bug is specifically -> `response.body.xhr.status === 0`. - -To compare buggy vs. fixed behavior, rebuild `@uppy/tus` on each branch -(`yarn workspace @uppy/tus build`) and restart. diff --git a/examples/tus-min/index.html b/examples/tus-min/index.html deleted file mode 100644 index 5b24823c7..000000000 --- a/examples/tus-min/index.html +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - Uppy + Tus — minimal local-upload repro (#6287) - - - -

Uppy + Tus — minimal local-upload repro

-

- Pick a normal file → success path.
- Pick a file whose name ends in .bin → server returns - HTTP 403 with a JSON body (error path, issue #6287). -

- -

Log (newest first)

-
- - - diff --git a/examples/tus-min/main.js b/examples/tus-min/main.js deleted file mode 100644 index 17915accb..000000000 --- a/examples/tus-min/main.js +++ /dev/null @@ -1,63 +0,0 @@ -import Uppy from '@uppy/core' -import Tus from '@uppy/tus' - -const TUS_ENDPOINT = 'http://localhost:1080/files/' - -const logEl = document.querySelector('#log') -function log(label, data) { - console.log(label, data) - const pre = document.createElement('pre') - pre.textContent = - data === undefined ? label : `${label}\n${JSON.stringify(data, null, 2)}` - logEl.prepend(pre) -} - -// Inspect the parts of `response` that the bug affects. `response.body.xhr` is -// an XMLHttpRequest, so we read its `status`/`responseText` rather than dumping -// the object. -function describeResponse(response) { - if (response === undefined) return '⚠️ response is undefined' - const xhr = response.body?.xhr - return { - status: response.status, - 'body.xhr.status': xhr?.status, - 'body.xhr.responseText': xhr?.responseText, - } -} - -// Only the Tus uploader — no Dashboard, no providers, no Companion. -const uppy = new Uppy({ autoProceed: true, debug: true }).use(Tus, { - endpoint: TUS_ENDPOINT, -}) - -uppy.on('upload-success', (file, response) => { - // BUG (success path): on a build without the fix, `body.xhr.status` is 0 - // because the completed request was aborted during cleanup. - log('✅ upload-success', describeResponse(response)) -}) - -uppy.on('upload-error', (file, error, response) => { - // BUG (#6287): on a build without the fix, `response` is `undefined`. - log('❌ upload-error', describeResponse(response)) - log(' error.message', error.message) -}) - -uppy.on('complete', (result) => { - const failed = result.failed?.[0] - log('🏁 complete — failed[0].response', failed ? describeResponse(failed.response) : '(none failed)') -}) - -document.querySelector('#file').addEventListener('change', (event) => { - const input = event.target - for (const file of input.files) { - try { - uppy.addFile({ name: file.name, type: file.type, data: file }) - } catch (err) { - log('addFile error', err.message) - } - } - input.value = '' -}) - -// Handy for poking at state from the devtools console. -window.uppy = uppy diff --git a/examples/tus-min/package.json b/examples/tus-min/package.json deleted file mode 100644 index 777223d4d..000000000 --- a/examples/tus-min/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "example-tus-min", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "start": "run-p start:server start:client", - "start:client": "vite", - "start:server": "node server.mjs" - }, - "dependencies": { - "@tus/file-store": "^2.1.0", - "@tus/server": "^2.4.1", - "@uppy/core": "workspace:*", - "@uppy/tus": "workspace:*" - }, - "devDependencies": { - "npm-run-all": "^4.1.5", - "vite": "^7.1.11" - } -} diff --git a/examples/tus-min/server.mjs b/examples/tus-min/server.mjs deleted file mode 100644 index 1976ed11e..000000000 --- a/examples/tus-min/server.mjs +++ /dev/null @@ -1,43 +0,0 @@ -import { FileStore } from '@tus/file-store' -import { Server } from '@tus/server' - -const host = '127.0.0.1' -const port = 1080 - -// The Vite dev client (start:client) runs on this origin. The tus server only -// sets CORS headers for origins listed here. -const clientOrigin = 'http://localhost:5173' - -const server = new Server({ - path: '/files', - datastore: new FileStore({ directory: './uploads' }), - allowedOrigins: [clientOrigin], - - // Reproduce issue #6287: reject some uploads with a non-2xx + JSON body, - // exactly like the server in the bug report ("BIN content type is disallowed"). - // - // Any file whose name ends in `.bin` is rejected at creation, so you can test - // BOTH paths against the same running server: - // - a normal file -> success path - // - a `*.bin` file -> error path (HTTP 403 with a JSON body) - async onUploadCreate(req, upload) { - const filename = upload.metadata?.filename ?? '' - if (filename.toLowerCase().endsWith('.bin')) { - throw { - status_code: 403, - body: JSON.stringify({ - message: - 'File cannot be uploaded as the BIN content type is disallowed!', - status_code: 403, - }), - } - } - // Accept the upload unchanged. (The hook must resolve to an object.) - return { metadata: upload.metadata } - }, -}) - -server.listen({ host, port }, () => { - console.log(`tus server listening on http://${host}:${port}/files`) - console.log(`CORS allowed for: ${clientOrigin}`) -}) diff --git a/examples/tus-min/vite.config.js b/examples/tus-min/vite.config.js deleted file mode 100644 index 294c4d522..000000000 --- a/examples/tus-min/vite.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'vite' - -// Fixed port so the server's CORS allow-list (http://localhost:5173) matches. -export default defineConfig({ - server: { port: 5173, strictPort: true }, -}) diff --git a/packages/@uppy/tus/src/index.test.ts b/packages/@uppy/tus/src/index.test.ts index f0f3ffb8c..3fc062f79 100644 --- a/packages/@uppy/tus/src/index.test.ts +++ b/packages/@uppy/tus/src/index.test.ts @@ -101,40 +101,5 @@ describe('Tus', () => { // `complete` result and via `getFile`. expect(core.getFile(id).response?.status).toBe(403) }) - - it('keeps the response readable after a successful upload', async () => { - const tusResumable = { 'Tus-Resumable': '1.0.0' } - nock('https://fake-endpoint.uppy.io') - .post('/files/') - .reply(201, '', { - ...tusResumable, - Location: 'https://fake-endpoint.uppy.io/files/abc', - }) - .patch('/files/abc') - .reply(204, '', { ...tusResumable, 'Upload-Offset': '1024' }) - - const core = new Core() - core.use(Tus, { - endpoint: 'https://fake-endpoint.uppy.io/files/', - retryDelays: [], - }) - const id = core.addFile({ - type: 'application/octet-stream', - source: 'test', - name: 'test.bin', - data: new Blob([new Uint8Array(1024)]), - }) - - const result = await core.upload() - expect(result?.successful).toHaveLength(1) - - // The response — including the underlying xhr — must still be readable - // after the upload completes (i.e. cleanup must not reset the xhr). - const response = core.getFile(id).response - expect(response?.status).toBe(200) - const { xhr } = response!.body! - expect(xhr).toBeInstanceOf(XMLHttpRequest) - expect(xhr.status).toBe(204) - }) }) }) diff --git a/packages/@uppy/tus/src/index.ts b/packages/@uppy/tus/src/index.ts index 0d6740a16..fbbadd1a8 100644 --- a/packages/@uppy/tus/src/index.ts +++ b/packages/@uppy/tus/src/index.ts @@ -234,8 +234,8 @@ export default class Tus extends BasePlugin< ): Promise { this.resetUploaderReferences(file.id) - // Captured in `onError` before the upload is aborted, so the failing - // server response can be forwarded to the `upload-error` event. + // Captured in `onError` and forwarded to the `upload-error` event in the + // `.catch` below, so consumers can read the failing server response. let errorResponse: | Omit['response']>, 'uploadURL'> | undefined @@ -380,10 +380,7 @@ export default class Tus extends BasePlugin< this.uppy.emit('upload-success', this.uppy.getFile(file.id), uploadResp) - // Do not abort the request here: the upload has completed, and aborting - // it would reset the underlying `xhr` (status `0`, empty body) that we - // just exposed on `response.body.xhr`. - this.resetUploaderReferences(file.id, { abortRequest: false }) + this.resetUploaderReferences(file.id) queuedRequest.done() if (upload.url) { diff --git a/yarn.lock b/yarn.lock index 6b6e741b1..3af9a61ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5681,23 +5681,6 @@ __metadata: languageName: node linkType: hard -"@redis/client@npm:^5.0.0": - version: 5.12.1 - resolution: "@redis/client@npm:5.12.1" - dependencies: - cluster-key-slot: "npm:1.1.2" - peerDependencies: - "@node-rs/xxhash": ^1.1.0 - "@opentelemetry/api": ">=1 <2" - peerDependenciesMeta: - "@node-rs/xxhash": - optional: true - "@opentelemetry/api": - optional: true - checksum: 10/6e7ac59761201ca0cb5eeff78e18b67eb0db2071b6bf3306d08697999a244329dd01a4d521963eb134da87c8ef6689ed52648f5fc15d6d2cd84a77cd5f3351c4 - languageName: node - linkType: hard - "@remix-run/node-fetch-server@npm:^0.9.0": version: 0.9.0 resolution: "@remix-run/node-fetch-server@npm:0.9.0" @@ -7676,20 +7659,6 @@ __metadata: languageName: node linkType: hard -"@tus/file-store@npm:^2.1.0": - version: 2.1.0 - resolution: "@tus/file-store@npm:2.1.0" - dependencies: - "@redis/client": "npm:^5.0.0" - "@tus/utils": "npm:^0.7.0" - debug: "npm:^4.3.4" - dependenciesMeta: - "@redis/client": - optional: true - checksum: 10/b057d9fffbb596213f83a9daf468d1bf0e25c1cb9a1eba517392d30ece00024ff9a225247225becec4fe7278738998ad19d3d94c2a975a4d465473f941dc66f6 - languageName: node - linkType: hard - "@tus/file-store@npm:latest": version: 2.0.0 resolution: "@tus/file-store@npm:2.0.0" @@ -7704,26 +7673,6 @@ __metadata: languageName: node linkType: hard -"@tus/server@npm:^2.4.1": - version: 2.4.1 - resolution: "@tus/server@npm:2.4.1" - dependencies: - "@redis/client": "npm:^5.0.0" - "@tus/utils": "npm:^0.7.0" - debug: "npm:^4.3.4" - ioredis: "npm:^5.4.1" - lodash.throttle: "npm:^4.1.1" - set-cookie-parser: "npm:^2.7.1" - srvx: "npm:~0.11.15" - dependenciesMeta: - "@redis/client": - optional: true - ioredis: - optional: true - checksum: 10/d1ac62b025bff046737482559d8a0ad7711cc53e70b339581cd698216bef1e124c26da45e79871b74b00767b486d8d43ca83d397e25ced29526e8d00c71a977b - languageName: node - linkType: hard - "@tus/server@npm:latest": version: 2.3.0 resolution: "@tus/server@npm:2.3.0" @@ -7751,13 +7700,6 @@ __metadata: languageName: node linkType: hard -"@tus/utils@npm:^0.7.0": - version: 0.7.0 - resolution: "@tus/utils@npm:0.7.0" - checksum: 10/2c5235e8cbdc0c795f3db36261317244e27b2ca1c210c4f2a37cf36eea7d5f2f75f555ee9b629231c89a78844539dbc905802a1769ea5229d25ec8f405dec17c - languageName: node - linkType: hard - "@tybys/wasm-util@npm:^0.10.0": version: 0.10.0 resolution: "@tybys/wasm-util@npm:0.10.0" @@ -13193,19 +13135,6 @@ __metadata: languageName: unknown linkType: soft -"example-tus-min@workspace:examples/tus-min": - version: 0.0.0-use.local - resolution: "example-tus-min@workspace:examples/tus-min" - dependencies: - "@tus/file-store": "npm:^2.1.0" - "@tus/server": "npm:^2.4.1" - "@uppy/core": "workspace:*" - "@uppy/tus": "workspace:*" - npm-run-all: "npm:^4.1.5" - vite: "npm:^7.1.11" - languageName: unknown - linkType: soft - "example-vue@workspace:examples/vue": version: 0.0.0-use.local resolution: "example-vue@workspace:examples/vue" @@ -21374,15 +21303,6 @@ __metadata: languageName: node linkType: hard -"srvx@npm:~0.11.15": - version: 0.11.16 - resolution: "srvx@npm:0.11.16" - bin: - srvx: bin/srvx.mjs - checksum: 10/5ec582e92d618220e24bbe9efe4900215081b1949327ee165f3cf4b01e766c458a8aaba50347e8fa1b536ac8b854e0e6d8cd970c62c64278487867699243124d - languageName: node - linkType: hard - "srvx@npm:~0.8.2": version: 0.8.7 resolution: "srvx@npm:0.8.7"