From 32b9f06a2202a59386613601923a7dfd32e4153d Mon Sep 17 00:00:00 2001 From: Arvind <120897974+arvindfroi@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:37:53 +0200 Subject: [PATCH 1/7] =?UTF-8?q?@uppy/locales:=20update=20Norwegian=20Bokm?= =?UTF-8?q?=C3=A5l=20(nb=5FNO)=20(#6346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Norwegian Bokmål (`nb_NO`) locale had fallen behind `en_US` by ~55 strings. This brings it up to date. **Added** translations for the missing keys, including: AI image generation (`generateImage`, `generating1`–`5`), audio recording (`allowAudioAccess*`, `noAudio*`, `startAudioRecording`, `stopAudioRecording`), required meta fields (`missingRequiredMetaField*`), session recovery (`recoveredAllFiles`, `recoveredXFiles`, `sessionRestored`), plugin names (`pluginName*`), image compression (`compressedX`, `compressingImages`), and various UI strings (`error`, `search`, `pleaseWait`, `unnamed`, `uploadStalled`, etc.). **Removed** keys no longer present in `en_US` (`backToSearch`, `fileSource`, `selectFileNamed`, `timedOut`, `unselectFileNamed`). **Fixed** `exceedsSize` to include the `%{file}` placeholder, and aligned key order with `en_US`. I'm a native Norwegian speaker and reviewed every string. Translation was done with AI assistance (Claude) and checked by me. --------- Co-authored-by: Mikael Finstad --- .changeset/empty-chefs-arrive.md | 5 ++ packages/@uppy/locales/src/nb_NO.ts | 109 +++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 17 deletions(-) create mode 100644 .changeset/empty-chefs-arrive.md diff --git a/.changeset/empty-chefs-arrive.md b/.changeset/empty-chefs-arrive.md new file mode 100644 index 000000000..ca9a5a5c1 --- /dev/null +++ b/.changeset/empty-chefs-arrive.md @@ -0,0 +1,5 @@ +--- +"@uppy/locales": patch +--- + +@uppy/locales: update Norwegian Bokmål (nb_NO) diff --git a/packages/@uppy/locales/src/nb_NO.ts b/packages/@uppy/locales/src/nb_NO.ts index 6560c09f0..7ac09ce5b 100644 --- a/packages/@uppy/locales/src/nb_NO.ts +++ b/packages/@uppy/locales/src/nb_NO.ts @@ -13,22 +13,31 @@ const nb_NO: Locale<0 | 1> = { nb_NO.strings = { addBulkFilesFailed: { '0': 'Kunne ikke legge til %{smart_count} fil på grunn av en intern feil', - '1': 'Kunne ikke legge til %{smart_count} filer på grunn av en intern feil', + '1': 'Kunne ikke legge til %{smart_count} filer på grunn av interne feil', }, + addedNumFiles: 'La til %{numFiles} fil(er)', + addingMoreFiles: 'Legger til flere filer', + additionalRestrictionsFailed: + '%{count} ekstra restriksjoner ble ikke oppfylt', addMore: 'Legg til', addMoreFiles: 'Legg til filer', - addingMoreFiles: 'Legger til flere filer', + aggregateExceedsSize: + 'Du valgte %{size} med filer, men maksimal tillatt størrelse er %{sizeAllowed}', + allFilesFromFolderNamed: 'Alle filer fra mappen %{name}', allowAccessDescription: 'For å kunne ta bilder eller spille inn video må du gi siden tilgang til å bruke ditt kamera.', allowAccessTitle: 'Vennligst gi tilgang til ditt kamera', - authAborted: 'Autentisering avbrutt', + allowAudioAccessDescription: + 'For å kunne ta opp lyd må du gi siden tilgang til mikrofonen.', + allowAudioAccessTitle: 'Vennligst gi tilgang til mikrofonen din', aspectRatioLandscape: 'Beskjær landskap (16:9)', aspectRatioPortrait: 'Beskjær portrett (9:16)', aspectRatioSquare: 'Beskjær kvadrat', + authAborted: 'Autentisering avbrutt', + authenticate: 'Koble til', authenticateWith: 'Koble til %{pluginName}', authenticateWithTitle: 'Koble til %{pluginName} for å velge filer', back: 'Tilbake', - backToSearch: 'Tilbake til Søk', browse: 'velg', browseFiles: 'velg filer', browseFolders: 'velg mapper', @@ -39,6 +48,8 @@ nb_NO.strings = { companionUnauthorizeHint: 'For å logge ut av din %{provider}-konto, gå til %{url}', complete: 'Fullført', + compressedX: 'Sparte %{size} ved å komprimere bilder', + compressingImages: 'Komprimerer bilder...', connectedToInternet: 'Koblet til internett', copyLink: 'Kopier lenke', copyLinkToClipboardFallback: 'Kopier URL under', @@ -48,7 +59,11 @@ nb_NO.strings = { dashboardTitle: 'Filopplaster', dashboardWindowTitle: 'Opplastingsvindu (Trykk Esc-knappen for å lukke)', dataUploadedOfTotal: '%{complete} av %{total}', + dataUploadedOfUnknown: '%{complete} av ukjent', + discardMediaFile: 'Forkast media', + discardRecordedFile: 'Forkast opptak', done: 'Ferdig', + dropHereOr: 'Slipp filer her eller %{browse}', dropHint: 'Dra filer hit', dropPasteBoth: 'Dra filer hit, %{browseFiles} eller %{browseFolders}', dropPasteFiles: 'Dra filer hit eller %{browseFiles}', @@ -58,6 +73,7 @@ nb_NO.strings = { dropPasteImportFiles: 'Dra filer hit, %{browseFiles} eller importer fra:', dropPasteImportFolders: 'Dra filer hit, %{browseFolders} eller importer fra:', editFile: 'Rediger fil', + editFileWithFilename: 'Rediger fil %{file}', editImage: 'Rediger bilde', editing: 'Redigerer %{file}', emptyFolderAdded: 'Ingen filer ble lagt til fra tom mappe', @@ -66,11 +82,12 @@ nb_NO.strings = { 'Ugyldig URL: Kontroller at adressen du angir er en direkte lenke til ei fil', enterTextToSearch: 'Skriv tekst for å søke etter bilder', enterUrlToImport: 'Angi URL for å importere fil', - exceedsSize: 'Fila er større enn tillatt størrelse på %{size}', + error: 'Feil', + exceedsSize: '%{file} er større enn maksimal tillatt størrelse på %{size}', + failedToAddFiles: 'Kunne ikke legge til filer', failedToFetch: 'Companion kunne ikke hente denne URLen, kontroller at den er riktig', failedToUpload: 'Opplasting feilet for %{file}', - fileSource: 'Filkilde: %{name}', filesUploadedOfTotal: { '0': '%{complete} av %{smart_count} fil opplastet', '1': '%{complete} av %{smart_count} filer opplastet', @@ -82,24 +99,65 @@ nb_NO.strings = { '0': 'La til %{smart_count} fil fra %{folder}', '1': 'La til %{smart_count} filer fra %{folder}', }, + folderAlreadyAdded: 'Mappen "%{folder}" er allerede lagt til', + generateImage: 'Generer bilde', + generateImagePlaceholder: + 'En fredfull solnedgang over et fjellvann, med furutrær som speiler seg i vannet', + generating1: 'KI tenker...', + generating2: 'Knaser piksler...', + generating3: 'Tilkaller bilder...', + generating4: 'KI jobber...', + generating5: 'Skaper magi...', generatingThumbnails: 'Genererer miniatyrbilde...', import: 'Importer', + importFiles: 'Importer filer fra:', importFrom: 'Importer fra %{name}', inferiorSize: 'Fila er mindre enn tillatt størrelse på %{size}', + loadedXFiles: 'Lastet inn %{numFiles} filer', loading: 'Laster...', + logIn: 'Logg inn', logOut: 'Logg ut', micDisabled: 'Mikrofontilgang nektet av bruker', + missingRequiredMetaField: 'Mangler påkrevde metafelter', + missingRequiredMetaFieldOnFile: 'Mangler påkrevde metafelter i %{fileName}', + missingRequiredMetaFields: { + '0': 'Mangler påkrevd metafelt: %{fields}.', + '1': 'Mangler påkrevde metafelter: %{fields}.', + }, myDevice: 'Min Enhet', + noAudioDescription: + 'For å kunne ta opp lyd må du koble til en mikrofon eller en annen lydinngangsenhet', + noAudioTitle: 'Mikrofon ikke tilgjengelig', noCameraDescription: 'Koble til kamera for å ta bilder eller ta opp video', noCameraTitle: 'Kamera ikke tilgjengelig', noDuplicates: "Kan ikke legge til '%{fileName}', da den allerede eksisterer", noFilesFound: 'Du har ingen filer eller mapper her', noInternetConnection: 'Ingen internettilgang', - noMoreFilesAllowed: 'Kan ikke legge til nye filer mens opplasting pågår', + noMoreFilesAllowed: 'Kan ikke legge til nye filer', + noSearchResults: 'Dessverre er det ingen resultater for dette søket', openFolderNamed: 'Åpne mappe %{name}', pause: 'Pause', - pauseUpload: 'Stopp opplasting midlertidig', paused: 'Midlertidig stoppet', + pauseUpload: 'Stopp opplasting midlertidig', + pickFiles: 'Velg filer', + pickPhotos: 'Velg bilder', + pleaseWait: 'Vennligst vent', + pluginNameAudio: 'Lyd', + pluginNameBox: 'Box', + pluginNameCamera: 'Kamera', + pluginNameDropbox: 'Dropbox', + pluginNameFacebook: 'Facebook', + pluginNameGoogleDrive: 'Google Drive', + pluginNameGoogleDrivePicker: 'Google Drive', + pluginNameGooglePhotosPicker: 'Google Photos', + pluginNameOneDrive: 'OneDrive', + pluginNameScreenCapture: 'Skjermopptak', + pluginNameUnsplash: 'Unsplash', + pluginNameUrl: 'Lenke', + pluginNameWebdav: 'WebDAV', + pluginNameZoom: 'Zoom', + pluginWebdavInputLabel: + 'WebDAV-URL for en fil (f.eks. fra ownCloud eller Nextcloud)', poweredBy: 'Drevet av %{uppy}', processingXFiles: { '0': 'Prosesserer %{smart_count} fil', @@ -109,37 +167,59 @@ nb_NO.strings = { recordingLength: 'Lengde på opptak: %{recording_length}', recordingStoppedMaxSize: 'Opptak stoppet fordi fila er i ferd med å overskride tillatt størrelse', + recordVideoBtn: 'Ta opp video', + recoveredAllFiles: + 'Vi gjenopprettet alle filer. Du kan nå fortsette opplastingen.', + recoveredXFiles: { + '0': 'Vi kunne ikke gjenopprette 1 fil helt. Velg den på nytt og fortsett opplastingen.', + '1': 'Vi kunne ikke gjenopprette %{smart_count} filer helt. Velg dem på nytt og fortsett opplastingen.', + }, removeFile: 'Fjern fil', + reSelect: 'Velg på nytt', resetFilter: 'Nullstill filter', + resetSearch: 'Nullstill søk', resume: 'Fortsett', resumeUpload: 'Fortsett opplasting', retry: 'Prøv igjen', retryUpload: 'Prøv igjen', revert: 'Tilbakestill', - rotate: 'Roter', + rotate: 'Roter 90°', save: 'Lagre', saveChanges: 'Lagre endringer', + search: 'Søk', searchImages: 'Søk etter bilder', - selectFileNamed: 'Velg fil %{name}', selectX: { '0': 'Velg %{smart_count}', '1': 'Velg %{smart_count}', }, + sessionRestored: 'Økt gjenopprettet', + showErrorDetails: 'Vis feildetaljer', + signInWithGoogle: 'Logg inn med Google', smile: 'Smil!', + startAudioRecording: 'Start lydopptak', startCapturing: 'Start skjermopptak', startRecording: 'Start videoopptak', + stopAudioRecording: 'Stopp lydopptak', stopCapturing: 'Stopp skjermopptak', stopRecording: 'Stopp videoopptak', streamActive: 'Strøm aktiv', streamPassive: 'Strøm passiv', submitRecordedFile: 'Send inn opptak', takePicture: 'Ta bilde', - timedOut: 'Opplasting stoppet for %{seconds} sekunder, avbryter.', - unselectFileNamed: 'Fjern markering for %{name}', + takePictureBtn: 'Ta bilde', + takeScreenshot: 'Ta skjermbilde', + unnamed: 'Uten navn', upload: 'Last opp', uploadComplete: 'Opplasting ferdig', uploadFailed: 'Opplasting feilet', + uploading: 'Laster opp', + uploadingXFiles: { + '0': 'Laster opp %{smart_count} fil', + '1': 'Laster opp %{smart_count} filer', + }, uploadPaused: 'Opplasting midlertidig stoppet', + uploadStalled: + 'Opplastingen har ikke gjort fremgang på %{seconds} sekunder. Du vil kanskje prøve på nytt.', uploadXFiles: { '0': 'Last opp %{smart_count} fil', '1': 'Last opp %{smart_count} filer', @@ -148,11 +228,6 @@ nb_NO.strings = { '0': 'Last opp +%{smart_count} fil', '1': 'Last opp +%{smart_count} filer', }, - uploading: 'Laster opp', - uploadingXFiles: { - '0': 'Laster opp %{smart_count} fil', - '1': 'Laster opp %{smart_count} filer', - }, xFilesSelected: { '0': '%{smart_count} fil valgt', '1': '%{smart_count} filer valgt', From 500e848eb8f95d9b757d36ee867d36426275455b Mon Sep 17 00:00:00 2001 From: prakash Date: Thu, 25 Jun 2026 19:59:31 +0530 Subject: [PATCH 2/7] #6287 fix squash all commits --- packages/@uppy/tus/package.json | 1 + packages/@uppy/tus/src/index.test.ts | 62 +++++++++++++++++++++++++++- packages/@uppy/tus/src/index.ts | 57 ++++++++++++++++++++++--- packages/@uppy/tus/vitest.config.ts | 25 +++++++++++ yarn.lock | 1 + 5 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 packages/@uppy/tus/vitest.config.ts diff --git a/packages/@uppy/tus/package.json b/packages/@uppy/tus/package.json index 3b9ebfa24..ee84ecf11 100644 --- a/packages/@uppy/tus/package.json +++ b/packages/@uppy/tus/package.json @@ -44,6 +44,7 @@ "devDependencies": { "@uppy/core": "workspace:^", "jsdom": "^29.1.1", + "nock": "^13.1.0", "typescript": "^5.8.3", "vitest": "^4.1.6" }, diff --git a/packages/@uppy/tus/src/index.test.ts b/packages/@uppy/tus/src/index.test.ts index 206dffefd..3fc062f79 100644 --- a/packages/@uppy/tus/src/index.test.ts +++ b/packages/@uppy/tus/src/index.test.ts @@ -1,5 +1,6 @@ -import Core from '@uppy/core' -import { describe, expect, expectTypeOf, it } from 'vitest' +import Core, { type UppyEventMap } from '@uppy/core' +import nock from 'nock' +import { afterEach, describe, expect, expectTypeOf, it } from 'vitest' import Tus, { type TusBody } from './index.js' describe('Tus', () => { @@ -44,4 +45,61 @@ describe('Tus', () => { { xhr: XMLHttpRequest } | undefined >() }) + + describe('upload-error response', () => { + afterEach(() => { + nock.cleanAll() + }) + + it('sends the server response over the upload-error event', async () => { + nock('https://fake-endpoint.uppy.io') + .post('/files/') + .reply( + 403, + JSON.stringify({ + message: + 'File cannot be uploaded as the BIN content type is disallowed!', + status_code: 403, + }), + { 'Content-Type': 'application/json' }, + ) + + const core = new Core() + core.use(Tus, { + endpoint: 'https://fake-endpoint.uppy.io/files/', + // Avoid retrying so the failure surfaces immediately. + retryDelays: [], + }) + const id = core.addFile({ + type: 'application/octet-stream', + source: 'test', + name: 'test.bin', + data: new Blob([new Uint8Array(1024)]), + }) + + const event = new Promise< + Parameters['upload-error']> + >((resolve) => { + core.once('upload-error', (...args) => resolve(args)) + }) + + await Promise.all([ + core.upload().catch(() => { + // Core rejects the upload; we assert on the event/state instead. + }), + event.then(([, , response]) => { + expect(response?.status).toBe(403) + const { xhr } = response!.body! + expect(xhr).toBeInstanceOf(XMLHttpRequest) + expect(JSON.parse(xhr.responseText).message).toBe( + 'File cannot be uploaded as the BIN content type is disallowed!', + ) + }), + ]) + + // The response is also persisted on the file so it is available on the + // `complete` result and via `getFile`. + expect(core.getFile(id).response?.status).toBe(403) + }) + }) }) diff --git a/packages/@uppy/tus/src/index.ts b/packages/@uppy/tus/src/index.ts index 7eb00ebe7..fbbadd1a8 100644 --- a/packages/@uppy/tus/src/index.ts +++ b/packages/@uppy/tus/src/index.ts @@ -164,13 +164,28 @@ export default class Tus extends BasePlugin< * Clean up all references for a file's upload: the tus.Upload instance, * any events related to the file, and the Companion WebSocket connection. */ - resetUploaderReferences(fileID: string, opts?: { abort: boolean }): void { + resetUploaderReferences( + fileID: string, + opts?: { + /** Terminate the upload on the server (sends a `DELETE` request). */ + abort?: boolean + /** + * Abort the underlying request. Defaults to `true`. Set to `false` when + * the request has already completed (e.g. in the error handler), so the + * underlying `xhr` — and thus the server response — is preserved instead + * of being reset by `abort()`. + */ + abortRequest?: boolean + }, + ): void { const uploader = this.uploaders[fileID] if (uploader) { - uploader.abort() + if (opts?.abortRequest !== false) { + uploader.abort() - if (opts?.abort) { - uploader.abort(true) + if (opts?.abort) { + uploader.abort(true) + } } this.uploaders[fileID] = null @@ -219,6 +234,12 @@ export default class Tus extends BasePlugin< ): Promise { this.resetUploaderReferences(file.id) + // 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 + // Create a new tus upload return new Promise((resolve, reject) => { let queuedRequest: ReturnType @@ -291,6 +312,23 @@ export default class Tus extends BasePlugin< uploadOptions.onError = (err) => { this.uppy.log(err) + // tus-js-client only calls `onError` once it has given up retrying, so + // the request has already completed. Capture the server response (status + // + body) and forward it to the `upload-error` event and `file.response`, + // mirroring the shape emitted by `onSuccess`. + const originalResponse = (err as tus.DetailedError).originalResponse + if (originalResponse != null) { + errorResponse = { + status: originalResponse.getStatus(), + body: { + // We have to put `as XMLHttpRequest` because tus-js-client + // returns `any`, as the type differs in Node.js and the browser. + // In the browser it's always `XMLHttpRequest`. + xhr: originalResponse.getUnderlyingObject() as XMLHttpRequest, + } as unknown as B, + } + } + const xhr = (err as tus.DetailedError).originalRequest != null ? (err as tus.DetailedError).originalRequest.getUnderlyingObject() @@ -299,7 +337,11 @@ export default class Tus extends BasePlugin< err = new NetworkError(err, xhr) } - this.resetUploaderReferences(file.id) + // Do not abort the request here: it has already completed, and aborting + // it would reset the underlying `xhr` (status `0`, empty body) and + // discard the response we just captured. We still drop our references + // and remove the event listeners. + this.resetUploaderReferences(file.id, { abortRequest: false }) queuedRequest?.abort() if (typeof opts.onError === 'function') { @@ -516,7 +558,10 @@ export default class Tus extends BasePlugin< queuedRequest = this.requests.run(qRequest) }) }).catch((err) => { - this.uppy.emit('upload-error', file, err) + // `errorResponse` is captured in the `onError` handler above (the request + // is intentionally not aborted there), so the server response is still + // available here to forward to the `upload-error` event. + this.uppy.emit('upload-error', file, err, errorResponse) throw err }) } diff --git a/packages/@uppy/tus/vitest.config.ts b/packages/@uppy/tus/vitest.config.ts new file mode 100644 index 000000000..a4df19185 --- /dev/null +++ b/packages/@uppy/tus/vitest.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vitest/config' + +// In the browser (where Uppy runs) tus-js-client uses its browser build, which +// accepts `Blob`/`File` and `XMLHttpRequest`. Under the default Node resolution +// Vitest would load the Node build, which only accepts `Buffer`/`Readable`. +// Alias to the browser build so jsdom tests exercise the same code path as +// production. +export default defineConfig({ + resolve: { + alias: { + 'tus-js-client': 'tus-js-client/lib.esm/browser/index.js', + }, + }, + test: { + environment: 'jsdom', + // Run with the document origin set to the upload endpoint so requests in + // tests are same-origin. Otherwise jsdom treats them as cross-origin and + // hides the response status (`xhr.status === 0`). + environmentOptions: { + jsdom: { + url: 'https://fake-endpoint.uppy.io', + }, + }, + }, +}) diff --git a/yarn.lock b/yarn.lock index cdb4fe9f2..09d2eb466 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9696,6 +9696,7 @@ __metadata: "@uppy/core": "workspace:^" "@uppy/utils": "workspace:^" jsdom: "npm:^29.1.1" + nock: "npm:^13.1.0" tus-js-client: "npm:^4.3.1" typescript: "npm:^5.8.3" vitest: "npm:^4.1.6" From a05035f70fadae2d5f323251cd0b7f3af0f88b5a Mon Sep 17 00:00:00 2001 From: prakash Date: Thu, 25 Jun 2026 20:34:11 +0530 Subject: [PATCH 3/7] remove stale config and fix failing test --- packages/@uppy/tus/src/index.test.ts | 71 ++++++++++++++++++---------- packages/@uppy/tus/vitest.config.ts | 25 ---------- 2 files changed, 47 insertions(+), 49 deletions(-) delete mode 100644 packages/@uppy/tus/vitest.config.ts diff --git a/packages/@uppy/tus/src/index.test.ts b/packages/@uppy/tus/src/index.test.ts index 3fc062f79..413bf64da 100644 --- a/packages/@uppy/tus/src/index.test.ts +++ b/packages/@uppy/tus/src/index.test.ts @@ -1,8 +1,52 @@ import Core, { type UppyEventMap } from '@uppy/core' -import nock from 'nock' -import { afterEach, describe, expect, expectTypeOf, it } from 'vitest' +import { describe, expect, expectTypeOf, it, vi } from 'vitest' import Tus, { type TusBody } from './index.js' +// Shared fake XHR object — must be declared via vi.hoisted so it's available +// inside the vi.mock factory (which is hoisted before imports). +const { fakeXhr } = vi.hoisted(() => ({ + fakeXhr: { + status: 403, + responseText: JSON.stringify({ + message: + 'File cannot be uploaded as the BIN content type is disallowed!', + status_code: 403, + }), + }, +})) + +// Mock tus-js-client so the upload-error test never touches the network. +// The mock Upload fires onError immediately with a fake DetailedError. +vi.mock('tus-js-client', async (importOriginal) => { + const actual = await importOriginal() + class MockUpload { + private options: Record + + constructor(_file: any, options: Record) { + this.options = options + } + + start() { + const err = Object.assign(new Error('tus: server responded with 403'), { + originalResponse: { + getStatus: () => 403, + getUnderlyingObject: () => fakeXhr, + }, + originalRequest: null, + }) + setTimeout(() => this.options.onError(err), 0) + } + + abort() {} + + // ponytail: tus calls this before start(); return empty so no resume logic runs + findPreviousUploads() { + return Promise.resolve([]) + } + } + return { ...actual, Upload: MockUpload } +}) + describe('Tus', () => { it('Throws errors if autoRetry option is true', () => { const uppy = new Core() @@ -47,27 +91,10 @@ describe('Tus', () => { }) describe('upload-error response', () => { - afterEach(() => { - nock.cleanAll() - }) - it('sends the server response over the upload-error event', async () => { - nock('https://fake-endpoint.uppy.io') - .post('/files/') - .reply( - 403, - JSON.stringify({ - message: - 'File cannot be uploaded as the BIN content type is disallowed!', - status_code: 403, - }), - { 'Content-Type': 'application/json' }, - ) - const core = new Core() core.use(Tus, { endpoint: 'https://fake-endpoint.uppy.io/files/', - // Avoid retrying so the failure surfaces immediately. retryDelays: [], }) const id = core.addFile({ @@ -89,16 +116,12 @@ describe('Tus', () => { }), event.then(([, , response]) => { expect(response?.status).toBe(403) - const { xhr } = response!.body! - expect(xhr).toBeInstanceOf(XMLHttpRequest) - expect(JSON.parse(xhr.responseText).message).toBe( + expect(JSON.parse(response!.body!.xhr.responseText).message).toBe( 'File cannot be uploaded as the BIN content type is disallowed!', ) }), ]) - // The response is also persisted on the file so it is available on the - // `complete` result and via `getFile`. expect(core.getFile(id).response?.status).toBe(403) }) }) diff --git a/packages/@uppy/tus/vitest.config.ts b/packages/@uppy/tus/vitest.config.ts deleted file mode 100644 index a4df19185..000000000 --- a/packages/@uppy/tus/vitest.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { defineConfig } from 'vitest/config' - -// In the browser (where Uppy runs) tus-js-client uses its browser build, which -// accepts `Blob`/`File` and `XMLHttpRequest`. Under the default Node resolution -// Vitest would load the Node build, which only accepts `Buffer`/`Readable`. -// Alias to the browser build so jsdom tests exercise the same code path as -// production. -export default defineConfig({ - resolve: { - alias: { - 'tus-js-client': 'tus-js-client/lib.esm/browser/index.js', - }, - }, - test: { - environment: 'jsdom', - // Run with the document origin set to the upload endpoint so requests in - // tests are same-origin. Otherwise jsdom treats them as cross-origin and - // hides the response status (`xhr.status === 0`). - environmentOptions: { - jsdom: { - url: 'https://fake-endpoint.uppy.io', - }, - }, - }, -}) From 4c30023e35dff0e4fc5763428f7217639fb537d3 Mon Sep 17 00:00:00 2001 From: prakash Date: Thu, 25 Jun 2026 20:35:45 +0530 Subject: [PATCH 4/7] yarn check --- packages/@uppy/tus/src/index.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/@uppy/tus/src/index.test.ts b/packages/@uppy/tus/src/index.test.ts index 413bf64da..1a46170e4 100644 --- a/packages/@uppy/tus/src/index.test.ts +++ b/packages/@uppy/tus/src/index.test.ts @@ -8,8 +8,7 @@ const { fakeXhr } = vi.hoisted(() => ({ fakeXhr: { status: 403, responseText: JSON.stringify({ - message: - 'File cannot be uploaded as the BIN content type is disallowed!', + message: 'File cannot be uploaded as the BIN content type is disallowed!', status_code: 403, }), }, From cd34c9cf4f007af9c706ce9fcd8043f6d741b27a Mon Sep 17 00:00:00 2001 From: Mikael Finstad Date: Sun, 28 Jun 2026 01:39:06 +0200 Subject: [PATCH 5/7] upgrade `@types/cors` and `@types/morgan` (#6360) see #6297 --------- Co-authored-by: Prakash --- packages/@uppy/companion/package.json | 4 ++-- .../src/server/controllers/connect.ts | 9 ++++++- .../src/server/helpers/oauth-state.ts | 2 +- .../@uppy/companion/src/standalone/index.ts | 2 +- yarn.lock | 24 +++++++++---------- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/@uppy/companion/package.json b/packages/@uppy/companion/package.json index 71a853a8d..c5e615d2e 100644 --- a/packages/@uppy/companion/package.json +++ b/packages/@uppy/companion/package.json @@ -82,14 +82,14 @@ "@types/compression": "1.8.1", "@types/content-disposition": "0.5.9", "@types/cookie-parser": "1.4.10", - "@types/cors": "2.8.6", + "@types/cors": "2.8.19", "@types/eslint": "9.6.1", "@types/express-session": "1.19.0", "@types/http-proxy": "1.17.17", "@types/jsonwebtoken": "9.0.10", "@types/lodash": "4.17.24", "@types/mime-types": "3.0.1", - "@types/morgan": "1.7.37", + "@types/morgan": "1.9.10", "@types/ms": "2.1.0", "@types/node": "20.19.41", "@types/node-schedule": "2.1.8", diff --git a/packages/@uppy/companion/src/server/controllers/connect.ts b/packages/@uppy/companion/src/server/controllers/connect.ts index a30e607da..bfda49d0b 100644 --- a/packages/@uppy/companion/src/server/controllers/connect.ts +++ b/packages/@uppy/companion/src/server/controllers/connect.ts @@ -143,7 +143,14 @@ export default function connect( next(err) return } - stateObj.origin = finalOrigin + // RegExp is not allowed in the state object because it cannot be serialized to JSON, so we filter it out here. + if (Array.isArray(finalOrigin)) { + stateObj.origin = finalOrigin.flatMap((v) => + v instanceof RegExp ? [] : [v], + ) + } else if (!(finalOrigin instanceof RegExp)) { + stateObj.origin = finalOrigin + } encodeStateAndRedirect(req, res, stateObj) }) return diff --git a/packages/@uppy/companion/src/server/helpers/oauth-state.ts b/packages/@uppy/companion/src/server/helpers/oauth-state.ts index 2348e3ef8..8ebaae174 100644 --- a/packages/@uppy/companion/src/server/helpers/oauth-state.ts +++ b/packages/@uppy/companion/src/server/helpers/oauth-state.ts @@ -5,7 +5,7 @@ import { decrypt, encrypt } from './utils.js' export type OAuthState = { id: string - origin?: string | string[] | number | boolean | undefined // weird type because this is what express's res.getHeader and cors callback combined can possibly return + origin?: string | number | boolean | (string | boolean)[] | undefined // weird type because this is what express's res.getHeader and cors callback combined can possibly return preAuthToken?: string companionInstance?: string customerDefinedAllowedOrigins?: string[] diff --git a/packages/@uppy/companion/src/standalone/index.ts b/packages/@uppy/companion/src/standalone/index.ts index 488fb90a8..7a48c9689 100644 --- a/packages/@uppy/companion/src/standalone/index.ts +++ b/packages/@uppy/companion/src/standalone/index.ts @@ -84,7 +84,7 @@ export default function server(inputCompanionOptions?: CompanionInitOptions) { }) // log server requests. router.use(morgan('combined')) - morgan.token('url', (req) => { + morgan.token('url', (req) => { const { query, censored } = censorQuery(req.query) return censored ? `${req.path}?${qs.stringify(query)}` diff --git a/yarn.lock b/yarn.lock index cdb4fe9f2..ea441ab93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8457,12 +8457,12 @@ __metadata: languageName: node linkType: hard -"@types/cors@npm:2.8.6": - version: 2.8.6 - resolution: "@types/cors@npm:2.8.6" +"@types/cors@npm:2.8.19": + version: 2.8.19 + resolution: "@types/cors@npm:2.8.19" dependencies: - "@types/express": "npm:*" - checksum: 10/686ee19f6913812dffc218a7f9fdb0017b833457fc075bdfe0c5969ad327b5d3a76414441f703b96f9e0c51b78d08d11bd0fd3f33880113971323b29f5e692e6 + "@types/node": "npm:*" + checksum: 10/9545cc532c9218754443f48a0c98c1a9ba4af1fe54a3425c95de75ff3158147bb39e666cb7c6bf98cc56a9c6dc7b4ce5b2cbdae6b55d5942e50c81b76ed6b825 languageName: node linkType: hard @@ -8708,12 +8708,12 @@ __metadata: languageName: node linkType: hard -"@types/morgan@npm:1.7.37": - version: 1.7.37 - resolution: "@types/morgan@npm:1.7.37" +"@types/morgan@npm:1.9.10": + version: 1.9.10 + resolution: "@types/morgan@npm:1.9.10" dependencies: - "@types/express": "npm:*" - checksum: 10/bd9151317067c254e6e2c5c7d7c1041e6e6fa4ef886ab3fdfb707772cf3de6174fd1156d2d62131b015062e86e4838ccec5eb1d22f351dcafa87541b6a2b4a91 + "@types/node": "npm:*" + checksum: 10/67c974efe93ae4f20084acc56800671fa7258eeef0126947f4cff8559493b2f72148676ca11c7bf88ae2e71031392108984d87bce913d89aeed78b82b82aee44 languageName: node linkType: hard @@ -9117,14 +9117,14 @@ __metadata: "@types/compression": "npm:1.8.1" "@types/content-disposition": "npm:0.5.9" "@types/cookie-parser": "npm:1.4.10" - "@types/cors": "npm:2.8.6" + "@types/cors": "npm:2.8.19" "@types/eslint": "npm:9.6.1" "@types/express-session": "npm:1.19.0" "@types/http-proxy": "npm:1.17.17" "@types/jsonwebtoken": "npm:9.0.10" "@types/lodash": "npm:4.17.24" "@types/mime-types": "npm:3.0.1" - "@types/morgan": "npm:1.7.37" + "@types/morgan": "npm:1.9.10" "@types/ms": "npm:2.1.0" "@types/node": "npm:20.19.41" "@types/node-schedule": "npm:2.1.8" From ad84895e08d86f30be8987bfedaa39ed42f86bf5 Mon Sep 17 00:00:00 2001 From: prakash Date: Tue, 30 Jun 2026 21:15:28 +0530 Subject: [PATCH 6/7] remove unused dep nock --- packages/@uppy/tus/package.json | 1 - yarn.lock | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/@uppy/tus/package.json b/packages/@uppy/tus/package.json index ee84ecf11..3b9ebfa24 100644 --- a/packages/@uppy/tus/package.json +++ b/packages/@uppy/tus/package.json @@ -44,7 +44,6 @@ "devDependencies": { "@uppy/core": "workspace:^", "jsdom": "^29.1.1", - "nock": "^13.1.0", "typescript": "^5.8.3", "vitest": "^4.1.6" }, diff --git a/yarn.lock b/yarn.lock index 72d061b3a..ea441ab93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9696,7 +9696,6 @@ __metadata: "@uppy/core": "workspace:^" "@uppy/utils": "workspace:^" jsdom: "npm:^29.1.1" - nock: "npm:^13.1.0" tus-js-client: "npm:^4.3.1" typescript: "npm:^5.8.3" vitest: "npm:^4.1.6" From 942db4723426048f52a0f3585156234c9c8621e8 Mon Sep 17 00:00:00 2001 From: prakash Date: Tue, 30 Jun 2026 21:15:42 +0530 Subject: [PATCH 7/7] add changeset --- .changeset/little-clouds-call.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/little-clouds-call.md diff --git a/.changeset/little-clouds-call.md b/.changeset/little-clouds-call.md new file mode 100644 index 000000000..833cf647a --- /dev/null +++ b/.changeset/little-clouds-call.md @@ -0,0 +1,5 @@ +--- +"@uppy/tus": major +--- + +@uppy/tus: don't abort the request on error, so the server response (status + body) is forwarded to the `upload-error` event and `file.response` instead of being reset to status `0`.