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

Commit b7fe7a9

Browse files
authored
feat(nextjs-component): experimental - allow serving API pages from default lambda (#1632)
1 parent 801c391 commit b7fe7a9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+8119
-19
lines changed

.github/workflows/e2e-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ jobs:
116116
app:
117117
# Current minor version of Next.js
118118
- next-app
119+
- next-app-experimental
119120
- next-app-using-serverless-trace
120121
- next-app-with-trailing-slash
121122
- next-app-with-base-path
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
cypress/videos
2+
cypress/screenshots
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"baseUrl": "http://localhost:3000",
3+
"supportFile": "cypress/support/index.ts",
4+
"responseTimeout": 15000,
5+
"requestTimeout": 15000,
6+
"experimentalFetchPolyfill": true,
7+
"retries": 4,
8+
"video": false
9+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
describe("API Routes Tests", () => {
2+
before(() => {
3+
cy.ensureAllRoutesNotErrored();
4+
});
5+
6+
describe("Basic API", () => {
7+
const path = "/api/basic-api";
8+
9+
["DELETE", "POST", "GET", "PUT", "PATCH", "OPTIONS", "HEAD"].forEach(
10+
(method) => {
11+
it(`serves API request for path ${path} and method ${method}`, () => {
12+
cy.request({ url: path, method: method }).then((response) => {
13+
expect(response.status).to.equal(200);
14+
cy.verifyResponseCacheStatus(response, false);
15+
16+
if (method === "HEAD") {
17+
expect(response.body).to.be.empty;
18+
} else {
19+
expect(response.body).to.deep.equal({
20+
name: "This is a basic API route.",
21+
method: method
22+
});
23+
}
24+
});
25+
});
26+
}
27+
);
28+
});
29+
30+
describe("Dynamic + Nested API", () => {
31+
const base = "api/nested/";
32+
33+
["DELETE", "POST", "GET", "PUT", "PATCH", "OPTIONS", "HEAD"].forEach(
34+
(method) => {
35+
const id = "1";
36+
const path = base + id;
37+
38+
it(`serves API request for path ${path} and method ${method}`, () => {
39+
cy.request({ url: path, method: method }).then((response) => {
40+
expect(response.status).to.equal(200);
41+
cy.verifyResponseCacheStatus(response, false);
42+
43+
if (method === "HEAD") {
44+
expect(response.body).to.be.empty;
45+
} else {
46+
expect(response.body).to.deep.equal({
47+
id: id,
48+
name: `User ${id}`,
49+
method: method
50+
});
51+
}
52+
});
53+
});
54+
}
55+
);
56+
57+
["1", "2", "3", "4", "5"].forEach((id) => {
58+
const path = base + id;
59+
it(`serves API request for path ${path} for different IDs`, () => {
60+
cy.request({ url: path, method: "GET" }).then((response) => {
61+
expect(response.status).to.equal(200);
62+
expect(response.body).to.deep.equal({
63+
id: id,
64+
name: `User ${id}`,
65+
method: "GET"
66+
});
67+
cy.verifyResponseCacheStatus(response, false);
68+
});
69+
});
70+
});
71+
});
72+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
describe("Data Requests", () => {
2+
const buildId = Cypress.env("NEXT_BUILD_ID");
3+
4+
describe("SSG data requests", () => {
5+
[{ path: "/ssg-page.json" }, { path: "/index.json" }].forEach(
6+
({ path }) => {
7+
const fullPath = `/_next/data/${buildId}${path}`;
8+
9+
it(`serves the SSG data request for path ${fullPath}`, () => {
10+
// Hit two times, and check that the response should definitely be cached after 2nd time
11+
for (let i = 0; i < 2; i++) {
12+
cy.request(fullPath).then((response) => {
13+
expect(response.status).to.equal(200);
14+
expect(response.headers["cache-control"]).to.not.be.undefined;
15+
16+
if (i === 1) {
17+
cy.verifyResponseCacheStatus(response, true);
18+
} else {
19+
expect(response.headers["x-cache"]).to.be.oneOf([
20+
"Miss from cloudfront",
21+
"Hit from cloudfront"
22+
]);
23+
}
24+
});
25+
}
26+
});
27+
28+
["HEAD", "GET"].forEach((method) => {
29+
it(`allows HTTP method for path ${fullPath}: ${method}`, () => {
30+
cy.request({ url: fullPath, method: method }).then((response) => {
31+
expect(response.status).to.equal(200);
32+
});
33+
});
34+
});
35+
36+
["DELETE", "POST", "OPTIONS", "PUT", "PATCH"].forEach((method) => {
37+
it(`disallows HTTP method for path ${fullPath} with 4xx error: ${method}`, () => {
38+
cy.request({
39+
url: fullPath,
40+
method: method,
41+
failOnStatusCode: false
42+
}).then((response) => {
43+
expect(response.status).to.be.at.least(400);
44+
expect(response.status).to.be.lessThan(500);
45+
});
46+
});
47+
});
48+
}
49+
);
50+
});
51+
52+
describe("SSR data requests", () => {
53+
[{ path: "/ssr-page-2.json" }].forEach(({ path }) => {
54+
const fullPath = `/_next/data/${buildId}${path}`;
55+
56+
it(`serves the SSR data request for path ${fullPath}`, () => {
57+
// Hit two times, both of which, the response should not be cached
58+
for (let i = 0; i < 2; i++) {
59+
cy.request(fullPath).then((response) => {
60+
expect(response.status).to.equal(200);
61+
cy.verifyResponseCacheStatus(response, false);
62+
expect(response.headers["cache-control"]).to.be.undefined;
63+
});
64+
}
65+
});
66+
67+
["HEAD", "GET"].forEach((method) => {
68+
it(`allows HTTP method for path ${fullPath}: ${method}`, () => {
69+
cy.request({ url: fullPath, method: method }).then((response) => {
70+
expect(response.status).to.equal(200);
71+
});
72+
});
73+
});
74+
75+
["DELETE", "POST", "OPTIONS", "PUT", "PATCH"].forEach((method) => {
76+
it(`disallows HTTP method for path ${fullPath} with 4xx error: ${method}`, () => {
77+
cy.request({
78+
url: fullPath,
79+
method: method,
80+
failOnStatusCode: false
81+
}).then((response) => {
82+
expect(response.status).to.be.at.least(400);
83+
expect(response.status).to.be.lessThan(500);
84+
});
85+
});
86+
});
87+
});
88+
});
89+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
describe("Headers Tests", () => {
2+
describe("Custom headers defined in next.config.js", () => {
3+
[
4+
{
5+
path: "/ssr-page",
6+
expectedHeaders: { "x-custom-header-ssr-page": "custom" }
7+
},
8+
{
9+
path: "/ssg-page",
10+
expectedHeaders: { "x-custom-header-ssg-page": "custom" }
11+
},
12+
{
13+
path: "/",
14+
expectedHeaders: { "x-custom-header-all": "custom" }
15+
},
16+
{
17+
path: "/not-found",
18+
expectedHeaders: { "x-custom-header-all": "custom" }
19+
},
20+
{
21+
path: "/api/basic-api",
22+
expectedHeaders: { "x-custom-header-api": "custom" }
23+
},
24+
{
25+
path: "/app-store-badge.png",
26+
expectedHeaders: { "x-custom-header-public-file": "custom" }
27+
}
28+
].forEach(({ path, expectedHeaders }) => {
29+
it(`add headers ${JSON.stringify(
30+
expectedHeaders
31+
)} for path ${path}`, () => {
32+
cy.request({
33+
url: path,
34+
failOnStatusCode: false
35+
}).then((response) => {
36+
for (const expectedHeader in expectedHeaders) {
37+
expect(response.headers[expectedHeader]).to.equal(
38+
expectedHeaders[expectedHeader]
39+
);
40+
}
41+
});
42+
});
43+
});
44+
});
45+
});
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
describe("Image Optimizer Tests", () => {
2+
describe("image optimization", () => {
3+
[{ contentType: "image/webp" }, { contentType: "image/png" }].forEach(
4+
({ contentType }) => {
5+
it(`serves image app-store-badge.png with content-type: ${contentType}`, () => {
6+
cy.request({
7+
url: "/_next/image?url=%2Fapp-store-badge.png&w=256&q=100",
8+
method: "GET",
9+
headers: { accept: contentType }
10+
}).then((response) => {
11+
// TODO: not sure why this is failing in CI
12+
//expect(response.headers["content-type"]).to.equal(contentType);
13+
expect(response.headers["cache-control"]).to.equal(
14+
"public, max-age=31536000, must-revalidate"
15+
);
16+
});
17+
});
18+
}
19+
);
20+
21+
// Higher quality should have higher file size
22+
[
23+
{ quality: "100", expectedContentLength: "5742" },
24+
{ quality: "50", expectedContentLength: "2654" }
25+
].forEach(({ quality, expectedContentLength }) => {
26+
it(`serves image app-store-badge.png with quality: ${quality}`, () => {
27+
cy.request({
28+
url: `/_next/image?url=%2Fapp-store-badge.png&w=256&q=${quality}`,
29+
method: "GET",
30+
headers: { accept: "image/webp" }
31+
}).then((response) => {
32+
// TODO: not sure why this is failing in CI
33+
// expect(response.headers["content-length"]).to.equal(
34+
// expectedContentLength
35+
// );
36+
expect(response.headers["cache-control"]).to.equal(
37+
"public, max-age=31536000, must-revalidate"
38+
);
39+
});
40+
});
41+
});
42+
43+
// Higher width should have higher file size
44+
[
45+
{ width: "128", expectedContentLength: "2600" },
46+
{ width: "64", expectedContentLength: "1192" }
47+
].forEach(({ width, expectedContentLength }) => {
48+
it(`serves image app-store-badge.png with width: ${width}`, () => {
49+
cy.request({
50+
url: `/_next/image?url=%2Fapp-store-badge.png&w=${width}&q=100`,
51+
method: "GET",
52+
headers: { accept: "image/webp" }
53+
}).then((response) => {
54+
// TODO: not sure why this is failing in CI
55+
// expect(response.headers["content-length"]).to.equal(
56+
// expectedContentLength
57+
// );
58+
expect(response.headers["cache-control"]).to.equal(
59+
"public, max-age=31536000, must-revalidate"
60+
);
61+
});
62+
});
63+
});
64+
65+
[
66+
{
67+
path: "/_next/image?url=https%3A%2F%2Fraw.githubusercontent.com%2Fserverless-nextjs%2Fserverless-next.js%2Fmaster%2Fpackages%2Fe2e-tests%2Fnext-app-experimental%2Fpublic%2Fapp-store-badge.png&q=100&w=128"
68+
}
69+
].forEach(({ path }) => {
70+
it(`serves external image: ${path}`, () => {
71+
cy.request({ url: path, method: "GET" });
72+
});
73+
});
74+
75+
[
76+
{ path: "/_next/image" },
77+
{ path: "/_next/image?w=256&q=100" },
78+
{ path: "/_next/image?url=%2Fapp-store-badge.png&w=256" },
79+
{ path: "/_next/image?url=%2Fapp-store-badge.png&q=100" }
80+
].forEach(({ path }) => {
81+
it(`missing query parameter fails with 400 status code: ${path}`, () => {
82+
cy.request({ url: path, method: "GET", failOnStatusCode: false }).then(
83+
(response) => {
84+
expect(response.status).to.equal(400);
85+
}
86+
);
87+
});
88+
});
89+
});
90+
91+
describe("image component page", () => {
92+
[{ path: "/image-component" }].forEach(({ path }) => {
93+
it(`serves page with image component and caches the image: ${path}`, () => {
94+
cy.ensureAllRoutesNotErrored(); // Visit routes only
95+
96+
cy.visit(path);
97+
98+
cy.ensureRouteCached(
99+
"/_next/image?url=%2Fapp-store-badge.png&w=1200&q=75"
100+
);
101+
102+
cy.visit(path);
103+
});
104+
});
105+
});
106+
});

0 commit comments

Comments
 (0)