-
-
Notifications
You must be signed in to change notification settings - Fork 382
feat: add forwardError option to enable error forwarding to next middleware
#2259
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
Changes from all commits
29335cb
a813442
ef885d0
67e67c1
afe1f82
441a9ca
ba17cf0
215a71c
829ba0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,6 +172,11 @@ | |
| "description": "Enable or disable setting `Cache-Control: public, max-age=31536000, immutable` response header for immutable assets (i.e. asset with a hash in file name like `image.a4c12bde.jpg`).", | ||
| "link": "https://github.com/webpack/webpack-dev-middleware#cacheimmutable", | ||
| "type": "boolean" | ||
| }, | ||
| "forwardError": { | ||
| "description": "Enable or disable forwarding errors to next middleware.", | ||
| "link": "https://github.com/webpack/webpack-dev-middleware#forwarderrors", | ||
| "type": "boolean" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add this to our README too
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep! |
||
| } | ||
| }, | ||
| "additionalProperties": false | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -146,7 +146,6 @@ async function frameworkFactory( | |
| app.use(item); | ||
| } | ||
| } | ||
|
|
||
| return [server, req, instance.devMiddleware]; | ||
| } | ||
| default: { | ||
|
|
@@ -3610,6 +3609,235 @@ describe.each([ | |
| }); | ||
| }); | ||
|
|
||
| describe("should call the next middleware for finished or errored requests when forwardError is enabled", () => { | ||
| let compiler; | ||
|
|
||
| const outputPath = path.resolve( | ||
| __dirname, | ||
| "./outputs/basic-test-errors-headers-sent", | ||
| ); | ||
|
|
||
| let nextWasCalled = false; | ||
|
|
||
| beforeAll(async () => { | ||
| compiler = getCompiler({ | ||
| ...webpackConfig, | ||
| output: { | ||
| filename: "bundle.js", | ||
| path: outputPath, | ||
| }, | ||
| }); | ||
|
|
||
| [server, req, instance] = await frameworkFactory( | ||
| name, | ||
| framework, | ||
| compiler, | ||
| { | ||
| etag: "weak", | ||
| lastModified: true, | ||
| forwardError: true, | ||
| }, | ||
| { | ||
| setupMiddlewares: (middlewares) => { | ||
| if (name === "hapi") { | ||
| // There's no such thing as "the next route handler" in hapi. One request is matched to one or no route handlers. | ||
| } else if (name === "koa") { | ||
| middlewares.push(async (ctx) => { | ||
| nextWasCalled = true; | ||
| ctx.status = 500; | ||
| ctx.body = "error"; | ||
| }); | ||
| } else if (name === "hono") { | ||
| middlewares.push(async (ctx) => { | ||
| nextWasCalled = true; | ||
| ctx.status(500); | ||
| return ctx.text("error"); | ||
| }); | ||
| } else { | ||
| middlewares.push((_error, _req, res, _next) => { | ||
| nextWasCalled = true; | ||
| res.statusCode = 500; | ||
| res.end("error"); | ||
| }); | ||
| } | ||
|
|
||
| return middlewares; | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| instance.context.outputFileSystem.mkdirSync(outputPath, { | ||
| recursive: true, | ||
| }); | ||
| instance.context.outputFileSystem.writeFileSync( | ||
| path.resolve(outputPath, "index.html"), | ||
| "HTML", | ||
| ); | ||
| instance.context.outputFileSystem.writeFileSync( | ||
| path.resolve(outputPath, "image.svg"), | ||
| "svg image", | ||
| ); | ||
| instance.context.outputFileSystem.writeFileSync( | ||
| path.resolve(outputPath, "file.text"), | ||
| "text", | ||
| ); | ||
|
|
||
| const originalMethod = | ||
| instance.context.outputFileSystem.createReadStream; | ||
|
|
||
| instance.context.outputFileSystem.createReadStream = | ||
| function createReadStream(...args) { | ||
| if (args[0].endsWith("image.svg")) { | ||
| const brokenStream = new this.ReadStream(...args); | ||
|
|
||
| brokenStream._read = function _read() { | ||
| const error = new Error("test"); | ||
| error.code = "ENAMETOOLONG"; | ||
| this.emit("error", error); | ||
| this.end(); | ||
| this.destroy(); | ||
| }; | ||
|
|
||
| return brokenStream; | ||
| } | ||
|
|
||
| return originalMethod(...args); | ||
| }; | ||
| }); | ||
|
|
||
| beforeEach(() => { | ||
| nextWasCalled = false; | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| await close(server, instance); | ||
| }); | ||
|
|
||
| it("should work with piping stream", async () => { | ||
| const response1 = await req.get("/file.text"); | ||
|
|
||
| expect(response1.statusCode).toBe(200); | ||
| expect(nextWasCalled).toBe(false); | ||
| }); | ||
|
|
||
| it('should return the "500" code for requests above root', async () => { | ||
| const response = await req.get("/public/..%2f../middleware.test.js"); | ||
|
|
||
| expect(response.statusCode).toBe(500); | ||
| if (name !== "hapi") { | ||
| expect(response.text).toBe("error"); | ||
| expect(nextWasCalled).toBe(true); | ||
| } else { | ||
| expect(nextWasCalled).toBe(false); | ||
| } | ||
| }); | ||
|
|
||
| it('should return the "500" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => { | ||
| const response1 = await req.get("/file.text"); | ||
|
|
||
| expect(response1.statusCode).toBe(200); | ||
| expect(response1.headers.etag).toBeDefined(); | ||
| expect(response1.headers.etag.startsWith("W/")).toBe(true); | ||
|
|
||
| const response2 = await req.get("/file.text").set("if-match", "test"); | ||
|
|
||
| expect(response2.statusCode).toBe(500); | ||
| if (name !== "hapi") { | ||
| expect(response2.text).toBe("error"); | ||
| expect(nextWasCalled).toBe(true); | ||
| } else { | ||
| expect(nextWasCalled).toBe(false); | ||
| } | ||
| }); | ||
|
|
||
| it('should return the "500" code for the "GET" request with the invalid range header', async () => { | ||
| const response = await req | ||
| .get("/file.text") | ||
| .set("Range", "bytes=9999999-"); | ||
|
|
||
| expect(response.statusCode).toBe(500); | ||
| if (name !== "hapi") { | ||
| expect(response.text).toBe("error"); | ||
| expect(nextWasCalled).toBe(true); | ||
| } else { | ||
| expect(nextWasCalled).toBe(false); | ||
| } | ||
| }); | ||
|
|
||
| // TODO: why koa and hono don't catch for their error handling when stream emit error? | ||
| (name === "koa" || name === "hono" ? it.skip : it)( | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The behavior here in Hono and Koa is a bit strange, and it has to do with how they handle streams when sending the response. If an error is thrown, it’s not really captured, in fact, Hono would treat it as if
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I know it, but let's resolve it later, not critical for us
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So do you want me to resolve it in another PR? |
||
| 'should return the "500" code for the "GET" request to the "image.svg" file when it throws a reading error', | ||
| async () => { | ||
| const response = await req.get("/image.svg"); | ||
|
|
||
| // eslint-disable-next-line jest/no-standalone-expect | ||
| expect(response.statusCode).toBe(500); | ||
| if (name !== "hapi") { | ||
| // eslint-disable-next-line jest/no-standalone-expect | ||
| expect(nextWasCalled).toBe(true); | ||
| } else { | ||
| // eslint-disable-next-line jest/no-standalone-expect | ||
| expect(nextWasCalled).toBe(false); | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| it('should return the "200" code for the "HEAD" request to the bundle file', async () => { | ||
| const response = await req.head("/file.text"); | ||
|
|
||
| expect(response.statusCode).toBe(200); | ||
| expect(response.text).toBeUndefined(); | ||
| expect(nextWasCalled).toBe(false); | ||
| }); | ||
|
|
||
| it('should return the "304" code for the "GET" request to the bundle file with etag and "if-none-match"', async () => { | ||
| const response1 = await req.get("/file.text"); | ||
|
|
||
| expect(response1.statusCode).toBe(200); | ||
| expect(response1.headers.etag).toBeDefined(); | ||
| expect(response1.headers.etag.startsWith("W/")).toBe(true); | ||
|
|
||
| const response2 = await req | ||
| .get("/file.text") | ||
| .set("if-none-match", response1.headers.etag); | ||
|
|
||
| expect(response2.statusCode).toBe(304); | ||
| expect(response2.headers.etag).toBeDefined(); | ||
| expect(response2.headers.etag.startsWith("W/")).toBe(true); | ||
|
|
||
| const response3 = await req | ||
| .get("/file.text") | ||
| .set("if-none-match", response1.headers.etag); | ||
|
|
||
| expect(response3.statusCode).toBe(304); | ||
| expect(response3.headers.etag).toBeDefined(); | ||
| expect(response3.headers.etag.startsWith("W/")).toBe(true); | ||
| expect(nextWasCalled).toBe(false); | ||
| }); | ||
|
|
||
| it('should return the "304" code for the "GET" request to the bundle file with lastModified and "if-modified-since" header', async () => { | ||
| const response1 = await req.get("/file.text"); | ||
|
|
||
| expect(response1.statusCode).toBe(200); | ||
| expect(response1.headers["last-modified"]).toBeDefined(); | ||
|
|
||
| const response2 = await req | ||
| .get("/file.text") | ||
| .set("if-modified-since", response1.headers["last-modified"]); | ||
|
|
||
| expect(response2.statusCode).toBe(304); | ||
| expect(response2.headers["last-modified"]).toBeDefined(); | ||
|
|
||
| const response3 = await req | ||
| .get("/file.text") | ||
| .set("if-modified-since", response2.headers["last-modified"]); | ||
|
|
||
| expect(response3.statusCode).toBe(304); | ||
| expect(response3.headers["last-modified"]).toBeDefined(); | ||
| expect(nextWasCalled).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe("should fallthrough for not found files", () => { | ||
| let compiler; | ||
|
|
||
|
|
||
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's partially true, there are certain things that don’t work, but I think it’s better to say it doesn’t work first, and then once we know it works correctly, say that it does.