Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const noop = () => {};
* @property {boolean=} lastModified options to generate last modified header
* @property {(boolean | number | string | { maxAge?: number, immutable?: boolean })=} cacheControl options to generate cache headers
* @property {boolean=} cacheImmutable is cache immutable
* @property {boolean=} forwardError forward error to next middleware
*/

/**
Expand Down
5 changes: 2 additions & 3 deletions src/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,6 @@ function wrapper(context) {
}

const acceptedMethods = context.options.methods || ["GET", "HEAD"];
// TODO do we need an option here?
const forwardError = false;

initState(res);

Expand All @@ -180,13 +178,14 @@ function wrapper(context) {
* @returns {Promise<void>}
*/
async function sendError(message, status, options) {
if (forwardError) {
if (context.options.forwardError) {
const error =
/** @type {Error & { statusCode: number }} */
(new Error(message));
error.statusCode = status;

await goNext(error);
return;
}

const escapeHtml = getEscapeHtml();
Expand Down
5 changes: 5 additions & 0 deletions src/options.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
},
"additionalProperties": false
Expand Down
241 changes: 241 additions & 0 deletions test/middleware.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ async function frameworkFactory(
}
}

if (options.errorMiddleware) {
app.use(options.errorMiddleware);
}

const server = await startServer(name, app);
const req = request(server);

Expand All @@ -147,6 +151,10 @@ async function frameworkFactory(
}
}

if (options.errorMiddleware) {
app.use(options.errorMiddleware);
}

return [server, req, instance.devMiddleware];
}
default: {
Expand All @@ -172,6 +180,10 @@ async function frameworkFactory(
}
}

if (options.errorMiddleware) {
app.use(options.errorMiddleware);
}

if (isFastify) {
await app.ready();
}
Expand Down Expand Up @@ -3610,6 +3622,235 @@ describe.each([
});
});

describe("should call the next middleware for finished or errored requests by default", () => {
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,
},
{
errorMiddleware: () => {
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") {
// Middleware de error para Koa: (err, ctx, next)
nextWasCalled = true;
} else if (name === "hono") {
// Middleware de error para Hono: (err, c, next)
nextWasCalled = true;
} else {
// Middleware de error para Express, Connect, Fastify, Router: (err, req, res, next)
nextWasCalled = true;
}
},
},
);

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);
};
});

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 not allow to get files above root", async () => {
await req.get("/public/..%2f../middleware.test.js");

// expect(response.statusCode).toBe(403);
// expect(response.headers["content-type"]).toBe(
// "text/html; charset=utf-8",
// );
// expect(response.text).toBe(`<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="utf-8">
// <title>Error</title>
// </head>
// <body>
// <pre>Forbidden</pre>
// </body>
// </html>`);
expect(nextWasCalled).toBe(true);
});

it('should return the "412" code for the "GET" request to the bundle file with etag and wrong "if-match" header', async () => {
await req.get("/file.text");

// expect(response1.statusCode).toBe(200);
// expect(response1.headers.etag).toBeDefined();
// expect(response1.headers.etag.startsWith("W/")).toBe(true);

await req.get("/file.text").set("if-match", "test");

// expect(response2.statusCode).toBe(412);
expect(nextWasCalled).toBe(true);
});

it('should return the "416" code for the "GET" request with the invalid range header', async () => {
await req.get("/file.text").set("Range", "bytes=9999999-");

// expect(response.statusCode).toBe(416);
// expect(response.headers["content-type"]).toBe(
// "text/html; charset=utf-8",
// );
// expect(response.text).toBe(
// `<!DOCTYPE html>
// <html lang="en">
// <head>
// <meta charset="utf-8">
// <title>Error</title>
// </head>
// <body>
// <pre>Range Not Satisfiable</pre>
// </body>
// </html>`,
// );
expect(nextWasCalled).toBe(true);
});

it('should return the "404" code for the "GET" request to the "image.svg" file when it throws a reading error', async () => {
await req.get("/image.svg");

// expect(response.statusCode).toBe(404);
// expect(response.headers["content-type"]).toBe(
// "text/html; charset=utf-8",
// );
// expect(response.text).toEqual(
// "<!DOCTYPE html>\n" +
// '<html lang="en">\n' +
// "<head>\n" +
// '<meta charset="utf-8">\n' +
// "<title>Error</title>\n" +
// "</head>\n" +
// "<body>\n" +
// "<pre>Not Found</pre>\n" +
// "</body>\n" +
// "</html>",
// );
expect(nextWasCalled).toBe(true);
});

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;

Expand Down
Loading