From 031bfcaf1d8d92fd7f79b3c6bd27594705fe3bc1 Mon Sep 17 00:00:00 2001 From: Prakash Date: Fri, 29 May 2026 16:31:51 +0530 Subject: [PATCH] test(tus): add minimal local-upload example to reproduce #6287 @uppy/core + @uppy/tus (local files only, no Dashboard/providers/Companion) against a tus-node-server backend that rejects *.bin files with HTTP 403 + a JSON body. One running server reproduces both the error path (#6287) and the success path. --- examples/tus-min/.gitignore | 1 + examples/tus-min/README.md | 59 ++++++++++++++++++++++++ examples/tus-min/index.html | 46 +++++++++++++++++++ examples/tus-min/main.js | 63 ++++++++++++++++++++++++++ examples/tus-min/package.json | 21 +++++++++ examples/tus-min/server.mjs | 43 ++++++++++++++++++ examples/tus-min/vite.config.js | 6 +++ yarn.lock | 80 +++++++++++++++++++++++++++++++++ 8 files changed, 319 insertions(+) create mode 100644 examples/tus-min/.gitignore create mode 100644 examples/tus-min/README.md create mode 100644 examples/tus-min/index.html create mode 100644 examples/tus-min/main.js create mode 100644 examples/tus-min/package.json create mode 100644 examples/tus-min/server.mjs create mode 100644 examples/tus-min/vite.config.js diff --git a/examples/tus-min/.gitignore b/examples/tus-min/.gitignore new file mode 100644 index 000000000..8fc0d8027 --- /dev/null +++ b/examples/tus-min/.gitignore @@ -0,0 +1 @@ +uploads/ diff --git a/examples/tus-min/README.md b/examples/tus-min/README.md new file mode 100644 index 000000000..74fce63ae --- /dev/null +++ b/examples/tus-min/README.md @@ -0,0 +1,59 @@ +# 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 new file mode 100644 index 000000000..5b24823c7 --- /dev/null +++ b/examples/tus-min/index.html @@ -0,0 +1,46 @@ + + + + + + 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 new file mode 100644 index 000000000..17915accb --- /dev/null +++ b/examples/tus-min/main.js @@ -0,0 +1,63 @@ +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 new file mode 100644 index 000000000..777223d4d --- /dev/null +++ b/examples/tus-min/package.json @@ -0,0 +1,21 @@ +{ + "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 new file mode 100644 index 000000000..1976ed11e --- /dev/null +++ b/examples/tus-min/server.mjs @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..294c4d522 --- /dev/null +++ b/examples/tus-min/vite.config.js @@ -0,0 +1,6 @@ +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/yarn.lock b/yarn.lock index 3af9a61ed..6b6e741b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5681,6 +5681,23 @@ __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" @@ -7659,6 +7676,20 @@ __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" @@ -7673,6 +7704,26 @@ __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" @@ -7700,6 +7751,13 @@ __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" @@ -13135,6 +13193,19 @@ __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" @@ -21303,6 +21374,15 @@ __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"