Conversation
|
@rosald thanks for taking a stab! This is what I was thinking about for #1864 However, the PR as-is wouldn't suffice as we are hiding errors. Most likely, the best approach is to have a new API, something like |
There was a problem hiding this comment.
Pull Request Overview
This PR replaces Node.js pipe() with pipeline() for stream handling to improve error handling and prevent client connections from hanging when stream errors occur.
- Replaces
pipe()withpipeline()for all stream types (Blob, ReadableStream, Response, and general streams) - Removes automatic error listeners from streams, delegating error handling to user code
- Updates tests to reflect the new behavior where error handling is the user's responsibility
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| lib/response.js | Fixes import name and removes automatic error listener for streams |
| lib/application.js | Replaces pipe() with pipeline() for all stream response types |
| tests/response/body.test.js | Updates test to verify no automatic error listeners are added |
| tests/application/respond.test.js | Updates test to add manual error handling for streams |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| if (body instanceof Blob) { return Stream.pipeline(body.stream(), res, (err) => {}) } | ||
| if (body instanceof ReadableStream) { return Stream.pipeline(body, res, (err) => {}) } | ||
| if (body instanceof Response) { return Stream.pipeline(body?.body || '', res, (err) => {}) } | ||
| if (isStream(body)) { return Stream.pipeline(body, res, (err) => {}) } |
There was a problem hiding this comment.
Empty error callback functions silently ignore errors. Consider logging errors or at minimum adding a comment explaining why errors are intentionally ignored.
| if (body instanceof Blob) { return Stream.pipeline(body.stream(), res, (err) => {}) } | |
| if (body instanceof ReadableStream) { return Stream.pipeline(body, res, (err) => {}) } | |
| if (body instanceof Response) { return Stream.pipeline(body?.body || '', res, (err) => {}) } | |
| if (isStream(body)) { return Stream.pipeline(body, res, (err) => {}) } | |
| if (body instanceof Blob) { return Stream.pipeline(body.stream(), res, (err) => { if (err) debug('Stream.pipeline error:', err) }) } | |
| if (body instanceof ReadableStream) { return Stream.pipeline(body, res, (err) => { if (err) debug('Stream.pipeline error:', err) }) } | |
| if (body instanceof Response) { return Stream.pipeline(body?.body || '', res, (err) => { if (err) debug('Stream.pipeline error:', err) }) } | |
| if (isStream(body)) { return Stream.pipeline(body, res, (err) => { if (err) debug('Stream.pipeline error:', err) }) } |
| if (isStream(body)) return body.pipe(res) | ||
| if (body instanceof Blob) { return Stream.pipeline(body.stream(), res, (err) => {}) } | ||
| if (body instanceof ReadableStream) { return Stream.pipeline(body, res, (err) => {}) } | ||
| if (body instanceof Response) { return Stream.pipeline(body?.body || '', res, (err) => {}) } |
There was a problem hiding this comment.
Using an empty string as fallback for body?.body may cause issues when passed to Stream.pipeline, which expects a stream-like object. Consider using a proper stream or handling the null case differently.
| if (body instanceof Response) { return Stream.pipeline(body?.body || '', res, (err) => {}) } | |
| if (body instanceof Response) { | |
| if (body.body) { | |
| return Stream.pipeline(body.body, res, (err) => {}) | |
| } else { | |
| return res.end() | |
| } | |
| } |
|
Duplicate to #1893 |
## Description - closes koajs#1834 - closes koajs#1882 - closes koajs#1889 ## Checklist - [x] I have ensured my pull request is not behind the main or master branch of the original repository. - [x] I have rebased all commits where necessary so that reviewing this pull request can be done without having to merge it first. - [x] I have written a commit message that passes commitlint linting. - [x] I have ensured that my code changes pass linting tests. - [x] I have ensured that my code changes pass unit tests. - [x] I have described my pull request and the reasons for code changes along with context if necessary. ## Checklist for 1834 (before re-pinging team) - [x] Update stackblitz - [stackblitz demostrating error and fix](https://stackblitz.com/edit/koa-patch-starter-pwupm4kr?file=README.md,index.js,patch%2Fkoa%2Flib%2Fapplication.js,vendor%2Fkoa%2Flib%2Fapplication.js) - [x] Address comments - [x] Re-review after merging 2 issues together
Checklist
pipe(rs, res) will not destroy res, when an error occurs. Thus causing client "hang" infinite when it has received some part of data.
Following code example, a readable stream has been implemented. When client request, the server will send some data, but after the error occurs, the client hang, the server doesn't close the socket. Chrome will hang. curl and ab will end in timeout.
After replacing with pipeline, when error occur, koa immediately close the socket, Chrome will say "Check internet connection" in download page. curl and ab will finish request as soon as possible.
stream.pipeline() will call stream.destroy(err) on all streams except they have already been closed.
the behavior may be better than hanging the request. Instead of client waitting indefinitely for the remaining bytes, closing the connection will break the expectation, but it's the only way to signal that the response is incomplete. The client will know that the response is truncated. Headers are already sent, an error occurs mid-stream, the client could handle this predictably.
Here's another to consider:
It's koa duty to close connection, it's app writer's duty to close the readable stream, and it's (better) stream implementers duty to release corresponding resources(request socket, fd, etc) of the readable stream when error occurs(but if it didn't work well, app writer should handle it, not koa).
Last benifit is pipeline() added support for webstreams(v18.16.0), making it easier.