-
Notifications
You must be signed in to change notification settings - Fork 203
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor server to run in all (Node.js compatible) runtimes and meta frameworks #710
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 82e21d9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
New dependencies detected. Learn more about Socket for GitHub ↗︎
|
This comment was marked as resolved.
This comment was marked as resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 10
🔭 Outside diff range comments (3)
packages/server/test/GetHandler.test.ts (1)
1-252
: Overall implementation successfully transitions to Web API types.The test suite has been successfully modernized to use Web API's Request/Response types and CancellationContext, aligning perfectly with the PR's objective of making the server runtime-agnostic. However, there's one area that needs attention:
- The skipped test for invalid streams should be addressed to maintain comprehensive test coverage.
packages/server/src/handlers/BaseHandler.ts (2)
179-184
: Correct the misuse ofstream.pipeline
inwriteToStore
methodThe use of
stream.pipeline
in thewriteToStore
method is incorrect. Thestream.pipeline
function expects the last argument to be a destination stream or an array of streams, but an async function is provided instead. This can lead to unexpected behavior or runtime errors.Apply this diff to fix the usage of
stream.pipeline
:- stream - .pipeline(data.pipe(proxy), new StreamLimiter(maxFileSize), async (stream) => { - return this.store.write(stream as StreamLimiter, upload.id, upload.offset) - }) - .then(resolve) - .catch(reject) - .finally(() => { - context.signal.removeEventListener('abort', onAbort) - }) + try { + await stream.pipeline( + data.pipe(proxy), + new StreamLimiter(maxFileSize), + this.store.writeStream(upload.id, upload.offset) + ) + resolve() + } catch (err) { + reject(err) + } finally { + context.signal.removeEventListener('abort', onAbort) + }Alternatively, consider refactoring
this.store.write
to return a writable stream that can be used directly in the pipeline.
110-127
: Ensure option callbacks acceptRequest
objectsThe methods
getLocker
,acquireLock
,getConfiguredMaxSize
, andcalculateMaxBodySize
have been updated to acceptRequest
objects instead ofhttp.IncomingMessage
. Make sure that any custom functions provided inthis.options
(such aslocker
,maxSize
, etc.) are compatible with theRequest
object to prevent runtime errors.Also applies to: 191-196, 203-240
🧹 Nitpick comments (15)
packages/server/test/OptionsHandler.test.ts (2)
28-33
: Consider adding tests for abort functionality.The CancellationContext is set up but its functionality isn't tested. Consider adding test cases to verify the abort behavior.
Example test case:
it('should handle aborted requests', async () => { const promise = handler.send(req, context) context.abort() await assert.rejects(promise, /aborted/) })
37-66
: Consider adding more comprehensive test cases.While the current tests cover the basic functionality, consider adding:
- Error cases (e.g., invalid paths)
- Content-Type header verification
- Edge cases for extensions
Example additional test:
it('should handle invalid paths', async () => { const invalidReq = new Request('https://example.com/invalid', { method: 'OPTIONS' }) const res = await handler.send(invalidReq, context) assert.equal(res.status, 404) })packages/server/test/PatchHandler.test.ts (4)
24-28
: Consider extracting common request creation logic into a helper function.The request creation logic is repeated across multiple tests. Consider creating a helper function to improve maintainability and reduce duplication.
+ function createTestRequest(path: string, options: { + method?: string, + headers?: Headers, + body?: BodyInit | null, + } = {}) { + return new Request(`https://example.com${path}`, { + method: options.method ?? 'PATCH', + headers: options.headers ?? new Headers(), + duplex: 'half', + body: options.body, + }) + } - req = new Request(`https://example.com${path}/1234`, { - method: 'PATCH', - headers: new Headers(), - duplex: 'half', - }) + req = createTestRequest(`${path}/1234`)
185-189
: Add more comprehensive response assertions.The test verifies basic response properties but could benefit from additional assertions to ensure the complete Response object behavior.
const res = await handler.send(req, context) assert.equal(res.headers.get('upload-offset'), '10') assert.equal(res.headers.has('Content-Length'), false) assert.equal(res.status, 204) + assert.equal(res.ok, true) + assert.equal(res.statusText, 'No Content') + assert.equal(res.body, null)
229-238
: Consider using more structured error assertions.The error object structure verification could be more comprehensive and type-safe.
try { await handler.send(req, context) throw new Error('failed test') } catch (e) { - assert.equal('body' in e, true) - assert.equal('status_code' in e, true) - assert.equal(e.body, 'Maximum size exceeded\n') - assert.equal(e.status_code, 413) + type ExpectedError = { + body: string + status_code: number + name: string + message: string + } + assert.equal(Object.prototype.hasOwnProperty.call(e, 'body'), true) + assert.equal(Object.prototype.hasOwnProperty.call(e, 'status_code'), true) + const error = e as ExpectedError + assert.equal(error.body, 'Maximum size exceeded\n') + assert.equal(error.status_code, 413) + assert.equal(error.name, 'Error') + assert.equal(typeof error.message, 'string') }
274-331
: Add edge cases for stream handling.While the current test covers the basic streaming scenario, consider adding tests for:
- Handling very large streams with memory constraints
- Network interruptions during streaming
- Malformed stream data
Example test for memory constraints:
it('should handle large streams without excessive memory usage', async () => { const handler = new PatchHandler(store, {path, locker: new MemoryLocker()}) const bodyStream = new PassThrough() const req = new Request(`https://example.com${path}/1234`, { method: 'PATCH', headers: new Headers({ 'Content-Type': 'application/offset+octet-stream', 'Upload-Offset': '0', }), duplex: 'half', body: bodyStream, }) // Monitor memory usage const initialMemory = process.memoryUsage().heapUsed let peakMemory = initialMemory const interval = setInterval(() => { const currentMemory = process.memoryUsage().heapUsed peakMemory = Math.max(peakMemory, currentMemory) }, 100) try { // Write 1GB of data in 1MB chunks for (let i = 0; i < 1024; i++) { bodyStream.write(Buffer.alloc(1024 * 1024)) // 1MB await new Promise(resolve => setTimeout(resolve, 10)) // Allow time for processing } bodyStream.end() await handler.send(req, context) // Verify memory usage didn't grow linearly with data size const memoryGrowth = (peakMemory - initialMemory) / (1024 * 1024) // MB assert.ok(memoryGrowth < 100, `Memory growth (${memoryGrowth}MB) exceeded 100MB`) } finally { clearInterval(interval) } })packages/server/src/handlers/OptionsHandler.ts (1)
10-10
: Use a constant for the 'Tus-Version' headerThe 'Tus-Version' header is hardcoded as
'1.0.0'
. Consider defining a constant or importing the version from a centralized configuration or package metadata. This approach ensures consistency across the codebase and simplifies future updates to the protocol version.packages/server/src/handlers/HeadHandler.ts (2)
56-56
: Confirm necessity of type assertion onfile.size
The type assertion
(file.size as number).toString()
assumesfile.size
is anumber
. Iffile.size
is already typed asnumber
, the assertion may be unnecessary. Verify the type offile.size
and remove the assertion if it's redundant.
63-63
: Check if type assertion onMetadata.stringify
is neededThe
Metadata.stringify(file.metadata)
function should return astring
. The type assertionas string
might be redundant. Ensure that the return type is correctly inferred to simplify the code.packages/server/src/web.ts (1)
25-29
: Simplify handling of destroyed requests inget_raw_body
When the request is destroyed, the function returns a canceled
ReadableStream
. Consider simplifying this by returningnull
or handling it within the main logic, as creating a canceled stream may add unnecessary complexity without significant benefits.packages/server/src/types.ts (1)
102-105
: Consider using more specific response types.The response types could be more specific to improve type safety:
- Consider using a union type for status_code to restrict to valid HTTP status codes
- Consider using a more specific type for headers to ensure valid HTTP header names
) => Promise<{ - status_code?: number + status_code?: 200 | 201 | 204 | 400 | 404 | 409 | 412 | 413 | 415 | 460 - headers?: Record<string, string | number> + headers?: {[K in HTTPHeaderName]?: string | number} body?: string }>Also applies to: 117-124
packages/server/src/handlers/PostHandler.ts (3)
39-39
: Consider making the headers parameter required.The
headers
parameter has a default value but is used throughout the method. Making it required would better reflect its importance and usage.-async send(req: Request, context: CancellationContext, headers = new Headers()) { +async send(req: Request, context: CancellationContext, headers: Headers) {
60-66
: Improve metadata error handling.The current implementation swallows the original error, making debugging harder. Consider preserving the error details.
if (upload_metadata) { try { metadata = Metadata.parse(upload_metadata ?? undefined) - } catch { - throw ERRORS.INVALID_METADATA + } catch (error) { + throw { + ...ERRORS.INVALID_METADATA, + details: error instanceof Error ? error.message : 'Unknown parsing error' + } } }
98-107
: Enhance onUploadCreate hook response handling.The hook's response handling could be more robust by checking if
patch
exists before accessing its properties.if (this.options.onUploadCreate) { try { const patch = await this.options.onUploadCreate(req, upload) - if (patch.metadata) { + if (patch?.metadata) { upload.metadata = patch.metadata } } catch (error) { log(`onUploadCreate error: ${error.body}`) throw error } }packages/server/test/PostHandler.test.ts (1)
47-53
: Add test cases for custom headers.Consider adding test cases to verify custom headers passed to the
send
method are properly handled.it('should handle custom headers', async () => { const handler = new PostHandler(store, options) const req = new Request(`https://example.com${options.path}`, { headers: {'upload-length': '512'} }) const customHeaders = new Headers({'X-Custom': 'test'}) const res = await handler.send(req, context, customHeaders) assert.equal(res.headers.get('X-Custom'), 'test') })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (25)
biome.json
(1 hunks)packages/server/package.json
(1 hunks)packages/server/src/handlers/BaseHandler.ts
(7 hunks)packages/server/src/handlers/DeleteHandler.ts
(2 hunks)packages/server/src/handlers/GetHandler.ts
(4 hunks)packages/server/src/handlers/HeadHandler.ts
(2 hunks)packages/server/src/handlers/OptionsHandler.ts
(1 hunks)packages/server/src/handlers/PatchHandler.ts
(5 hunks)packages/server/src/handlers/PostHandler.ts
(6 hunks)packages/server/src/server.ts
(10 hunks)packages/server/src/types.ts
(6 hunks)packages/server/src/validators/HeaderValidator.ts
(1 hunks)packages/server/src/web.ts
(1 hunks)packages/server/test/BaseHandler.test.ts
(2 hunks)packages/server/test/DeleteHandler.test.ts
(5 hunks)packages/server/test/GetHandler.test.ts
(8 hunks)packages/server/test/HeadHandler.test.ts
(6 hunks)packages/server/test/OptionsHandler.test.ts
(2 hunks)packages/server/test/PatchHandler.test.ts
(5 hunks)packages/server/test/PostHandler.test.ts
(10 hunks)packages/server/test/Server.test.ts
(8 hunks)packages/server/test/utils.ts
(0 hunks)packages/utils/src/models/DataStore.ts
(2 hunks)test/src/e2e.test.ts
(1 hunks)tsconfig.base.json
(1 hunks)
💤 Files with no reviewable changes (1)
- packages/server/test/utils.ts
🔇 Additional comments (45)
packages/server/test/OptionsHandler.test.ts (2)
6-12
: LGTM! Clean import organization.The imports are well-organized and include the necessary types for the new request handling approach.
24-35
: LGTM! Modern request handling setup.The setup correctly uses the fetch API's Request object and includes proper cancellation support, aligning with the PR's objective to move away from Node.js-specific types.
packages/server/test/GetHandler.test.ts (5)
10-10
: LGTM! Type declarations align with PR objectives.The changes correctly transition from Node.js-specific HTTP types to Web API's Request type and introduce CancellationContext for request cancellation handling.
Also applies to: 17-18
21-27
: LGTM! Test setup properly initializes Web API constructs.The beforeEach hook correctly sets up:
- A Web API Request object
- An AbortController for cancellation
- A CancellationContext with proper signal and abort methods
71-82
: Investigate and fix the skipped test case.The test for invalid stream handling has been skipped. This could leave a gap in error handling coverage.
Could you clarify why this test was skipped? If it's still relevant, we should update it to work with the new Web API constructs rather than skipping it.
108-111
: LGTM! Response assertions properly updated.The test correctly validates the Web API Response object's status and headers.
235-236
: LGTM! Path handler correctly returns Web API Response.The registered path handler properly returns a Web API Response object, maintaining consistency with the new API.
packages/server/src/handlers/OptionsHandler.ts (2)
7-23
: Refactored 'send' method enhances compatibility and code clarityThe updated
send
method now accepts aRequest
,CancellationContext
, andHeaders
, aligning it with the Fetch API standards. This change improves compatibility across various runtimes and meta-frameworks, promoting a more universal handling mechanism. The use of theHeaders
object to set response headers enhances code modularity and readability.
7-7
: Verify the necessity of the unused 'context' parameterThe
context
parameter in thesend
method is currently not utilized within the method body. Ensure that this parameter is required, possibly for interface compliance or future expansion. If it's not necessary, consider removing it to simplify the method signature and improve maintainability.packages/server/src/handlers/HeadHandler.ts (6)
6-6
: Method signature updated to use Fetch API'sRequest
The
send
method now accepts aRequest
object instead of Node.js-specific types. This enhances compatibility across different runtimes and meta-frameworks.
39-39
: Response instantiated using Fetch API'sResponse
objectThe response is now created with
new Response('', {status: 200, headers})
, aligning with the Fetch API standards and improving cross-environment compatibility.
44-44
: Headers set using Fetch API'sHeaders
methodsHeaders are now being set using
res.headers.set()
, which is appropriate for theHeaders
object in the Fetch API.
47-47
: Verifyfile.offset
is defined and correctly typedEnsure that
file.offset
is always defined and of typenumber
before calling.toString()
. This prevents potential runtime errors iffile.offset
isundefined
or not a number.If necessary, add validation or default values to handle cases where
file.offset
might be undefined.
52-52
: SetUpload-Defer-Length
header appropriatelySetting the
Upload-Defer-Length
header to'1'
when the upload length is deferred is correct and complies with the protocol specifications.
66-66
: ReturnResponse
object to complete the request handlingReturning the
Response
object finalizes the request handling using the Fetch API approach, which is appropriate for modern runtimes.packages/server/src/handlers/PatchHandler.ts (2)
14-14
: Updated method signature aligns with modern Request handlingChanging the
send
method to acceptRequest
,CancellationContext
, andHeaders
enhances compatibility with various runtimes and aligns with the refactoring objectives.
99-104
: Ensure Node.js version compatibility forReadable.fromWeb
usageThe
Readable.fromWeb(req.body)
method requires Node.js 17.0.0 or higher. Please verify that the project's minimum Node.js version supports this feature, or implement a fallback for older versions.packages/server/src/handlers/DeleteHandler.ts (2)
5-5
: Confirm the necessity of default parameter forheaders
The
send
method now includes a default parameterheaders = new Headers()
. Verify if providing a defaultHeaders
instance is necessary or if it could lead to unintended side effects. Ifheaders
should always be provided by the caller, consider removing the default to enforce explicit passing.
28-28
: Verify thewrite
method returns the correct response typeThe
write
method is called and its result assigned towrittenRes
. Ensure thatthis.write
correctly returns aResponse
object compatible with the expected usage in the rest of the method, especially when emitting events and returning the response.packages/server/test/BaseHandler.test.ts (6)
22-26
: Update tests to reflect changes inwrite
methodThe test for
write()
now captures the returnedResponse
object and checksres.status
. This aligns the test with the updated implementation ofwrite
. The use ofassert.equal(res.status, 200)
correctly verifies the status code.
31-33
: Adjust header assertions to useResponse
objectThe test now uses
res.headers.get(header)
to assert header values, reflecting the change from a mock response to the nativeResponse
object. This ensures that header checks are accurately performed.
36-40
: Convert test to async function for body content verificationThe test for writing the body has been updated to an async function to await
res.text()
. This change is appropriate for handling asynchronous operations when reading the response body.
43-46
: Use Fetch API'sRequest
in tests for URL parsingReplacing mock requests with the native
Request
object in tests ensures consistency with the updated handler methods. The test correctly verifies thatgetFileIdFromRequest
extracts the file ID from the URL.
64-72
: Test custom URL generation with updated request objectThe test confirms that a custom
generateUrl
function produces the expected URL when provided with aRequest
object. The assertion verifies that query parameters are correctly appended.
78-85
: Validate custom file ID extraction logicThe test checks that a custom
getFileIdFromRequest
function correctly extracts and modifies the file ID from the request URL. Using the nativeRequest
object ensures that the URL parsing aligns with the actual request handling.packages/server/src/web.ts (2)
6-65
: Review correctness ofget_raw_body
implementationThe
get_raw_body
function handles parsing the raw request body from anhttp.IncomingMessage
. Ensure that it correctly manages edge cases, such as missing content types, zero content lengths, and destroyed requests. The use of aReadableStream
and proper event handling appears appropriate.
101-172
: Ensure robust error handling insetResponse
The
setResponse
function includes complex logic for setting headers and streaming the response body, with careful error and cancellation handling. Verify that all potential edge cases and exceptions are properly managed to prevent issues such as unhandled promise rejections or partially sent responses.packages/server/src/server.ts (2)
130-134
: Verify the compatibility ofHeaders
constructor withreq.headers
In the
handle
method,req.headers
fromhttp.IncomingMessage
is being passed directly tonew Headers(req.headers)
. TheHeaders
constructor may not accept plain objects with string values in all environments. This can lead to issues in header manipulation.Please ensure that
req.headers
is properly converted into an array of[name, value]
pairs or use an appropriate method to create aHeaders
instance. You can verify compatibility by reviewing the Node.js and Fetch API documentation.
297-299
: Reconsider the removal of thereq.on('close')
listenerCommenting out the
req.on('close', ...)
event listener may affect how the server handles aborted requests. This listener is important for cleaning up resources when a client disconnects unexpectedly.Assess whether the removal impacts request cancellation handling and resource management. If necessary, reintroduce the listener or implement an alternative mechanism to handle request closures.
packages/server/test/DeleteHandler.test.ts (1)
15-15
: LGTM! Clean transition to Fetch API.The changes correctly update the request object type and initialization to use the modern Fetch API, which aligns with the PR's objective of runtime compatibility.
Also applies to: 25-25
packages/server/test/HeadHandler.test.ts (1)
11-11
: LGTM! Good URL handling practice.The addition of a url constant and proper Request initialization improves code clarity and maintainability.
Also applies to: 18-18, 22-24
packages/server/src/validators/HeaderValidator.ts (1)
92-92
: LGTM! Type alignment with Fetch API.The parameter type change from
string | undefined
tostring | null
correctly aligns with the Fetch API's Headers.get() return type.packages/server/src/types.ts (2)
15-15
: LGTM! Clean type updates for request handlers.The type changes correctly modernize the request handling interfaces while maintaining type safety.
Also applies to: 54-56, 62-62, 71-74, 80-80
150-150
: LGTM! Clear RouteHandler type definition.The RouteHandler type is correctly updated to use Request and Response types from the Fetch API.
packages/server/test/Server.test.ts (1)
254-265
: Re-enable the HTTP method override test.This test is currently skipped. Since HTTP method override is an important feature, consider updating and re-enabling it to use the new Request/Response types.
✅ Verification successful
🏁 Script executed:
#!/bin/bash # Search for other tests that might depend on HTTP method override rg -l "x-http-method-override" .Length of output: 70
Re-enable the HTTP method override test
The only occurrence of"x-http-method-override"
is inpackages/server/test/Server.test.ts
, confirming that this test is the sole reference. Please remove the.skip
(and address the accompanying@ts-expect-error
by updating to the new Request/Response types) so that HTTP method overriding is properly validated throughout the codebase.test/src/e2e.test.ts (1)
1132-1132
: Verify the change in locking behavior.The assertion for a 400 status code has been commented out. Please confirm if this represents an intentional change in how locks are handled when a request is cancelled.
✅ Verification successful
🏁 Script executed:
#!/bin/bash # Search for related lock handling code rg "acquireLock|releaseLock" .Length of output: 1252
Locking behavior change appears intentional.
The search results show that the core locking functions (e.g. acquireLock/releaseLock) remain unaltered across the repository. There is no new logic indicating a change in cancellation handling (such as returning a 400 error) in the locker implementation or its usage in request handlers. It seems that the commented-out assertion was deliberately removed to reflect updated test expectations rather than a modification in the underlying locking behavior.tsconfig.base.json (1)
12-13
: Configuration Option Trade-off: "skipLibCheck"
The addition of"skipLibCheck": true
can significantly improve build performance by skipping type checks in declaration files. Please ensure that this trade-off is acceptable for your project since it may mask type issues in third-party libraries.biome.json (2)
7-7
: Expanded Ignore Patterns in Files
The updated"ignore": [".git", "node_modules", "./**/dist/**/*"]
ensures that common folders not relevant to source code are excluded from processing. This update is standard and should help avoid unnecessary file processing.
13-17
: Linter Rule Adjustments for Style and Performance
The changes turning"noParameterAssign": "off"
(under the"style"
section) and"noDelete": "off"
(under the"performance"
section) indicate a conscious decision to relax these rules. Please verify that allowing parameter mutation and object property deletion won’t lead to unintended side effects or maintenance challenges in your codebase.packages/server/package.json (2)
24-25
: New Dependency: set-cookie-parser
The dependency"set-cookie-parser": "^2.7.1"
has been added to support improved cookie handling with the fetch API’s response processing. Please confirm that this version has been evaluated for compatibility and security within your project’s context.
32-32
: Type Definitions for set-cookie-parser
Adding"@types/set-cookie-parser": "^2.4.10"
to devDependencies is a necessary adjunct to ensure proper TypeScript type checking for the new package. This change is appropriate for maintaining type safety in your codebase.packages/server/src/handlers/GetHandler.ts (4)
61-64
: Verify that all calls tosend
are updated to the new signature.The
send
method now accepts aRequest
,CancellationContext
, and an optionalHeaders
object instead of the Node.js-specificIncomingMessage
andServerResponse
. Please ensure that all invocations ofsend
throughout the codebase are updated to match this new signature to prevent type errors or runtime issues.
68-69
: Confirm that all route handlers return aResponse
object.The handler functions stored in
this.paths
are now expected to return aResponse
object compliant with the Fetch API. Please verify that all registered route handlers have been updated accordingly to ensure proper functionality.
93-93
: Verify thatacquireLock
method supportsRequest
andCancellationContext
.The
acquireLock
method is now being called withreq: Request
,id
, andcontext: CancellationContext
. Ensure that theacquireLock
method has been updated to accept these parameters and that it handles theRequest
object correctly.
100-100
:⚠️ Potential issueEnsure compatibility of
file_stream
with Fetch APIResponse
.The
Response
constructor expects the body to be a webReadableStream
, butfile_stream
is likely a Node.jsReadable
stream, which is not directly compatible. To prevent runtime issues, convert the Node.js stream to a webReadableStream
usingReadable.toWeb()
from thestream
module:- return new Response(file_stream, {headers, status: 200}) + import { Readable } from 'stream' + const webStream = Readable.toWeb(file_stream) + return new Response(webStream, {headers, status: 200})Note that
Readable.toWeb()
is available from Node.js v17.0.0 onwards. If you're targeting earlier versions, consider using a polyfill or a library likeweb-streams-polyfill
to perform the conversion.To check if
Readable.toWeb()
is available in your Node.js environment, you can run the following command:Alternatively, you can search for methods to convert a Node.js
Readable
stream to a webReadableStream
:✅ Verification successful
🏁 Script executed:
#!/bin/bash # Check Node.js version node -vLength of output: 17
🌐 Web query:
How to convert a Node.js Readable stream to a web ReadableStream for use with Fetch API Response?
💡 Result:
To convert a Node.js `Readable` stream to a web `ReadableStream` for use with the Fetch API `Response`, there are two primary approaches: --- ### 1. Using `readable-stream-node-to-web` Package Install via npm: ```bash npm install readable-stream-node-to-web ``` **Implementation**: ```javascript const nodeToWebStream = require('readable-stream-node-to-web'); const nodeStream = getNodeReadableStream(); // Your Node.js stream source // Convert to web ReadableStream const webStream = nodeToWebStream(nodeStream); // Use with Fetch API Response const response = new Response(webStream, { headers: { 'Content-Type': 'text/plain' } }); ``` - **Key Features**: - Works in browsers via bundlers like Webpack/Browserify[1]. - ES5-compatible syntax[1]. --- ### 2. Using Native Node.js `stream/web` APIs (v16.5+) Node.js natively supports Web Streams via `node:stream/web`: ```javascript const { Readable } = require('node:stream'); const { ReadableStream } = require('node:stream/web'); // Convert using Readable.toWeb() const nodeStream = Readable.from(['Hello', 'World']); // Example source const webStream = Readable.toWeb(nodeStream); // Example usage with Fetch API const response = new Response(webStream); ``` **Key Differences**: | Method | Compatibility | Dependencies | Stream Types Supported | |----------------------------|---------------------|--------------|---------------------------------| | `readable-stream-node-to-web` | Older browsers/Node | External | Classic/Node-specific streams | | Native `Readable.toWeb()` | Node.js ≥16.5 | None | Modern Web/Node hybrid streams | --- ### Common Use Case with Fetch API After conversion, use the web `ReadableStream` directly: ```javascript // Example: Stream data via Fetch response fetch('https://example.com') .then((response) => { const nodeReadable = response.body.pipeThrough(transformStream); const webStream = Readable.toWeb(nodeReadable); // or use package return new Response(webStream); }); ``` For backward compatibility or browser environments, use the package method[1][4]. For modern Node.js applications, prefer native methods[3][9]. Citations: - 1: https://github.com/xuset/readable-stream-node-to-web - 2: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream - 3: https://www.dennisokeeffe.com/blog/2024-07-08-readable-streams-in-nodejs - 4: https://2ality.com/2022/06/web-streams-nodejs.html - 5: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams - 6: https://css-tricks.com/web-streams-everywhere-and-fetch-for-node-js/ - 7: https://betterstack.com/community/guides/scaling-nodejs/nodejs-streams-vs-web-streams-api/ - 8: https://www.loginradius.com/blog/engineering/guest-post/http-streaming-with-nodejs-and-fetch-api/ - 9: https://nodejs.org/api/webstreams.html - 10: https://github.com/node-fetch/node-fetch/issues/754
Stream Conversion Required for Compatibility
The Node.js version is v22.9.0, which supports the native conversion using
Readable.toWeb()
. The web query confirms that converting a Node.jsReadable
stream to a webReadableStream
withReadable.toWeb()
is the recommended approach for use with the Fetch API'sResponse
constructor. Therefore, the suggested code change is valid.
- File:
packages/server/src/handlers/GetHandler.ts
- Modification: Convert
file_stream
usingReadable.toWeb(file_stream)
before passing it into theResponse
constructor.
@@ -1129,7 +1129,7 @@ describe('EndToEnd', () => { | |||
const [res1, res2] = await Promise.allSettled([req1, req2]) | |||
assert.equal(res1.status, 'fulfilled') | |||
assert.equal(res2.status, 'fulfilled') | |||
assert.equal(res1.value.statusCode, 400) | |||
// assert.equal(res1.value.statusCode, 400) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an important e2e locking test. The test with all its assertions still works as expected except that the first request now succeeds with 200
instead of 400
. This might be blocking for the merge? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels odd, yes, but I don't think it will cause major problems in production.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, that is an incredible work! I left a few minor comments, but I am not sure I fully understood the goal of this PR. It removes the dependency on Node's http module for the main handlers, but adds a dependency on Node's stream module, so the handler can still only be used in Node-compatible runtimes (as the PR title mentions).
What exactly do we gain here now? After this PR, tus-node-server can still only be used in Node-compatible runtimes. Was this not the case before?
Or is this just a stepping stone for future PRs to make the handler completely independent from Node specifics?
@@ -252,7 +251,7 @@ describe('Server', () => { | |||
request(listener).get('/').set('Tus-Resumable', TUS_RESUMABLE).expect(404, {}, done) | |||
}) | |||
|
|||
it('should allow overriding the HTTP method', (done) => { | |||
it.skip('should allow overriding the HTTP method', (done) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this still be implemented?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed HTTP method overriding as it seems very outdated, the reasoning from the code commend being when used in Flash. Do you think we should keep it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tus-java-client might still use method overriding when the used Java SDK does not support the PATCH method. However, I cannot tell you how common this case is right now.
@@ -1129,7 +1129,7 @@ describe('EndToEnd', () => { | |||
const [res1, res2] = await Promise.allSettled([req1, req2]) | |||
assert.equal(res1.status, 'fulfilled') | |||
assert.equal(res2.status, 'fulfilled') | |||
assert.equal(res1.value.statusCode, 400) | |||
// assert.equal(res1.value.statusCode, 400) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels odd, yes, but I don't think it will cause major problems in production.
@@ -0,0 +1,176 @@ | |||
import type http from 'node:http' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused by the name of this file. Its content seems to be about interactions between Node HTTP classes and Fetch API classes. Maybe this can be clarified through a better name?
There was a problem hiding this 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
🧹 Nitpick comments (5)
packages/server/src/web.ts (3)
1-1
: Consider renaming the file to better reflect its purpose.The current filename
web.ts
doesn't clearly indicate that this file contains utilities for converting between Node.js HTTP types and Web API types. Consider renaming it to something more descriptive likehttp-adapters.ts
orrequest-response-adapters.ts
.
6-65
: Add JSDoc documentation for the getRawBody function.This function handles several complex edge cases and stream management scenarios. Adding comprehensive JSDoc documentation would help future maintainers understand:
- The purpose and return type of the function
- The handling of different HTTP versions
- The stream backpressure mechanism
- The cancellation behavior
Add documentation like this:
+/** + * Extracts the raw body from an incoming HTTP request. + * + * @param req - The Node.js HTTP incoming message + * @returns A ReadableStream of the request body, or null if: + * - Content-Type header is missing + * - Content-Length is 0 + * - HTTP/1.1 request with no Content-Length and no Transfer-Encoding + */ function getRawBody(req: http.IncomingMessage) {
164-167
: Improve error handling in the stream piping logic.The catch block could be more specific about the types of errors it handles. Consider categorizing errors and providing different handling strategies based on the error type.
- } catch (error) { - cancel(error instanceof Error ? error : new Error(String(error))) + } catch (error) { + if (error instanceof TypeError) { + // Handle type errors (e.g., stream already closed) + cancel(new Error(`Stream error: ${error.message}`)) + } else if (error instanceof Error) { + // Handle known errors + cancel(error) + } else { + // Handle unknown errors + cancel(new Error(String(error))) + }packages/server/src/server.ts (2)
31-33
: Consider removing the deprecated event.The
POST_RECEIVE
event is marked as deprecated in favor ofPOST_RECEIVE_V2
. Since this appears to be a major refactoring, it might be a good time to remove the deprecated event.
208-217
: Consider extracting CORS header logic to a separate method.The CORS header setting logic could be extracted to improve readability and maintainability.
+ private setCorsHeaders(headers: Headers, origin: string | null) { + headers.set('Access-Control-Allow-Origin', this.getCorsOrigin(origin)) + headers.set('Access-Control-Expose-Headers', EXPOSED_HEADERS) + if (this.options.allowedCredentials === true) { + headers.set('Access-Control-Allow-Credentials', 'true') + } + }Then use it in the handler:
- headers.set( - 'Access-Control-Allow-Origin', - this.getCorsOrigin(req.headers.get('origin')) - ) - headers.set('Access-Control-Expose-Headers', EXPOSED_HEADERS) - if (this.options.allowedCredentials === true) { - headers.set('Access-Control-Allow-Credentials', 'true') - } + this.setCorsHeaders(headers, req.headers.get('origin'))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
packages/server/src/handlers/PatchHandler.ts
(5 hunks)packages/server/src/server.ts
(10 hunks)packages/server/src/types.ts
(6 hunks)packages/server/src/web.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/server/src/types.ts
🔇 Additional comments (9)
packages/server/src/handlers/PatchHandler.ts (5)
22-22
: LGTM! Header validation properly uses the Web standard Headers interface.The code correctly uses the
Headers.get()
method and handles null checks appropriately.Also applies to: 26-26, 29-30, 73-74
113-131
: LGTM! Response construction properly handles headers and hook responses.The code correctly:
- Merges input headers using
Object.fromEntries
- Handles hook responses by properly merging status, body, and headers
165-168
: LGTM! Improved error handling prevents redundant context aborts.The code now checks if the context is already aborted before attempting to abort it, which is a good practice.
99-104
: Consider updating store interface to use Web Streams.The current implementation converts Web Streams to Node.js streams to maintain compatibility with the store interface. Consider updating the store interface to use Web Streams directly in a future PR.
Let's check if there are any store implementations that might be affected:
✅ Verification successful
🏁 Script executed:
#!/bin/bash # Search for store implementations rg -l "implements.*Store" --type tsLength of output: 215
Store interface update: Maintain current conversion with a note to refactor in the future.
The search confirms that all store implementations (e.g., in FileKvStore.ts, RedisKvStore.ts, IoRedisKvStore.ts, MemoryKvStore.ts) are designed to work with Node.js streams. Thus, the current approach—converting a Web Stream (via
Readable.fromWeb(req.body)
) into a Node.js stream—is necessary to maintain compatibility. The proposal to update the store interface to accept Web Streams directly is a valid future improvement but would require coordinated changes across these implementations.
2-2
: Consider using Web Streams API instead of Node.js Readable.The PR aims to make the server runtime-agnostic, but the code still depends on Node.js-specific
Readable
streams. Consider using the Web Streams API (ReadableStream
) for better compatibility across different runtimes.Also applies to: 14-14, 100-100
✅ Verification successful
🌐 Web query:
What is the current browser support for Web Streams API?
💡 Result:
The Web Streams API has achieved widespread adoption across modern browsers with some variations in implementation details and historical support:
Desktop Browser Support
Browser Full Support Versions Partial Support Versions Chrome ✅ 89+ ◐ 52-88 Edge ✅ 89+ ◐ 14-88 Firefox ✅ 102+ ◐ 65-101 Safari ◐ 10+ (Partial) ❌ <10 Opera ✅ 76+ ◐ 39-75 Mobile Browser Support
Browser Support Status Chrome for Android ✅ Full (All recent versions) Safari on iOS ◐ Partial (10.3+) Firefox for Android ✅ Full (130+) Samsung Internet ✅ Full (15+) Key Details:
Core Components:
ReadableStream
: Available since Chrome 43[2][7], Firefox 65[2][7], Safari 10.1[2][7]WritableStream
: Later adoption (e.g., Chrome 59[2], Firefox not fully implemented[2])Safari Limitations:
- Supports streams but blocks usage with
fetch()
withoutduplex
option[3]- Partial implementation of transform streams[4][5]
Standardization Milestones:
- Became stable in Node.js v21[4][8]
- Supported across all major JS environments (browsers/Node.js/Deno)[4]
Notable Exceptions:
- No IE11/legacy browser support[1][5]
- Opera Mini/KaiOS lack support[1]
For production use today:
// Feature detection example if ('ReadableStream' in window && 'WritableStream' in window) { // Safe to use Web Streams API }Mobile developers should test target devices due to fragmented iOS/Safari implementation[1][5]. The API now enables advanced use cases like gRPC-web streaming[6] and real-time media processing[9].
Citations:
- 1: https://caniuse.com/streams
- 2: https://udn.realityripple.com/docs/Web/API/Streams_API
- 3: https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests
- 4: https://betterstack.com/community/guides/scaling-nodejs/nodejs-streams-vs-web-streams-api/
- 5: https://www.lambdatest.com/web-technologies/streams
- 6: https://groups.google.com/g/grpc-io/c/V3xdgcjns6Y
- 7: https://developer.mozilla.org/en-US/docs/Web/API/Streams_API..
- 8: https://nodejs.org/api/webstreams.html
- 9: https://web.dev/articles/streams
- 10: https://platform.uno/blog/state-of-webassembly-2024-2025/
Switch to Web Streams API for Cross-Runtime Compatibility
The current implementation in
packages/server/src/handlers/PatchHandler.ts
still relies on the Node.js-specificReadable
stream, which contradicts the PR’s goal of runtime-agnosticism. The Web query confirms that the Web Streams API is now widely supported across modern browsers—with full support in Chrome, Edge, and Firefox, and acceptable support in Safari and mobile browsers. To fully achieve runtime-agnostic behavior, consider:
- Replacing the
import {Readable} from 'node:stream'
with usage ofReadableStream
from the Web Streams API.- Reviewing method signatures (e.g., the change at line 14) and corresponding implementations to ensure compatibility.
- Incorporating fallback mechanisms or polyfills if targeting environments with partial support.
packages/server/src/web.ts (2)
88-89
: Address TypeScript error instead of suppressing it.The code uses
@ts-expect-error
to suppress a TypeScript error when setting headers. Instead of suppressing the error, consider using proper type casting.
170-172
: LGTM!The implementation correctly uses Node.js's built-in
Readable.toWeb
utility to convert file streams.packages/server/src/server.ts (2)
139-141
: LGTM!The new
handleWeb
method provides a clean interface for frameworks that use the Web API Request type.
244-261
: LGTM!The updated
write
method cleanly integrates with the Web API by returning a Response object. The implementation handles content length and connection headers appropriately.
Web standards only cover a limited standardized problem space, such as request/response, files (but not file system interactions), and streams. For our purposes it's therefore impossible to not use some Node.js dependencies. While some other JS runtimes offer their own Node.js equivalents, such as Deno and Bun, all mature runtimes (Cloudflare Workers, AWS Lambda, Deno Deploy, Bun, etc) offer Node.js compatibility. However, to not make all runtimes conform to Node's req/res just because they happen to be first, the WinterTC makes sure we have a standardized way to make basic servers. So some of the runtimes + all meta frameworks (Next.js, Nuxt, React Router, Svelte, SolidStart, etc) only expose a |
Thanks for the explanation! That helped me a lot understand the purpose of the PR 👍 I didn't know there a runtime that provide just selected modules for Node-compatiblity, such as streams. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/server/src/handlers/GetHandler.ts (1)
95-96
: 🛠️ Refactor suggestionAvoid using
@ts-expect-error
by updating type definitions.The use of
@ts-expect-error
indicates that TypeScript is unable to infer the correct types forthis.store.read(id)
.Consider updating the store's type definitions to include the optional
read
method:interface DataStore { read?: (id: string) => Promise<ReadableStream>; // ... other methods }
🧹 Nitpick comments (5)
packages/server/README.md (3)
163-163
: Add missing comma for better readability.Add a comma after "response" to improve sentence clarity.
-If the function returns the (modified) response the upload will be created. +If the function returns the (modified) response, the upload will be created.🧰 Tools
🪛 LanguageTool
[uncategorized] ~163-~163: Possible missing comma found.
Context: ... If the function returns the (modified) response the upload will be created. - You can o...(AI_HYDRA_LEO_MISSING_COMMA)
207-207
: Add period after 'etc' in American English.For consistency with American English style.
-or a meta framework (such as Next.js app router, Nuxt, React Router, SvelteKit, etc) +or a meta framework (such as Next.js app router, Nuxt, React Router, SvelteKit, etc.)🧰 Tools
🪛 LanguageTool
[style] ~207-~207: In American English, abbreviations like “etc.” require a period.
Context: ... router, Nuxt, React Router, SvelteKit, etc) or a Node.js compatible runtime based ...(ETC_PERIOD)
69-70
: Consider adding a code example for the maxSize function.The documentation would benefit from a practical example showing how to implement a dynamic maxSize function.
// Example implementation: const maxSize = async (req, id) => { // Dynamic size based on user role const userRole = await getUserRole(req); return userRole === 'premium' ? 1024 * 1024 * 1024 : 100 * 1024 * 1024; };.changeset/polite-bikes-train.md (2)
5-6
: Fix punctuation in meta frameworks list.Add a period after "etc" to follow proper American English style.
- (such as Next.js, Nuxt, React Router, SvelteKit, etc) and other Node.js compatible runtime environments. + (such as Next.js, Nuxt, React Router, SvelteKit, etc.) and other Node.js compatible runtime environments.🧰 Tools
🪛 LanguageTool
[style] ~6-~6: In American English, abbreviations like “etc.” require a period.
Context: ...Next.js, Nuxt, React Router, SvelteKit, etc) and other Node.js compatible runtime e...(ETC_PERIOD)
10-13
: Fix typo in metadata merge description.Remove the duplicated word "will".
- This will will internally merge the existing metadata with the new metadata. + This will internally merge the existing metadata with the new metadata.🧰 Tools
🪛 LanguageTool
[duplication] ~12-~12: Possible typo: you repeated a word.
Context: ...a: Record<string, string> }>`. This will will internally merge the existing metadata ...(ENGLISH_WORD_REPEAT_RULE)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.changeset/polite-bikes-train.md
(1 hunks)packages/server/README.md
(17 hunks)packages/server/src/handlers/GetHandler.ts
(4 hunks)
🧰 Additional context used
🪛 LanguageTool
.changeset/polite-bikes-train.md
[style] ~6-~6: In American English, abbreviations like “etc.” require a period.
Context: ...Next.js, Nuxt, React Router, SvelteKit, etc) and other Node.js compatible runtime e...
(ETC_PERIOD)
[duplication] ~12-~12: Possible typo: you repeated a word.
Context: ...a: Record<string, string> }>`. This will will internally merge the existing metadata ...
(ENGLISH_WORD_REPEAT_RULE)
packages/server/README.md
[uncategorized] ~163-~163: Possible missing comma found.
Context: ... If the function returns the (modified) response the upload will be created. - You can o...
(AI_HYDRA_LEO_MISSING_COMMA)
[style] ~207-~207: In American English, abbreviations like “etc.” require a period.
Context: ... router, Nuxt, React Router, SvelteKit, etc) or a Node.js compatible runtime based ...
(ETC_PERIOD)
🔇 Additional comments (9)
packages/server/README.md (2)
199-209
: LGTM! Great addition of Web API support.The addition of
handleWeb
method alongside the traditionalhandle
method is a significant improvement, enabling better integration with modern frameworks and runtimes that use the Web API's Request/Response model.🧰 Tools
🪛 LanguageTool
[style] ~207-~207: In American English, abbreviations like “etc.” require a period.
Context: ... router, Nuxt, React Router, SvelteKit, etc) or a Node.js compatible runtime based ...(ETC_PERIOD)
507-528
: LGTM! Well-documented Next.js App Router integration.The new App Router integration example is clear, concise, and follows Next.js best practices by exporting individual HTTP method handlers.
.changeset/polite-bikes-train.md (2)
1-4
: LGTM! Appropriate version bump for breaking changes.The major version bump is correctly specified given the significant API changes being introduced.
7-9
: LGTM! Clear documentation of breaking changes.The breaking changes are well documented, clearly indicating the transition from Node.js-specific types to web standard Request/Response objects.
packages/server/src/handlers/GetHandler.ts (5)
2-2
: LGTM! Import changes align with modern web standards.The addition of
CancellationContext
andERRORS
imports from@tus/utils
aligns with the PR's objective to modernize the codebase.
60-64
: LGTM! Method signature modernization.The updated signature aligns with web standards by:
- Using the fetch API's
Request
type instead ofhttp.IncomingMessage
- Adding
CancellationContext
for better concurrency control- Using the web standard
Headers
class with a sensible default
65-65
: Ensure thatreq.url
is an absolute URL to preventTypeError
innew URL()
.Using
new URL(req.url)
assumes thatreq.url
is an absolute URL. Ifreq.url
is a relative URL, this will throw aTypeError
.#!/bin/bash # Search for URL construction patterns in the codebase rg "new URL\(.*?req\.url" -A 2
82-82
: UpdateonIncomingRequest
to accept aRequest
object.The
onIncomingRequest
hook is now called with aRequest
object instead ofhttp.IncomingMessage
.#!/bin/bash # Search for onIncomingRequest implementations rg "onIncomingRequest.*?\(" -A 2
93-103
: LGTM! Improved concurrency control with proper cleanup.The lock acquisition and cleanup in a try-finally block ensures proper resource management.
Closes #461
Refactor the request handlers and hooks to be based on
Request
(from the fetch API) instead ofhttp.IncomingMessage
/http.ServerResponse
(which are Node.js specific) as we can turn the Node.js ones intoRequest
/Response
but not the other way around. That means we can have ahandle(req, res)
method for Node.js servers and ahandleWeb(req)
for other runtimes and meta-frameworks (such as Next.js React Router, SvelteKit, etc) which both use the same core logic.Changes
getRequest
/setResponse
utils (inweb.ts
) to turnhttp.IncomingMessage
/http.ServerResponse
intoRequest
/Response
handle(req, res)
function to use the new utils and pass aRequest
to a newhandler()
methodhandler()
is the new shared request handlerRequest
handleWeb(req: Request)
for runtimes/frameworks based on the fetch apiBaseHandler.extractHostAndProto()
static so we can use it too inhandle()
as we must construct a valid url to create aRequest
object.Readable.fromWeb(req.body)
to keep all that logic exactly the same.Request
/Response
Request
/Response
addPipableStreamBody
test util. It seems no longer needed.Notes for reviewers
handleWeb()
works in the app router.finish
,close
) to cancel the context. We don't have that forRequest
/Response
so we can't easily do that. Now we cancel the context after the handler is done but before the response is send to the client. We don't abort when the request is aborted (potentially problematic) yet no tests are failing.getRequest()
and cancel context based on the Node.js events there but that means we only deal with that case forhandle()
and nothandleWeb()
?tus-node-server/packages/server/src/web.ts
Lines 33 to 65 in cd5394c
@tus/server
but not necessarily for all stores (unless we want to align versions). More breaking changes would be done in other PRs, such as increasing the required Node.js version and removing more deprecated things.