Skip to content
This repository was archived by the owner on Jan 28, 2025. It is now read-only.

Commit 1824e60

Browse files
authored
feat (core, lambda-at-edge): not found support in SSG fallback (#1366)
1 parent 3f75647 commit 1824e60

File tree

19 files changed

+245
-72
lines changed

19 files changed

+245
-72
lines changed

packages/compat-layers/lambda-at-edge-compat/__tests__/next-aws-cloudfront.response.test.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,36 @@ describe("Response Tests", () => {
119119
});
120120
});
121121

122-
it("writeHead preserves existing special CloudFront Headers", () => {
122+
it("writeHead preserves existing Headers", () => {
123+
expect.assertions(1);
124+
125+
const cloudFrontReadOnlyHeaders = {
126+
"Content-Length": "1234",
127+
"x-custom-1": "1"
128+
};
129+
130+
const { res, responsePromise } = create({
131+
request: {
132+
uri: "/",
133+
headers: {}
134+
},
135+
response: {
136+
headers: cloudFrontReadOnlyHeaders
137+
}
138+
});
139+
140+
res.writeHead(200, {});
141+
res.end();
142+
143+
return responsePromise.then((response) => {
144+
expect(response.headers).toEqual({
145+
"content-length": "1234",
146+
"x-custom-1": "1"
147+
});
148+
});
149+
});
150+
151+
it("writeHead does not overwrite special CloudFront Headers", () => {
123152
expect.assertions(1);
124153

125154
const cloudFrontReadOnlyHeaders = {

packages/compat-layers/lambda-at-edge-compat/next-aws-cloudfront.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,16 @@ const HttpStatusCodes = {
9292

9393
const toCloudFrontHeaders = (headers, headerNames, originalHeaders) => {
9494
const result = {};
95-
const lowerCaseOriginalHeaders = {};
96-
Object.entries(originalHeaders).forEach(([header, value]) => {
97-
lowerCaseOriginalHeaders[header.toLowerCase()] = value;
95+
96+
Object.entries(originalHeaders).forEach(([headerName, headerValue]) => {
97+
result[headerName.toLowerCase()] = headerValue;
9898
});
9999

100100
Object.entries(headers).forEach(([headerName, headerValue]) => {
101101
const headerKey = headerName.toLowerCase();
102102
headerName = headerNames[headerKey] || headerName;
103103

104104
if (readOnlyCloudFrontHeaders[headerKey]) {
105-
if (lowerCaseOriginalHeaders[headerKey]) {
106-
result[headerKey] = lowerCaseOriginalHeaders[headerKey];
107-
}
108105
return;
109106
}
110107

@@ -217,6 +214,7 @@ const handler = (
217214
const headerNames = {};
218215
res.writeHead = (status, headers) => {
219216
response.status = status;
217+
response.statusDescription = HttpStatusCodes[status];
220218

221219
if (headers) {
222220
res.headers = Object.assign(res.headers, headers);

packages/e2e-tests/next-app-dynamic-routes/cypress/integration/pages.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ describe("Pages Tests", () => {
348348
prerendered: true
349349
},
350350
{
351-
path: "/optional-catch-all-ssg-with-fallback/not-found",
351+
path: "/optional-catch-all-ssg-with-fallback/not-prerendered",
352352
param: "",
353353
prerendered: false
354354
}
@@ -450,6 +450,49 @@ describe("Pages Tests", () => {
450450
// However, verified manually that it works correctly.
451451
});
452452
});
453+
454+
[{ path: "/optional-catch-all-ssg-with-fallback/not-found" }].forEach(
455+
({ path }) => {
456+
["HEAD", "GET"].forEach((method) => {
457+
it(`allows HTTP method for path ${path}: ${method} and returns 200 status`, () => {
458+
cy.request({
459+
url: path,
460+
method: method,
461+
failOnStatusCode: false
462+
}).then((response) => {
463+
expect(response.status).to.equal(200);
464+
});
465+
});
466+
});
467+
468+
["DELETE", "POST", "OPTIONS", "PUT", "PATCH"].forEach((method) => {
469+
it(`disallows HTTP method for path ${path} with 4xx status code: ${method}`, () => {
470+
cy.request({
471+
url: path,
472+
method: method,
473+
failOnStatusCode: false
474+
}).then((response) => {
475+
expect(response.status).to.be.gte(400);
476+
});
477+
});
478+
});
479+
480+
it(`returns 404 on data request for ${path}`, () => {
481+
const fullPath = `/_next/data/${buildId}${path.replace(
482+
/\/$/,
483+
"/index"
484+
)}.json`;
485+
486+
cy.request({
487+
url: fullPath,
488+
method: "GET",
489+
failOnStatusCode: false
490+
}).then((response) => {
491+
expect(response.status).to.equal(404);
492+
});
493+
});
494+
}
495+
);
453496
});
454497

455498
describe("Dynamic SSR page", () => {

packages/e2e-tests/next-app-dynamic-routes/pages/optional-catch-all-ssg-with-fallback/[[...catch]].tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react";
2-
import { GetServerSidePropsContext } from "next";
2+
import { GetServerSidePropsContext, GetStaticPropsResult } from "next";
33

44
type OptionalCatchAllPageProps = {
55
name: string;
@@ -22,9 +22,15 @@ export default function OptionalCatchAllPage(
2222

2323
export async function getStaticProps(
2424
ctx: GetServerSidePropsContext
25-
): Promise<{ props: OptionalCatchAllPageProps }> {
25+
): Promise<GetStaticPropsResult<OptionalCatchAllPageProps>> {
2626
const catchAll = ((ctx.params?.catch as string[]) ?? []).join("/");
2727

28+
if (catchAll === "not-found") {
29+
return {
30+
notFound: true
31+
};
32+
}
33+
2834
return {
2935
props: { name: "serverless-next.js", catch: catchAll }
3036
};

packages/libs/core/src/build/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export const prepareBuildManifests = async (
4646
},
4747
ssg: {
4848
dynamic: {},
49-
nonDynamic: {}
49+
nonDynamic: {},
50+
notFound: {}
5051
}
5152
},
5253
publicFiles: {},
@@ -138,6 +139,13 @@ export const prepareBuildManifests = async (
138139
}
139140
);
140141

142+
// Add not found SSG routes
143+
const notFound: { [key: string]: true } = {};
144+
(prerenderManifest.notFoundRoutes ?? []).forEach((route) => {
145+
notFound[route] = true;
146+
});
147+
ssgPages.notFound = notFound;
148+
141149
// Include only SSR routes that are in runtime use
142150
const ssrPages = (pageManifest.pages.ssr = usedSSR(
143151
pageManifest,

packages/libs/core/src/handle/fallback.ts

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ type Fallback = {
2323
renderOpts: any;
2424
};
2525

26+
const renderNotFound = async (
27+
event: Event,
28+
manifest: PageManifest,
29+
routesManifest: RoutesManifest,
30+
getPage: (page: string) => any
31+
): Promise<StaticRoute | void> => {
32+
const route = notFoundPage(event.req.url ?? "", manifest, routesManifest);
33+
if (route.isStatic) {
34+
return route as StaticRoute;
35+
}
36+
37+
return await renderRoute(event, route, manifest, routesManifest, getPage);
38+
};
39+
2640
const renderFallback = async (
2741
event: Event,
2842
route: FallbackRoute,
@@ -40,6 +54,17 @@ const renderFallback = async (
4054
res,
4155
"passthrough"
4256
);
57+
58+
if (renderOpts.isNotFound) {
59+
if (route.isData) {
60+
res.setHeader("Content-Type", "application/json");
61+
res.statusCode = 404;
62+
res.end(JSON.stringify({ notFound: true }));
63+
return;
64+
}
65+
return renderNotFound(event, manifest, routesManifest, getPage);
66+
}
67+
4368
return { isStatic: false, route, html, renderOpts };
4469
} catch (error) {
4570
return renderErrorPage(
@@ -94,14 +119,5 @@ export const handleFallback = async (
94119
}
95120
}
96121

97-
const errorRoute = notFoundPage(
98-
event.req.url ?? "",
99-
manifest,
100-
routesManifest
101-
);
102-
if (errorRoute.isStatic) {
103-
return errorRoute as StaticRoute;
104-
}
105-
106-
return renderRoute(event, errorRoute, manifest, routesManifest, getPage);
122+
return await renderNotFound(event, manifest, routesManifest, getPage);
107123
};

packages/libs/core/src/route/data.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export const handleDataReq = (
5353
revalidate: ssg.initialRevalidateSeconds
5454
};
5555
}
56+
if ((pages.ssg.notFound ?? {})[localeUri] && !isPreview) {
57+
return notFoundData(uri, manifest, routesManifest);
58+
}
5659
if (pages.ssr.nonDynamic[localeUri]) {
5760
return {
5861
isData: true,

packages/libs/core/src/route/page.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export const handlePageReq = (
5656
statusCode
5757
};
5858
}
59+
if ((pages.ssg.notFound ?? {})[localeUri] && !isPreview) {
60+
return notFoundPage(uri, manifest, routesManifest);
61+
}
5962
if (pages.ssr.nonDynamic[localeUri]) {
6063
return {
6164
isData: false,

packages/libs/core/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ export type PageManifest = Manifest & {
9393
nonDynamic: {
9494
[key: string]: NonDynamicSSG;
9595
};
96+
notFound?: {
97+
[key: string]: true;
98+
};
9699
};
97100
ssr: {
98101
dynamic: { [key: string]: string };

packages/libs/core/tests/handle/default.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe("Default handler", () => {
2525
beforeAll(async () => {
2626
prerenderManifest = {
2727
version: 3,
28-
notFoundRoutes: [],
28+
notFoundRoutes: ["/fallback/404"],
2929
routes: {
3030
"/ssg": {
3131
initialRevalidateSeconds: false,
@@ -262,6 +262,7 @@ describe("Default handler", () => {
262262
${"/foo"} | ${"pages/[root].html"}
263263
${"/html/bar"} | ${"pages/html/[page].html"}
264264
${"/fallback/new"} | ${"pages/fallback/new.html"}
265+
${"/fallback/404"} | ${"pages/404.html"}
265266
${"/rewrite-path"} | ${"pages/[root].html"}
266267
`("Routes static page $uri to file $file", async ({ uri, file }) => {
267268
const route = await handleDefault(
@@ -282,6 +283,7 @@ describe("Default handler", () => {
282283
it.each`
283284
uri | file
284285
${"/_next/data/test-build-id/fallback/new.json"} | ${"/_next/data/test-build-id/fallback/new.json"}
286+
${"/_next/data/test-build-id/fallback/404.json"} | ${"pages/404.html"}
285287
${"/_next/data/test-build-id/not-found.json"} | ${"pages/404.html"}
286288
${"/_next/data/not-build-id/fallback/new.json"} | ${"pages/404.html"}
287289
`("Routes static data route $uri to file $file", async ({ uri, file }) => {

0 commit comments

Comments
 (0)