Skip to content

Comments

feat: Add support for file upload as binary data via Buffer, Readable, ReadableStream#2925

Merged
mtrezza merged 17 commits intoparse-community:alphafrom
mtrezza:feat/file-upload-stream
Feb 20, 2026
Merged

feat: Add support for file upload as binary data via Buffer, Readable, ReadableStream#2925
mtrezza merged 17 commits intoparse-community:alphafrom
mtrezza:feat/file-upload-stream

Conversation

@mtrezza
Copy link
Member

@mtrezza mtrezza commented Feb 20, 2026

Pull Request

Issue

Add support for file upload via Buffer, Readable, ReadableStream.

Summary by CodeRabbit

  • New Features

    • Optional controller API to upload raw binary data; direct binary upload support for Buffer and ReadableStream inputs with automatic routing in save flows.
  • Behavior Changes

    • Streams with metadata/tags are disallowed; buffers with metadata/tags fall back to base64 uploads; fallback to base64 if binary upload not supported.
    • Streaming request bodies use half‑duplex where applicable and are excluded from retry logic.
  • Documentation / Types

    • Public types updated to include buffer and stream input formats.
  • Tests

    • Extensive tests covering buffer/stream uploads, headers, auth, fallbacks, and error cases.

@parse-github-assistant
Copy link

parse-github-assistant bot commented Feb 20, 2026

🚀 Thanks for opening this pull request!

@parseplatformorg
Copy link
Contributor

parseplatformorg commented Feb 20, 2026

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

Adds Buffer and Stream file sources, an optional FileController.saveBinary API, DefaultController binary upload handling, ReadableStream support in RESTController, type updates, and extensive unit/integration tests for buffer/stream save/getData and auth/header behaviors.

Changes

Cohort / File(s) Summary
Core manager & types
src/CoreManager.ts, types/CoreManager.d.ts
Added optional saveBinary?(name: string, source: FileSource, options?: FileSaveOptions) => Promise<{ name: string; url: string }> to the FileController interface.
File source types
types/ParseFile.d.ts
Extended FileSource union to include { format: 'buffer'; buffer: any; type?: string } and { format: 'stream'; stream: any; type?: string }.
ParseFile implementation
src/ParseFile.ts
Constructor and getData accept Buffer and Stream sources; save flow uses saveBinary for raw buffer/stream uploads when available, falls back to saveBase64 for buffer-with-metadata/tags or when saveBinary missing, and errors on streams with metadata/tags. Added saveBinary to DefaultController to POST binary bodies with proper headers and auth handling.
REST request streaming
src/RESTController.ts
When request body is a ReadableStream, set fetchOptions.duplex = 'half' and skip retry logic for ReadableStream bodies to avoid retrying streamed uploads.
Unit tests
src/__tests__/ParseFile-test.js, src/__tests__/RESTController-test.js
Added comprehensive tests for Buffer, Node Readable, and Web ReadableStream sources, save/getData flows, saveBinary header/auth handling, fallback/error cases, and RESTController ReadableStream behavior (duplex and retry).
Integration tests
integration/test/ParseFileTest.js
Added integration tests for saving Parse.File from Buffer and Readable streams, including content-type variants and getData verification.

Sequence Diagram

sequenceDiagram
    participant Client as ParseFile Client
    participant FileCtrl as FileController / DefaultController
    participant REST as RESTController
    participant Server as Parse Server

    Client->>FileCtrl: save(name, source{buffer|stream}, options)
    FileCtrl->>REST: prepare upload (decide saveBinary vs saveBase64)
    alt source.format == 'stream' and has metadata/tags
        FileCtrl-->>Client: Error("cannot save stream with metadata/tags")
    else source in {'buffer','stream'} and no metadata/tags
        alt FileCtrl.saveBinary exists
            REST->>Server: HTTP POST (raw body, Content-Type, auth headers, duplex='half' for ReadableStream)
            Server-->>REST: { name, url }
        else source == 'buffer'
            REST->>Server: HTTP POST (JSON with base64, metadata)
            Server-->>REST: { name, url }
        else
            FileCtrl-->>Client: Error("saveBinary required for stream uploads")
        end
    end
    REST-->>FileCtrl: { name, url }
    FileCtrl-->>Client: return { name, url }
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description is incomplete, missing the 'Approach' section detailing the implementation changes and lacks explicit task completion status. Add the 'Approach' section describing how binary file uploads were implemented, and mark or document which tasks were completed (tests, documentation).
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature addition: support for file uploads from binary sources (Buffer, Readable, ReadableStream).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (6)
src/__tests__/ParseFile-test.js (2)

601-611: Consider using expect.assertions(1) for expected-throw tests.

Multiple tests use the expect(true).toBe(false) anti-pattern to fail if no error is thrown (lines 607, 1048, 1063, 1105, 1158). If the async function resolves unexpectedly, the test could pass silently if the catch block has a bug. Using expect.assertions(n) or await expect(...).rejects.toThrow(...) is more idiomatic Jest and provides a clearer failure message.

♻️ Example using rejects.toThrow
 it('getData from stream-backed unsaved file throws', async () => {
   const { Readable } = require('stream');
   const stream = new Readable({ read() { this.push(null); } });
   const file = new ParseFile('parse.txt', stream, 'text/plain');
-  try {
-    await file.getData();
-    expect(true).toBe(false);
-  } catch (e) {
-    expect(e.message).toBe('Cannot retrieve data for unsaved ParseFile.');
-  }
+  await expect(file.getData()).rejects.toThrow('Cannot retrieve data for unsaved ParseFile.');
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/ParseFile-test.js` around lines 601 - 611, The test for
ParseFile.getData uses the anti-pattern expect(true).toBe(false) to assert a
throw; update the test (and the other similar tests referencing ParseFile usage)
to use Jest's idiomatic assertions by either calling expect.assertions(1) at the
top of the test or converting the try/catch to await
expect(file.getData()).rejects.toThrow('Cannot retrieve data for unsaved
ParseFile.'); target the test that constructs new ParseFile('parse.txt', stream,
'text/plain') and any other occurrences noted (the tests around the getData
calls) and replace the fail-on-success pattern with one of these approaches.

886-966: Missing test: saving a Web ReadableStream (non-Node.js stream) via saveBinary.

The stream save test at line 932 uses a Node.js Readable, which exercises the Readable.toWeb() conversion path (line 626-627 of ParseFile.ts). There's no test for passing a Web ReadableStream directly to saveBinary, which would exercise the else branch at line 634 of ParseFile.ts where the stream is used as-is.

Consider adding a test that creates a new ReadableStream(...) (from stream/web) and verifies it's passed directly to ajax without conversion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/ParseFile-test.js` around lines 886 - 966, Add a test that
covers passing a Web ReadableStream directly into ParseFile.save/saveBinary so
the code path that uses the stream as-is is exercised: create a Web
ReadableStream (imported from 'stream/web' or global ReadableStream), pass it
into new ParseFile('parse.txt', webStream, 'text/plain'), set up CoreManager
RESTController.ajax mock as in other tests, call file.save(), and assert ajax
was called with the request body equal to that Web ReadableStream (or at least
that ajax received the same object reference) and headers include the correct
Content-Type and X-Parse-Application-ID; this will exercise the else branch in
ParseFile.saveBinary that does not call Readable.toWeb().
src/ParseFile.ts (3)

574-648: saveBinary bypasses RESTController.request() — duplicated auth/header logic.

saveBinary manually constructs auth headers (Application ID, JS Key, Master Key, Session Token) and calls ajax() directly, duplicating logic from RESTController.request(). This means saveBinary misses:

  • Installation ID header
  • FORCE_REVOCABLE_SESSION flag
  • Custom REQUEST_HEADERS (though ajax adds these internally at line 149 of RESTController.ts)
  • IDEMPOTENCY request ID

The existing deleteFile method follows the same ajax-direct pattern, so this is at least consistent with the codebase. However, it's worth considering whether the binary upload should also include installation ID and idempotency headers for parity with saveBase64 (which goes through request()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 574 - 648, saveBinary constructs auth headers
and calls CoreManager.getRESTController().ajax() directly, duplicating logic and
omitting headers added by RESTController.request() (e.g., installation ID,
FORCE_REVOCABLE_SESSION, REQUEST_HEADERS, IDEMPOTENCY); update saveBinary to
delegate to RESTController.request() like saveBase64 (or, if ajax must be used,
replicate all header injection performed by RESTController.request()), ensuring
you include installation ID, FORCE_REVOCABLE_SESSION flag, any CoreManager
REQUEST_HEADERS, and an idempotency request ID; reference the saveBinary
function and CoreManager.getRESTController().request()/ajax(), and mirror how
deleteFile or saveBase64 handle auth/session headers to keep behavior
consistent.

619-637: require('stream') will be included in browser bundles.

Line 625 uses require('stream') inside a conditional block. While the runtime guard (typeof stream.pipe === 'function') prevents execution in browsers, bundlers (Webpack, Rollup, etc.) will still include the stream module in the output unless it's externalized. This inflates bundle size for browser consumers.

Consider using a dynamic import() or documenting that bundler configs should externalize stream for browser targets. This is consistent with pre-existing patterns in the SDK but worth noting for bundle-conscious users.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 619 - 637, The current use of
require('stream') inside the guarded block causes bundlers to include the Node
'stream' module; replace the synchronous require with a dynamic import to keep
the module out of browser bundles: change the code in ParseFile (the branch
handling source.format === 'stream') to use await import('stream') (or
import('stream').then(...)) to obtain Readable and then check typeof
Readable.toWeb === 'function' before calling Readable.toWeb(stream); update the
containing function to be async (or handle the returned promise) so the import
can be awaited, and keep the existing fallback/error behavior if Readable.toWeb
is unavailable.

639-647: URL construction duplicates deleteFile — consider extracting a helper.

Lines 639-643 repeat the same SERVER_URL + trailing slash + 'files/' + name pattern found in deleteFile (lines 703-707). A small helper like getFileUrl(name) would reduce duplication.

♻️ Suggested helper extraction
+function getFileUrl(name: string): string {
+  let url = CoreManager.get('SERVER_URL');
+  if (url[url.length - 1] !== '/') {
+    url += '/';
+  }
+  return url + 'files/' + name;
+}
+
 const DefaultController = {
   // ...
   saveBinary: async function (...) {
     // ...
-    let url = CoreManager.get('SERVER_URL');
-    if (url[url.length - 1] !== '/') {
-      url += '/';
-    }
-    url += 'files/' + name;
+    const url = getFileUrl(name);
     // ...
   },
   // ...
   deleteFile: function (...) {
     // ...
-    let url = CoreManager.get('SERVER_URL');
-    if (url[url.length - 1] !== '/') {
-      url += '/';
-    }
-    url += 'files/' + name;
+    const url = getFileUrl(name);
     // ...
   },
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 639 - 647, The URL construction for file
endpoints is duplicated in this file (the block building url from
CoreManager.get('SERVER_URL') and appending 'files/' + name appears in this
method and in deleteFile); extract a small helper function getFileUrl(name)
inside ParseFile.ts that reads CoreManager.get('SERVER_URL'), ensures a trailing
'/', and returns serverUrl + 'files/' + name, then replace the inline
construction in both the current method and deleteFile with calls to
getFileUrl(name) to remove duplication and keep behavior identical.
types/ParseFile.d.ts (1)

25-32: Consider using more specific types for buffer and stream instead of any.

Using any for buffer and stream loses type safety. Even in a cross-environment declaration file, you could narrow these to Buffer (or { toString(encoding: string): string }) and ReadableStream | NodeJS.ReadableStream respectively, behind conditional/union types. This would give consumers better autocompletion and catch misuse at compile time.

That said, this is a pragmatic choice given the cross-environment constraints.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/ParseFile.d.ts` around lines 25 - 32, The union branch for ParseFile
that uses format:'buffer' and format:'stream' currently types properties buffer
and stream as any; change them to more specific types to restore type safety:
for the buffer branch replace buffer:any with a narrower type such as Buffer | {
toString(encoding?: string): string } (or a union that covers both browser and
Node), and for the stream branch replace stream:any with ReadableStream |
NodeJS.ReadableStream (or a union that covers both environments); update the
ParseFile union branches (format: 'buffer' / format: 'stream') accordingly so
consumers get proper autocompletion and compile-time checks while keeping
cross-environment compatibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/__tests__/ParseFile-test.js`:
- Around line 601-611: The test for ParseFile.getData uses the anti-pattern
expect(true).toBe(false) to assert a throw; update the test (and the other
similar tests referencing ParseFile usage) to use Jest's idiomatic assertions by
either calling expect.assertions(1) at the top of the test or converting the
try/catch to await expect(file.getData()).rejects.toThrow('Cannot retrieve data
for unsaved ParseFile.'); target the test that constructs new
ParseFile('parse.txt', stream, 'text/plain') and any other occurrences noted
(the tests around the getData calls) and replace the fail-on-success pattern
with one of these approaches.
- Around line 886-966: Add a test that covers passing a Web ReadableStream
directly into ParseFile.save/saveBinary so the code path that uses the stream
as-is is exercised: create a Web ReadableStream (imported from 'stream/web' or
global ReadableStream), pass it into new ParseFile('parse.txt', webStream,
'text/plain'), set up CoreManager RESTController.ajax mock as in other tests,
call file.save(), and assert ajax was called with the request body equal to that
Web ReadableStream (or at least that ajax received the same object reference)
and headers include the correct Content-Type and X-Parse-Application-ID; this
will exercise the else branch in ParseFile.saveBinary that does not call
Readable.toWeb().

In `@src/ParseFile.ts`:
- Around line 574-648: saveBinary constructs auth headers and calls
CoreManager.getRESTController().ajax() directly, duplicating logic and omitting
headers added by RESTController.request() (e.g., installation ID,
FORCE_REVOCABLE_SESSION, REQUEST_HEADERS, IDEMPOTENCY); update saveBinary to
delegate to RESTController.request() like saveBase64 (or, if ajax must be used,
replicate all header injection performed by RESTController.request()), ensuring
you include installation ID, FORCE_REVOCABLE_SESSION flag, any CoreManager
REQUEST_HEADERS, and an idempotency request ID; reference the saveBinary
function and CoreManager.getRESTController().request()/ajax(), and mirror how
deleteFile or saveBase64 handle auth/session headers to keep behavior
consistent.
- Around line 619-637: The current use of require('stream') inside the guarded
block causes bundlers to include the Node 'stream' module; replace the
synchronous require with a dynamic import to keep the module out of browser
bundles: change the code in ParseFile (the branch handling source.format ===
'stream') to use await import('stream') (or import('stream').then(...)) to
obtain Readable and then check typeof Readable.toWeb === 'function' before
calling Readable.toWeb(stream); update the containing function to be async (or
handle the returned promise) so the import can be awaited, and keep the existing
fallback/error behavior if Readable.toWeb is unavailable.
- Around line 639-647: The URL construction for file endpoints is duplicated in
this file (the block building url from CoreManager.get('SERVER_URL') and
appending 'files/' + name appears in this method and in deleteFile); extract a
small helper function getFileUrl(name) inside ParseFile.ts that reads
CoreManager.get('SERVER_URL'), ensures a trailing '/', and returns serverUrl +
'files/' + name, then replace the inline construction in both the current method
and deleteFile with calls to getFileUrl(name) to remove duplication and keep
behavior identical.

In `@types/ParseFile.d.ts`:
- Around line 25-32: The union branch for ParseFile that uses format:'buffer'
and format:'stream' currently types properties buffer and stream as any; change
them to more specific types to restore type safety: for the buffer branch
replace buffer:any with a narrower type such as Buffer | { toString(encoding?:
string): string } (or a union that covers both browser and Node), and for the
stream branch replace stream:any with ReadableStream | NodeJS.ReadableStream (or
a union that covers both environments); update the ParseFile union branches
(format: 'buffer' / format: 'stream') accordingly so consumers get proper
autocompletion and compile-time checks while keeping cross-environment
compatibility.

@codecov
Copy link

codecov bot commented Feb 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (a46908c) to head (34f001d).
⚠️ Report is 7 commits behind head on alpha.

Additional details and impacted files
@@            Coverage Diff            @@
##             alpha     #2925   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           64        64           
  Lines         6235      6318   +83     
  Branches      1493      1529   +36     
=========================================
+ Hits          6235      6318   +83     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/__tests__/ParseFile-test.js (2)

605-615: Optional: guard error-path tests with expect.assertions(n).

The expect(true).toBe(false) sentinel is used without expect.assertions(1), so if getData() resolves instead of rejects (e.g., after a future refactor), the test passes vacuously — only the unreachable sentinel fires, while the catch block (and the message check) is silently skipped.

🛠️ Proposed fix
  it('getData from stream-backed unsaved file throws', async () => {
    const { Readable } = require('stream');
    const stream = new Readable({ read() { this.push(null); } });
    const file = new ParseFile('parse.txt', stream, 'text/plain');
+   expect.assertions(1);
    try {
      await file.getData();
-     expect(true).toBe(false);
    } catch (e) {
      expect(e.message).toBe('Cannot retrieve data for unsaved ParseFile.');
    }
  });

The same pattern appears in multiple new error-path tests (lines 1065–1070, 1107–1113, 1160–1166). Applying expect.assertions(1) consistently there too would remove the sentinel lines and make failures unambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/ParseFile-test.js` around lines 605 - 615, The error-path test
for ParseFile.getData uses a sentinel (expect(true).toBe(false)) which can mask
a missing rejection; replace the sentinel by adding expect.assertions(1) at the
start of the test and keep the existing try/catch assertion that checks
e.message === 'Cannot retrieve data for unsaved ParseFile.'; apply the same
change (add expect.assertions(1) and remove sentinels) to the other error-path
tests that use the sentinel (the tests around getData/error checks at the other
indicated locations).

993-1021: UserController override not restored — minor test isolation leak.

CoreManager.set('UserController', { currentUserAsync: … }) at line 994 is never reverted. Subsequent tests within the FileController describe block will execute saveBinary with an active currentUserAsync that injects 'currentUserToken' as a session token header, which is unintended side-state. This is consistent with the existing pattern at lines 667–705, but worth noting as each test should be self-contained.

🛠️ Proposed fix
  it('saveBinary includes session token from currentUserAsync', async () => {
+   const originalUserController = CoreManager.get('UserController');
    CoreManager.set('UserController', {
      currentUserAsync() {
        return Promise.resolve({ getSessionToken() { return 'currentUserToken'; } });
      },
    });
    ...
    expect(ajax).toHaveBeenCalledWith(...);
+   CoreManager.set('UserController', originalUserController);
  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/ParseFile-test.js` around lines 993 - 1021, The test overrides
CoreManager's UserController via CoreManager.set('UserController', {
currentUserAsync() { ... } }) and never restores it, leaking state; wrap the
override so you save the original controller (const original =
CoreManager.get('UserController')) before calling CoreManager.set and restore it
after the async test (in a finally block or using afterEach) by calling
CoreManager.set('UserController', original) so subsequent tests won't see the
stubbed currentUserAsync/session token.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/ParseFile-test.js`:
- Around line 890-919: The buffer-backed test "saves buffer-backed files via
saveBinary" leaves CoreManager state dirty by setting
CoreManager.set('JAVASCRIPT_KEY','testJsKey') without cleanup; update that test
to restore/clear the key after the test (e.g., call
CoreManager.set('JAVASCRIPT_KEY', null) or use an afterEach) so it doesn't leak
into other tests, and in the master-key test that asserts removal of the header,
explicitly set CoreManager.set('JAVASCRIPT_KEY','someKey') at the start of that
test so the assertion on headers
(expect(...).not.toHaveProperty('X-Parse-JavaScript-Key')) actually verifies
deletion rather than passing vacuously.
- Around line 430-448: The Web ReadableStream test for ParseFile is missing the
assertion that _data is undefined; update the test within the ParseFile 'can
create files with a Web ReadableStream' case to include the same assertion as
the Node Readable stream test by adding an assertion that file._data is
undefined so both stream code paths behave identically (refer to ParseFile usage
and the test block that constructs the Web ReadableStream and inspects
file._source).

---

Nitpick comments:
In `@src/__tests__/ParseFile-test.js`:
- Around line 605-615: The error-path test for ParseFile.getData uses a sentinel
(expect(true).toBe(false)) which can mask a missing rejection; replace the
sentinel by adding expect.assertions(1) at the start of the test and keep the
existing try/catch assertion that checks e.message === 'Cannot retrieve data for
unsaved ParseFile.'; apply the same change (add expect.assertions(1) and remove
sentinels) to the other error-path tests that use the sentinel (the tests around
getData/error checks at the other indicated locations).
- Around line 993-1021: The test overrides CoreManager's UserController via
CoreManager.set('UserController', { currentUserAsync() { ... } }) and never
restores it, leaking state; wrap the override so you save the original
controller (const original = CoreManager.get('UserController')) before calling
CoreManager.set and restore it after the async test (in a finally block or using
afterEach) by calling CoreManager.set('UserController', original) so subsequent
tests won't see the stubbed currentUserAsync/session token.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ParseFile.ts (1)

17-17: ⚠️ Potential issue | 🟠 Major

FileData type missing Buffer and ReadableStream — TypeScript callers cannot pass these types without casting.

The FileData union at line 17 is number[] | Base64 | Blob | Uri, and this definition is repeated in types/ParseFile.d.ts. Neither Buffer nor ReadableStream is included. The constructor parameter at line 114 uses data?: FileData, so TypeScript will reject these types at compile time, even though the runtime detection logic at lines 122–144 handles them correctly via duck-typing and instanceof checks.

Additionally, FileSource in the declaration file includes format: 'buffer' and format: 'stream' variants, confirming the internal support for these types, but the public input type FileData was not updated accordingly.

Suggested fix

Update the FileData type in both src/ParseFile.ts (line 17) and types/ParseFile.d.ts (line 8):

-type FileData = number[] | Base64 | Blob | Uri;
+type FileData = number[] | Base64 | Blob | Uri | Buffer | ReadableStream;

Note: For stricter typing, consider using ReadableStream<Uint8Array> for the Web API variant and a Node.js-specific type for the Node stream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` at line 17, The FileData union in ParseFile.ts (type
FileData) and its duplicate in types/ParseFile.d.ts is missing Buffer and
readable-stream types, causing TypeScript callers to need casts even though
ParseFile constructor (data?: FileData) and runtime checks
(duck-typing/instanceof in the constructor logic) support them; update the
FileData union to include Node Buffer and stream types (e.g., Buffer and the
appropriate Node Readable type) and also include the Web ReadableStream variant
(e.g., ReadableStream<Uint8Array>) in both the source file and the declaration
file so TypeScript callers can pass Buffer and streams without casting, keeping
the constructor signature (data?: FileData) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ParseFile.ts`:
- Around line 311-319: The save() implementation throws synchronously when
this._source.format === 'stream' (two places checking hasMetadataOrTags and
controller.saveBinary); change those synchronous throws to return rejected
Promises so callers using promise .catch() will receive the error—either convert
save() to async and keep throws, or replace each throw new Error(...) with
returning Promise.reject(new Error(...)); update the checks in the save()
function where the stream-format errors are raised (the two blocks referencing
hasMetadataOrTags and controller.saveBinary) accordingly.
- Line 329: The property _data on the ParseFile class is typed as string |
undefined but is being assigned null; change those assignments to undefined to
match the declared type—update the assignment where you currently set this._data
= null and the similar assignment in the saveFile path to this._data =
undefined; ensure no other code relies on null checks (convert any `=== null`
checks to `=== undefined` or use == null if intentionally handling both).
- Around line 624-651: When the file upload code converts a Node Readable to a
Web ReadableStream in ParseFile (body = NodeReadable.toWeb(stream)), the
RESTController fetch call must pass duplex: 'half' for streaming request bodies;
update the fetch invocation in RESTController.ajax (the method that uses
options.requestTask/AbortController and performs the fetch) to detect if the
request body is a ReadableStream (e.g., using body instanceof
globalThis.ReadableStream or checking typeof body.getReader === 'function') and,
if so, include duplex: 'half' in the fetch init options before calling fetch.

---

Outside diff comments:
In `@src/ParseFile.ts`:
- Line 17: The FileData union in ParseFile.ts (type FileData) and its duplicate
in types/ParseFile.d.ts is missing Buffer and readable-stream types, causing
TypeScript callers to need casts even though ParseFile constructor (data?:
FileData) and runtime checks (duck-typing/instanceof in the constructor logic)
support them; update the FileData union to include Node Buffer and stream types
(e.g., Buffer and the appropriate Node Readable type) and also include the Web
ReadableStream variant (e.g., ReadableStream<Uint8Array>) in both the source
file and the declaration file so TypeScript callers can pass Buffer and streams
without casting, keeping the constructor signature (data?: FileData) unchanged.

@mtrezza mtrezza changed the title feat: Add support for file upload via Buffer, Readable, ReadableStream feat: Add support for file upload as binary data via Buffer, Readable, ReadableStream Feb 20, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/ParseFile.ts (1)

17-17: ⚠️ Potential issue | 🟠 Major

FileData type doesn't include Buffer or ReadableStream — TypeScript callers will get type errors.

The constructor now accepts Buffer and ReadableStream at runtime, but FileData (used as the data parameter type) is still:

type FileData = number[] | Base64 | Blob | Uri;

A TypeScript caller doing new ParseFile('name', Buffer.from([...])) gets:

Argument of type 'Buffer' is not assignable to parameter of type 'FileData | undefined'.

The same problem applies to NodeJS.Readable and ReadableStream. The same gap exists in types/ParseFile.d.ts Line 8.

🐛 Proposed fix
-type FileData = number[] | Base64 | Blob | Uri;
+type FileData = number[] | Base64 | Blob | Uri | Buffer | ReadableStream | NodeJS.ReadableStream;

Apply the same change to types/ParseFile.d.ts Line 8.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` at line 17, The FileData union type is missing Buffer and
Readable/ReadableStream variants so TypeScript callers get type errors when
passing Buffer or streams to the ParseFile constructor; update the FileData type
definition (the type alias named FileData in src/ParseFile.ts and the parallel
declaration in types/ParseFile.d.ts) to include Buffer and NodeJS.Readable |
ReadableStream (or the appropriate stream type used elsewhere) in the union so
the constructor's data parameter accepts Buffer and streams at compile time as
it does at runtime.
🧹 Nitpick comments (1)
types/ParseFile.d.ts (1)

25-33: buffer: any and stream: any could use stricter types.

Using any sacrifices type safety for these fields. Consider narrower types such as Buffer | ArrayBuffer | Uint8Array for buffer and ReadableStream | NodeJS.ReadableStream (or an intersection) for stream, consistent with the actual runtime checks in the constructor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@types/ParseFile.d.ts` around lines 25 - 33, The union variant with format:
'buffer' and the variant with format: 'stream' use overly broad any types for
the buffer and stream fields; change the buffer field in the { format: 'buffer'
} variant from any to a stricter union such as Buffer | ArrayBuffer | Uint8Array
and change the stream field in the { format: 'stream' } variant from any to a
stream type such as ReadableStream | NodeJS.ReadableStream (or an appropriate
intersection) so the declaration matches the runtime constructor checks and
improves type safety for the ParseFile type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/__tests__/ParseFile-test.js`:
- Around line 995-1023: The UserController mock set via
CoreManager.set('UserController', { currentUserAsync() { ... } }) in the
"saveBinary includes session token from currentUserAsync" test leaks into the
next master-key test; save the original controller before overriding (e.g.,
const originalUserController = CoreManager.get('UserController')) and restore it
after the test (in a finally block or afterEach) so ParseFile.save / saveBinary
tests do not inherit the mocked currentUserAsync; ensure the restore uses
CoreManager.set('UserController', originalUserController) and that the cleanup
runs even if the test fails.

---

Outside diff comments:
In `@src/ParseFile.ts`:
- Line 17: The FileData union type is missing Buffer and Readable/ReadableStream
variants so TypeScript callers get type errors when passing Buffer or streams to
the ParseFile constructor; update the FileData type definition (the type alias
named FileData in src/ParseFile.ts and the parallel declaration in
types/ParseFile.d.ts) to include Buffer and NodeJS.Readable | ReadableStream (or
the appropriate stream type used elsewhere) in the union so the constructor's
data parameter accepts Buffer and streams at compile time as it does at runtime.

---

Duplicate comments:
In `@src/ParseFile.ts`:
- Around line 316-324: The synchronous throws inside ParseFile.save are breaking
the promise contract; replace both throw statements (the checks using
this._source.format === 'stream' && hasMetadataOrTags and this._source.format
=== 'stream' && !controller.saveBinary) with returning rejected promises (e.g.,
return Promise.reject(new Error(...))) so errors are delivered through the
promise chain instead of thrown synchronously; update the save() method in the
ParseFile class to return Promise.reject(...) for those two error cases.
- Line 334: The assignment this._data = null violates the declared type
(this._data: string | undefined) in the ParseFile class; change the assignment
to this._data = undefined so it matches the type used elsewhere in the class
(look for other uses of _data and methods like parse()/reset() that rely on
undefined) and update any related null checks to check for undefined if
necessary.
- Around line 629-656: The streaming upload path currently converts Node streams
via NodeReadable.toWeb and passes the resulting ReadableStream into
RESTController.ajax without setting duplex:'half' and without handling retries;
update RESTController.ajax so that when the request body is a ReadableStream
(detect via isReadableStream or instanceof check) you (1) include
RequestInit.duplex = 'half' when using fetch with a Web ReadableStream body (so
Node.js/undici accepts the body) and (2) disable automatic retry logic for that
request body (i.e., do not re-run the captured dispatch closure that would reuse
a consumed ReadableStream) so that 5xx responses propagate instead of causing
"Body is unusable" errors; ensure calls originating from ParseFile (where
NodeReadable.toWeb is used) will therefore send duplex:'half' and skip retries
for stream bodies.

---

Nitpick comments:
In `@types/ParseFile.d.ts`:
- Around line 25-33: The union variant with format: 'buffer' and the variant
with format: 'stream' use overly broad any types for the buffer and stream
fields; change the buffer field in the { format: 'buffer' } variant from any to
a stricter union such as Buffer | ArrayBuffer | Uint8Array and change the stream
field in the { format: 'stream' } variant from any to a stream type such as
ReadableStream | NodeJS.ReadableStream (or an appropriate intersection) so the
declaration matches the runtime constructor checks and improves type safety for
the ParseFile type.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/RESTController.ts`:
- Around line 160-164: The retry logic is reusing the same ReadableStream body
which becomes locked after the first fetch and causes a misleading TypeError on
retry; update dispatch (which uses fetchOptions and fetchOptions.body) to detect
when fetchOptions.body is a ReadableStream (typeof ReadableStream !==
'undefined' && fetchOptions.body instanceof ReadableStream) and in that case
disable/skip the retry path on 5XX / status-0 responses so the original server
error/response is propagated instead of attempting to re-invoke dispatch with
the same stream; ensure the fetch still sets fetchOptions.duplex = 'half' for
stream bodies but do not enqueue any retry attempts when the body is a
ReadableStream.

@mtrezza
Copy link
Member Author

mtrezza commented Feb 20, 2026

@coderabbitai resume

@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

✅ Actions performed

Reviews resumed.

@mtrezza mtrezza merged commit e42caf6 into parse-community:alpha Feb 20, 2026
13 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ParseFile.ts`:
- Around line 127-149: The FileData union type is missing binary and stream
variants that the ParseFile constructor handles (Uint8Array, Buffer,
NodeJS.ReadableStream, ReadableStream); update the FileData type definition used
by ParseFile to include Uint8Array, Buffer, NodeJS.ReadableStream, and
ReadableStream so TypeScript callers match runtime behavior. Modify both the
runtime source type (in src/ParseFile.ts where the ParseFile constructor and
this._source handling exist) and the declaration file (types/ParseFile.d.ts
where FileData is declared) to add these union members, keeping existing members
(number[] | Base64 | Blob | Uri) intact. Ensure imports/ambient types for
NodeJS.ReadableStream and global ReadableStream are referenced or declared so
the types compile in both Node and browser environments.

Comment on lines +127 to +149
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) {
this._source = {
format: 'buffer',
buffer: data,
type: specifiedType,
};
} else if (
data !== null &&
typeof data === 'object' &&
typeof (data as any).pipe === 'function' &&
typeof (data as any).read === 'function'
) {
this._source = {
format: 'stream',
stream: data,
type: specifiedType,
};
} else if (typeof ReadableStream !== 'undefined' && data instanceof ReadableStream) {
this._source = {
format: 'stream',
stream: data,
type: specifiedType,
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the FileData type definition in src/ParseFile.ts
rg "type FileData" src/ParseFile.ts -A 1

# Find the constructor signature in types/ParseFile.d.ts
fd -e d.ts "ParseFile" | head -5

Repository: parse-community/Parse-SDK-JS

Length of output: 186


🏁 Script executed:

#!/bin/bash
# Show constructor and FileData context in types/ParseFile.d.ts
find . -name "ParseFile.d.ts" -type f | head -1

Repository: parse-community/Parse-SDK-JS

Length of output: 94


🏁 Script executed:

#!/bin/bash
# Search for ParseFile.d.ts location and examine it
fd "ParseFile.d.ts" --type f

Repository: parse-community/Parse-SDK-JS

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Read the types/ParseFile.d.ts file to see constructor and FileData definition
cat types/ParseFile.d.ts | head -100

Repository: parse-community/Parse-SDK-JS

Length of output: 3817


🏁 Script executed:

#!/bin/bash
# Check src/ParseFile.ts for FileData type definition context
rg "type FileData" src/ParseFile.ts -B 2 -A 2

Repository: parse-community/Parse-SDK-JS

Length of output: 216


🏁 Script executed:

#!/bin/bash
# Verify constructor parameter handling in src/ParseFile.ts
sed -n '116,150p' src/ParseFile.ts

Repository: parse-community/Parse-SDK-JS

Length of output: 1275


🏁 Script executed:

#!/bin/bash
# Check package.json for Node.js version requirements
cat package.json | grep -A 10 '"engines"'

Repository: parse-community/Parse-SDK-JS

Length of output: 326


🏁 Script executed:

#!/bin/bash
# Check the save() method implementation at lines 311-353 for synchronous throws
sed -n '311,353p' src/ParseFile.ts

Repository: parse-community/Parse-SDK-JS

Length of output: 1782


🏁 Script executed:

#!/bin/bash
# Check line 334 for _data = null pattern
sed -n '330,345p' src/ParseFile.ts

Repository: parse-community/Parse-SDK-JS

Length of output: 712


🏁 Script executed:

#!/bin/bash
# Verify what types are actually accepted by checking the full constructor implementation
sed -n '116,200p' src/ParseFile.ts | grep -A 5 "constructor\|FileData"

Repository: parse-community/Parse-SDK-JS

Length of output: 295


🏁 Script executed:

#!/bin/bash
# Check if there are overloaded constructor signatures or any type guards
rg "constructor.*FileData|Buffer|Readable" src/ParseFile.ts types/ParseFile.d.ts

Repository: parse-community/Parse-SDK-JS

Length of output: 1737


🏁 Script executed:

#!/bin/bash
# Get more context around NodeReadable.toWeb usage to verify Node.js version requirement
rg "NodeReadable.toWeb" src/ParseFile.ts -B 3 -A 3

Repository: parse-community/Parse-SDK-JS

Length of output: 325


🏁 Script executed:

#!/bin/bash
# Check the saveBinary implementation to see if NodeReadable.toWeb is guarded
sed -n '584,650p' src/ParseFile.ts

Repository: parse-community/Parse-SDK-JS

Length of output: 2223


🏁 Script executed:

#!/bin/bash
# Verify if there's any Uint8Array handling that should be in FileData
rg "Uint8Array" src/ParseFile.ts types/ParseFile.d.ts -B 1 -A 1

Repository: parse-community/Parse-SDK-JS

Length of output: 1206


Update FileData type to include Uint8Array, Buffer, and stream types.

The constructor accepts Buffer, Uint8Array, NodeJS.ReadableStream, and ReadableStream, but the FileData type definition (number[] | Base64 | Blob | Uri) excludes them. TypeScript users passing these types will get compilation errors, despite the documentation and runtime implementation supporting them.

Both src/ParseFile.ts and types/ParseFile.d.ts need the type updated:

📝 Proposed fix
-type FileData = number[] | Base64 | Blob | Uri;
+type FileData = number[] | Uint8Array | Base64 | Blob | Uri | Buffer | NodeJS.ReadableStream | ReadableStream;

Apply the same update to both files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ParseFile.ts` around lines 127 - 149, The FileData union type is missing
binary and stream variants that the ParseFile constructor handles (Uint8Array,
Buffer, NodeJS.ReadableStream, ReadableStream); update the FileData type
definition used by ParseFile to include Uint8Array, Buffer,
NodeJS.ReadableStream, and ReadableStream so TypeScript callers match runtime
behavior. Modify both the runtime source type (in src/ParseFile.ts where the
ParseFile constructor and this._source handling exist) and the declaration file
(types/ParseFile.d.ts where FileData is declared) to add these union members,
keeping existing members (number[] | Base64 | Blob | Uri) intact. Ensure
imports/ambient types for NodeJS.ReadableStream and global ReadableStream are
referenced or declared so the types compile in both Node and browser
environments.

@mtrezza mtrezza deleted the feat/file-upload-stream branch February 20, 2026 14:31
parseplatformorg pushed a commit that referenced this pull request Feb 20, 2026
# [8.2.0-alpha.1](8.1.1-alpha.6...8.2.0-alpha.1) (2026-02-20)

### Features

* Add support for file upload as binary data via `Buffer`, `Readable`, `ReadableStream` ([#2925](#2925)) ([e42caf6](e42caf6))
@parseplatformorg
Copy link
Contributor

🎉 This change has been released in version 8.2.0-alpha.1

@parseplatformorg parseplatformorg added the state:released-alpha Released as alpha version label Feb 20, 2026
parseplatformorg pushed a commit that referenced this pull request Feb 20, 2026
# [8.2.0](8.1.0...8.2.0) (2026-02-20)

### Bug Fixes

* `Parse.Object.createWithoutData` doesn't preserve object subclass ([#2907](#2907)) ([01dc94d](01dc94d))
* `Parse.Query.and/or/nor` loosing custom class types ([#2903](#2903)) ([89fdb07](89fdb07))
* `Parse.serverURL` not accessible via global `Parse` scope ([#2917](#2917)) ([4e78681](4e78681))
* Cloud trigger type errors for void returns and subclass constructors ([#2904](#2904)) ([de9f56d](de9f56d))
* Missing or incorrect type exports ([#2909](#2909)) ([3caa4ec](3caa4ec))
* Type error in `Parse.Query.equalTo` when matching optional array ([#2901](#2901)) ([8c96da9](8c96da9))

### Features

* Add request header `X-Parse-Upload-Mode` to identify file upload as binary data via `Buffer`, `Readable`, `ReadableStream` ([#2927](#2927)) ([a66bb06](a66bb06))
* Add support for file upload as binary data via `Buffer`, `Readable`, `ReadableStream` ([#2925](#2925)) ([e42caf6](e42caf6))
@parseplatformorg
Copy link
Contributor

🎉 This change has been released in version 8.2.0

@parseplatformorg parseplatformorg added the state:released Released as stable version label Feb 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state:released Released as stable version state:released-alpha Released as alpha version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants