Skip to content

Commit

Permalink
fix(lambda-at-edge): fix SSG fallback with basepath (#992)
Browse files Browse the repository at this point in the history
  • Loading branch information
jvarho committed Apr 15, 2021
1 parent 72b3c69 commit dff598b
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 21 deletions.
34 changes: 17 additions & 17 deletions packages/libs/lambda-at-edge/src/default-handler.ts
Expand Up @@ -495,10 +495,12 @@ const handleOriginRequest = async ({
// 1. URI routes to _error.js
// 2. URI is not unmatched, but it's not in prerendered routes nor is for an SSG fallback, i.e this is an SSR data request, we need to SSR render the JSON
break S3Check;
} else {
// Otherwise, this is an SSG data request, so continue to get to try to get the JSON from S3.
// For fallback SSG, this will fail the first time but the origin response handler will render and store in S3.
s3Origin.path = basePath;
request.uri = uri;
}

// Otherwise, this is an SSG data request, so continue to get to try to get the JSON from S3.
// For fallback SSG, this will fail the first time but the origin response handler will render and store in S3.
}

addS3HostHeader(request, normalisedS3DomainName);
Expand Down Expand Up @@ -582,10 +584,11 @@ const handleOriginResponse = async ({
}) => {
const response = event.Records[0].cf.response;
const request = event.Records[0].cf.request;
const { uri } = request;
const { status } = response;
if (status !== "403") {
// Set 404 status code for 404.html page. We do not need normalised URI as it will always be "/404.html"
if (request.uri === "/404.html") {
if (uri === "/404.html") {
response.status = "404";
response.statusDescription = "Not Found";
}
Expand All @@ -597,7 +600,6 @@ const handleOriginResponse = async ({
return response;
}

const uri = normaliseUri(request.uri, routesManifest);
const { domainName, region } = request.origin!.s3!;
const bucketName = domainName.replace(`.s3.${region}.amazonaws.com`, "");

Expand All @@ -609,6 +611,7 @@ const handleOriginResponse = async ({
maxAttempts: 3,
retryStrategy: await buildS3RetryStrategy()
});
const s3BasePath = basePath ? `${basePath.replace(/^\//, "")}/` : "";
let pagePath;
if (
isDataRequest(uri) &&
Expand All @@ -629,23 +632,20 @@ const handleOriginResponse = async ({
"passthrough"
);
if (isSSG) {
const jsonKey = uri.replace(/^\//, "");
const htmlKey = uri
.replace(/^\/?_next\/data/, "static-pages")
.replace(/.json$/, ".html");
const s3JsonParams = {
Bucket: bucketName,
Key: `${basePath}${basePath === "" ? "" : "/"}${uri.replace(
/^\//,
""
)}`,
Key: `${s3BasePath}${jsonKey}`,
Body: JSON.stringify(renderOpts.pageData),
ContentType: "application/json",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
};
const s3HtmlParams = {
Bucket: bucketName,
Key: `${basePath}${basePath === "" ? "" : "/"}static-pages/${
manifest.buildId
}/${request.uri
.replace(`/_next/data/${manifest.buildId}/`, "")
.replace(".json", ".html")}`,
Key: `${s3BasePath}${htmlKey}`,
Body: html,
ContentType: "text/html",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
Expand All @@ -667,9 +667,9 @@ const handleOriginResponse = async ({
if (!hasFallback) return response;

// If route has fallback, return that page from S3, otherwise return 404 page
const s3Key = `${basePath}${basePath === "" ? "" : "/"}static-pages/${
manifest.buildId
}${hasFallback.fallback || "/404.html"}`;
const s3Key = `${s3BasePath}static-pages/${manifest.buildId}${
hasFallback.fallback || "/404.html"
}`;

const { GetObjectCommand } = await import(
"@aws-sdk/client-s3/commands/GetObjectCommand"
Expand Down
@@ -0,0 +1,223 @@
import { handler } from "../../src/default-handler";
import { createCloudFrontEvent } from "../test-utils";
import {
CloudFrontResultResponse,
CloudFrontHeaders,
CloudFrontResponse
} from "aws-lambda";
import { S3Client } from "@aws-sdk/client-s3/S3Client";

jest.mock("@aws-sdk/client-s3/S3Client", () =>
require("../mocks/s3/aws-sdk-s3-client.mock")
);

jest.mock("@aws-sdk/client-s3/commands/GetObjectCommand", () =>
require("../mocks/s3/aws-sdk-s3-client-get-object-command.mock")
);

jest.mock("@aws-sdk/client-s3/commands/PutObjectCommand", () =>
require("../mocks/s3/aws-sdk-s3-client-put-object-command.mock")
);

jest.mock(
"../../src/manifest.json",
() => require("./default-build-manifest.json"),
{
virtual: true
}
);

jest.mock(
"../../src/prerender-manifest.json",
() => require("./prerender-manifest.json"),
{
virtual: true
}
);

jest.mock(
"../../src/routes-manifest.json",
() => require("./default-basepath-routes-manifest.json"),
{
virtual: true
}
);

const mockPageRequire = (mockPagePath: string): void => {
jest.mock(
`../../src/${mockPagePath}`,
() => require(`../shared-fixtures/built-artifact/${mockPagePath}`),
{
virtual: true
}
);
};

describe("Lambda@Edge origin response", () => {
let s3Client: S3Client;
beforeEach(() => {
s3Client = new S3Client({});
});
describe("Fallback pages", () => {
it("serves fallback page from S3", async () => {
const event = createCloudFrontEvent({
uri: "/tests/prerender-manifest-fallback/not-yet-built",
host: "mydistribution.cloudfront.net",
config: { eventType: "origin-response" } as any,
response: {
status: "403"
} as any
});

const result = await handler(event);
const response = result as CloudFrontResponse;

expect(s3Client.send).toHaveBeenCalledWith({
Command: "GetObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key:
"basepath/static-pages/build-id/tests/prerender-manifest-fallback/[fallback].html"
});

expect(response).toEqual({
status: "200",
statusDescription: "OK",
headers: {
"cache-control": [
{
key: "Cache-Control",
value: "public, max-age=0, s-maxage=0, must-revalidate" // Fallback page shouldn't be cached as it will override the path for a just generated SSG page.
}
],
"content-type": [
{
key: "Content-Type",
value: "text/html"
}
]
},
body: "S3Body"
});
});

it("serves 404 page from S3 for fallback: false", async () => {
const event = createCloudFrontEvent({
uri: "/tests/prerender-manifest/[staticPageName]",
host: "mydistribution.cloudfront.net",
config: { eventType: "origin-response" } as any,
response: {
status: "403"
} as any
});

const result = await handler(event);
const response = result as CloudFrontResponse;

expect(s3Client.send).toHaveBeenCalledWith({
Command: "GetObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key: "basepath/static-pages/build-id/404.html"
});

expect(response).toEqual({
status: "404",
statusDescription: "Not Found",
headers: {
"cache-control": [
{
key: "Cache-Control",
value: "public, max-age=0, s-maxage=2678400, must-revalidate"
}
],
"content-type": [
{
key: "Content-Type",
value: "text/html"
}
]
},
body: "S3Body"
});
});

it("renders and uploads HTML and JSON for fallback SSG data requests", async () => {
const event = createCloudFrontEvent({
uri: "/_next/data/build-id/fallback/not-yet-built.json",
host: "mydistribution.cloudfront.net",
config: { eventType: "origin-response" } as any,
response: {
headers: {},
status: "403"
} as any
});

mockPageRequire("pages/fallback/[slug].js");

const response = await handler(event);

const cfResponse = response as CloudFrontResultResponse;
const decodedBody = Buffer.from(
cfResponse.body as string,
"base64"
).toString("utf8");

const headers = response.headers as CloudFrontHeaders;
expect(headers["content-type"][0].value).toEqual("application/json");
expect(JSON.parse(decodedBody)).toEqual({
page: "pages/fallback/[slug].js"
});
expect(cfResponse.status).toEqual(200);

expect(s3Client.send).toHaveBeenNthCalledWith(1, {
Command: "PutObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key: "basepath/_next/data/build-id/fallback/not-yet-built.json",
Body: JSON.stringify({
page: "pages/fallback/[slug].js"
}),
ContentType: "application/json",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
});
expect(s3Client.send).toHaveBeenNthCalledWith(2, {
Command: "PutObjectCommand",
Bucket: "my-bucket.s3.amazonaws.com",
Key: "basepath/static-pages/build-id/fallback/not-yet-built.html",
Body: "<div>Rendered Page</div>",
ContentType: "text/html",
CacheControl: "public, max-age=0, s-maxage=2678400, must-revalidate"
});
});
});

describe("SSR data requests", () => {
it("does not upload to S3", async () => {
const event = createCloudFrontEvent({
uri: "/_next/data/build-id/customers/index.json",
host: "mydistribution.cloudfront.net",
config: { eventType: "origin-response" } as any,
response: {
headers: {},
status: "403"
} as any
});

mockPageRequire("pages/customers/[customer].js");

const response = await handler(event);

const cfResponse = response as CloudFrontResultResponse;
const decodedBody = Buffer.from(
cfResponse.body as string,
"base64"
).toString("utf8");

const headers = response.headers as CloudFrontHeaders;
expect(headers["content-type"][0].value).toEqual("application/json");
expect(JSON.parse(decodedBody)).toEqual({
page: "pages/customers/[customer].js"
});
expect(cfResponse.status).toEqual(200);
expect(s3Client.send).not.toHaveBeenCalled();
});
});
});
Expand Up @@ -413,11 +413,11 @@ describe("Lambda@Edge", () => {
s3: {
authMethod: "origin-access-identity",
domainName: "my-bucket.s3.amazonaws.com",
path: "",
path: "/basepath",
region: "us-east-1"
}
});
expect(request.uri).toEqual(path);
expect(request.uri).toEqual(path.slice(9));
}
);

Expand Down
Expand Up @@ -125,6 +125,8 @@ describe("Lambda@Edge", () => {
${"/basepath/users/test/catch/all"} | ${"/users/[...user].html"}
${"/basepath/john/123"} | ${"/[username]/[id].html"}
${"/basepath/tests/prerender-manifest/example-static-page"} | ${"/tests/prerender-manifest/example-static-page.html"}
${"/basepath/tests/prerender-manifest-fallback/not-built"} | ${"/tests/prerender-manifest-fallback/not-built.html"}
${"/basepath/preview"} | ${"/preview.html"}
`(
"serves page $expectedPage from S3 for path $path",
async ({ path, expectedPage }) => {
Expand Down Expand Up @@ -386,11 +388,11 @@ describe("Lambda@Edge", () => {
s3: {
authMethod: "origin-access-identity",
domainName: "my-bucket.s3.amazonaws.com",
path: "",
path: "/basepath",
region: "us-east-1"
}
});
expect(request.uri).toEqual(path);
expect(request.uri).toEqual(path.slice(9));
}
);

Expand Down

0 comments on commit dff598b

Please sign in to comment.