Repository navigation
feat: accept in-memory upload inputs - #733
Conversation
…-inputs Conflict in types/cloudinary_ts_spec.ts: keep the new upload() type tests from this branch. Use the "UploadStream | Promise<UploadApiResponse>" order from master for upload_large.
In promise mode, call_api reports a failure to the callback and also rejects. The Blob branch then called the callback again from its .catch. Now the Blob branch calls the callback only for a failure before call_api returns. Examples of such a failure are a Blob read error or a synchronous throw. Add a unit test that sends a mocked HTTP 500 response.
Code Review
Every finding below was reproduced with a standalone Node.js script against this branch ( 1. Blob uploads read
|
| File.name | public_id | original_filename |
|---|---|---|
zdjęcie.png |
zdj_cie_… |
"zdj\u0019cie" |
日本.png |
random (use_filename lost) | (missing) |
say "hi".png |
say_… |
"say " |
name with CRLF + injected Content-Type: text/plain |
x_… |
"x" (still stored as png) |
control: string path zdjęcie.png (2.11.0 code path) |
zdj_cie_… |
"zdj\u0019cie" |
In 2.11.0, upload(path) with a non-Latin-1 name and upload_stream({filename: 'a"b\r\nX-Injected: 1'}) produce the same broken headers.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The multipart file part header interpolates the filename raw and is encoded with 'binary' (latin1). File.name, which is often user-supplied, can therefore contain quotes or CR/LF that break or inject header lines, and any non-Latin-1 characters are truncated to one byte. ## Fix Focus Areas - lib/uploader.js[650-651] - lib/uploader.js[743-762] - lib/uploader.js[774-782] ## Recommended Fix Follow the HTML multipart/form-data rules that browsers use: replace `"` with `%22`, CR with `%0D` and LF with `%0A` in the filename, and encode the header as UTF-8 instead of 'binary'. Apply this to all inputs (paths, streams, Blob/File). Add tests for a quote, CRLF, a Polish name and a CJK name, and assert `original_filename` in an integration test.
4. Byte inputs upload as "file", so raw assets lose their extension ≡ Correctness
Description
Buffer, Uint8Array and ArrayBuffer inputs (and a Blob without a name) default the multipart filename to the bare string "file". Cloudinary derives the raw asset extension and the use_filename public_id from it, so raw uploads get no extension and every asset reports original_filename: "file". A workaround exists (options.filename), but it is not typed or documented.
Code
+function toUploadSource(file, options = {}, extra = {}) { + return { + data: Buffer.isBuffer(file) ? file : Buffer.from(file), + filename: options.filename || extra.filename || "file",
Evidence
Live API:
Buffer, resource_type raw, use_filename -> public_id "file_es6iiq" original_filename "file" Buffer, resource_type raw -> public_id "wyjpsl7tbvsxnshvqn6y" (no extension) control: path report.csv, raw, use_filename -> public_id "report_quirkp.csv" original_filename "report" Buffer image -> original_filename "file"Captured request:
filename="file"for Buffer, Uint8Array and ArrayBuffer. Withoptions.filename: 'report.csv'the header carriesreport.csv, and the value is not added to the signed params.
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description In-memory uploads default the multipart filename to "file". Raw assets lose their extension, and original_filename/use_filename are meaningless. The `options.filename` override works but is undiscoverable. ## Fix Focus Areas - lib/uploader.js[160-166] - types/index.d.ts (UploadApiOptions) ## Recommended Fix Add `filename?: string` to UploadApiOptions with a doc comment, and document it in the README next to the new Buffer/Blob examples. Optionally, warn or document that `resource_type: 'raw'` byte uploads need a filename to keep their extension. Make the raw-buffer integration test assert on `public_id`/`original_filename`, not only on `resource_type`.
5. jsdom-only fallback shipped in production code 🧹 Maintainability
Description
getBlobBuffer() scans the Blob's own symbol properties for jsdom's private _buffer field. It exists only because uploader_spec loads jsdom-global, whose jsdom@15 Blob replaces global.Blob and has no arrayBuffer(). The FileReader branch after it cannot run: FileReader is undefined in Node and under the repo's jsdom-global setup.
Code
+function getBlobBuffer(file) { + ... + for (let symbol of Object.getOwnPropertySymbols(file)) { + let implementation = file[symbol]; + if (implementation && Buffer.isBuffer(implementation._buffer)) { + return implementation._buffer;
Evidence
# repo devDependencies: jsdom 15.2.1 / jsdom-global 3.0.2 global.Blob replaced by jsdom? true | blob.arrayBuffer: undefined own symbols: [ 'Symbol(impl)' ] -> keys [ '_buffer', 'type' ] FileReader defined (plain Node 24 / under jsdom-global): false / false # upload(new Blob(['jsdom-bytes'])) under jsdom-global this branch: uploaded body = "jsdom-bytes" same code, getBlobBuffer removed: FAILED -> Blob upload requires Blob.arrayBuffer() or FileReader support
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `getBlobBuffer` reads jsdom's private implementation object to support the integration test environment, and the `FileReader` fallback is unreachable in Node. Production behaviour should not depend on jsdom internals. ## Fix Focus Areas - lib/uploader.js[111-150] - test/integration/api/uploader/uploader_spec.js[55-62] - test/integration/api/uploader/uploader_spec.js[133-143] ## Recommended Fix Remove `getBlobBuffer` and the `FileReader` branch, and simply call `file.arrayBuffer()`. In the spec, construct the Blob with Node's own `require('buffer').Blob` (or keep the native `global.Blob` the way `URL` is already preserved) so the test exercises the real runtime Blob.
6. Error shape and timing depend on the input type ≡ Correctness
Description
When reading a Blob fails, the promise rejects with a bare Error, while an fs read failure rejects with {error}. Synchronous config errors such as a missing cloud_name are thrown synchronously for string and Buffer inputs but become an async rejection plus a callback call for Blob inputs.
Code
+ }).catch((error) => { + if (!apiCalled) { + callback({ + error + }); + } + throw error; + });
Evidence
missing file path (fs error) REJECTED with plain object keys=[error] Blob whose arrayBuffer() rejects REJECTED with Error instance (e.error=undefined) string path, no cloud_name THREW SYNC "Must supply cloud_name" callback calls=0 Buffer, no cloud_name THREW SYNC "Must supply cloud_name" callback calls=0 Blob, no cloud_name REJECTED "Must supply cloud_name" callback calls=1
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The Blob branch rejects with the raw Error (`throw error`), while other read failures reject with `{ error }`. Config errors are thrown synchronously for non-Blob inputs but delivered asynchronously for Blob inputs. ## Fix Focus Areas - lib/uploader.js[54-90] ## Recommended Fix Reject with `{ error }` to match the fs read-failure path. Resolve config and params synchronously before the async Blob read (this also fixes the options-mutation finding), so config errors surface the same way for every input type. Add unit tests for both cases.
7. Whole payload is copied into memory ⚡ Performance
Description
Buffer.from(uint8Array) copies the full payload, where a zero-copy view is possible. Blobs, including file-backed ones from fs.openAsBlob, are fully loaded with arrayBuffer() instead of being streamed.
Evidence
Uint8Array 200MB: arrayBuffers grew by 200MB synchronously control: Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength) grew by 0MB (zero-copy view: true) fs.openAsBlob 200MB file: peak extra arrayBuffers during upload ≈ 200MB
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description In-memory uploads double peak memory: Uint8Array is copied, and Blobs are read fully into an ArrayBuffer. ## Fix Focus Areas - lib/uploader.js[160-166] - lib/uploader.js[61-67] ## Recommended Fix For Uint8Array, use `Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength)`. For Blobs, consider piping `Readable.fromWeb(blob.stream())` into the existing upload stream path instead of materialising an ArrayBuffer.
8. UploadFile type breaks on @types/node < 18 without the DOM lib 🧩 Compatibility
Description
The new UploadFile type references the global Blob. TypeScript consumers on @types/node 14 or 16 without lib: ["dom"] get TS2304, where 2.11.0 compiled. Consumers on @types/node 18 or later are unaffected.
Evidence
tsc --strict --lib es2020 --types node --skipLibCheck false, consumer callingcloudinary.v2.uploader.upload('a.png'):this PR @types/node@14.18.63 error TS2304: Cannot find name 'Blob'. (types/index.d.ts(1288,76)) 2.11.0 @types/node@14.18.63 OK this PR @types/node@16.18.126 error TS2304: Cannot find name 'Blob'. 2.11.0 @types/node@16.18.126 OK this PR @types/node@18.19.130 OK this PR @types/node@22.20.5 OK
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description `UploadFile` references the global `Blob`, which older @types/node versions do not declare without the DOM lib. ## Fix Focus Areas - types/index.d.ts[1288] ## Recommended Fix Either accept this and document @types/node >= 18 as the minimum for TS users, or replace `Blob` with a structural type such as `{ arrayBuffer(): Promise<ArrayBuffer>; type: string; size: number; name?: string }`. The structural type also matches the shape-based runtime detection suggested for the Blob-detection finding.
9. Redundant branches in the new helpers 🧹 Maintainability
Description
Buffer.isBuffer in isUploadData is subsumed by isUint8Array (a Buffer is a Uint8Array). The typeof Uint8Array/ArrayBuffer !== "undefined" guards are dead on Node >= 9, the declared engine. The typeof Blob guard is still needed. The FileReader branch is unreachable (see finding 5). Note: the apiCalled flag is needed as written; a two-argument .then() would stop routing synchronous call_api errors to the callback.
Code
+function isUploadData(file) { + return Buffer.isBuffer(file) || isUint8Array(file) || isArrayBuffer(file); +}
Evidence
Buffer.from('x') instanceof Uint8Array: true typeof FileReader (Node v24.13.0): undefined
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The new type-detection helpers carry checks that can never change the result on supported Node versions. ## Fix Focus Areas - lib/uploader.js[99-158] ## Recommended Fix Simplify to `isUploadData = f => f instanceof Uint8Array || f instanceof ArrayBuffer`, and drop the dead `typeof` guards and the FileReader branch. Keep the `typeof Blob` guard (or switch to shape-based detection).
Checked and dismissed: "Buffer paths are now uploaded as content". In 2.11.0, upload(Buffer.from(path)) already threw ERR_INVALID_ARG_TYPE (in basename()), so no existing caller regresses.
🤖 Generated with Claude Code. Findings were verified with local repro scripts and a live staging API before posting.
Apply the review findings on in-memory upload inputs:
- Make the upload source and call call_api at once, so that a later
change to options does not change the upload. Read the Blob body in
post(), with the same error handling as the fs path. A read failure
now rejects with { error }, and config errors are thrown at once for
all input types.
- Find a Blob by its shape, so that polyfill Blobs and Blobs from other
realms also work.
- Escape quotes, CR and LF in the multipart filename and encode the
file header as UTF-8.
- Add the filename option to UploadApiOptions and the README.
- Remove the jsdom-only Blob fallback and the FileReader branch. Use
the Node Blob in the integration spec.
- Send a Uint8Array as a zero-copy view. Stream a Blob with
Readable.fromWeb, and read it with arrayBuffer() if that fails.
- Use a structural UploadBlob type in place of the global Blob.
Node versions before 15.7 (and 14.18) have no Blob in the buffer module. Skip the integration test on these versions, as the File test does.
Skip only the tests that need a native Blob. On Node before 15.7, the tests with a Blob-like object now test the arrayBuffer() fallback. Do not use assert.rejects, because Node 9 does not have it.
Summary
uploader.upload()andunsigned_upload()to accept in-memory binary inputsBuffer,Uint8Array,ArrayBuffer, andBlobdata directly in the multipart body instead of treating them like file pathsTesting
fs.createReadStreamNotes
filenameoption (raw uploads take their extension from it)