Skip to content

Only add x-fah-middleware header to routes that have middleware enabled #325

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
11 changes: 10 additions & 1 deletion packages/@apphosting/adapter-nextjs/e2e/middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ before(() => {
});

describe("middleware", () => {
it("should have x-fah-adapter header and x-fah-middleware header on all routes", async () => {
it("should have x-fah-adapter header on all routes", async () => {
const routes = [
"/",
"/ssg",
Expand All @@ -33,6 +33,15 @@ describe("middleware", () => {
`nextjs-${adapterVersion}`,
`Route ${route} missing x-fah-adapter header`,
);
}
});

it("should have x-fah-middleware header on middleware routes", async () => {
// Middleware is configured to run on these routes via run-local.ts with-middleware setup function
const routes = ["/ssg", "/ssr"];

for (const route of routes) {
const response = await fetch(posix.join(host, route));
assert.equal(
response.headers.get("x-fah-middleware"),
"true",
Expand Down
6 changes: 3 additions & 3 deletions packages/@apphosting/adapter-nextjs/e2e/run-local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ const scenarios: Scenario[] = [
setup: async (cwd: string) => {
// Create a middleware.ts file
const middlewareContent = `
import type { NextRequest } from 'next/server'
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
// This is a simple middleware that doesn't modify the request
console.log('Middleware executed', request.nextUrl.pathname);
console.log("Middleware executed", request.nextUrl.pathname);
}

export const config = {
matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
matcher: ["/ssg", "/ssr"],
};
`;

Expand Down
42 changes: 23 additions & 19 deletions packages/@apphosting/adapter-nextjs/src/overrides.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe("route overrides", () => {
assert.deepStrictEqual(updatedManifest, expectedManifest);
});

it("should add middleware header when middleware exists", async () => {
it("should add middleware header only to routes for which middleware is enabled", async () => {
const { addRouteOverrides } = await importOverrides;
const initialManifest: RoutesManifest = {
version: 3,
Expand All @@ -109,8 +109,8 @@ describe("route overrides", () => {
page: "/",
matchers: [
{
regexp: "^/.*$",
originalSource: "/:path*",
regexp: "/hello",
originalSource: "/hello",
},
],
},
Expand All @@ -130,7 +130,7 @@ describe("route overrides", () => {
fs.readFileSync(routesManifestPath, "utf-8"),
) as RoutesManifest;

assert.strictEqual(updatedManifest.headers.length, 1);
assert.strictEqual(updatedManifest.headers.length, 2);

const expectedManifest: RoutesManifest = {
version: 3,
Expand All @@ -150,9 +150,13 @@ describe("route overrides", () => {
key: "x-fah-adapter",
value: "nextjs-1.0.0",
},
{ key: "x-fah-middleware", value: "true" },
],
},
{
source: "/hello",
regex: "/hello",
headers: [{ key: "x-fah-middleware", value: "true" }],
},
],
};

Expand All @@ -172,13 +176,13 @@ describe("next config overrides", () => {
...config,
images: {
...(config.images || {}),
...(config.images?.unoptimized === undefined && config.images?.loader === undefined
? { unoptimized: true }
...(config.images?.unoptimized === undefined && config.images?.loader === undefined
? { unoptimized: true }
: {}),
},
});

const config = typeof originalConfig === 'function'
const config = typeof originalConfig === 'function'
? async (...args) => {
const resolvedConfig = await originalConfig(...args);
return fahOptimizedConfig(resolvedConfig);
Expand All @@ -194,12 +198,12 @@ describe("next config overrides", () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

/** @type {import('next').NextConfig} */
const nextConfig = {
/* config options here */
}

module.exports = nextConfig
`;

Expand All @@ -213,7 +217,7 @@ describe("next config overrides", () => {
normalizeWhitespace(`
// @ts-nocheck
const originalConfig = require('./next.config.original.js');

${nextConfigOverrideBody}

module.exports = config;
Expand All @@ -225,14 +229,14 @@ describe("next config overrides", () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
/* config options here */
}

export default nextConfig
`;

Expand All @@ -257,7 +261,7 @@ describe("next config overrides", () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
// @ts-check

export default (phase, { defaultConfig }) => {
/**
* @type {import('next').NextConfig}
Expand All @@ -280,7 +284,7 @@ describe("next config overrides", () => {
import originalConfig from './next.config.original.mjs';

${nextConfigOverrideBody}

export default config;
`),
);
Expand All @@ -290,11 +294,11 @@ describe("next config overrides", () => {
const { overrideNextConfig } = await importOverrides;
const originalConfig = `
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
/* config options here */
}

export default nextConfig
`;

Expand All @@ -307,9 +311,9 @@ describe("next config overrides", () => {
normalizeWhitespace(`
// @ts-nocheck
import originalConfig from './next.config.original';

${nextConfigOverrideBody}

module.exports = config;
`),
);
Expand Down
54 changes: 33 additions & 21 deletions packages/@apphosting/adapter-nextjs/src/overrides.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AdapterMetadata, MiddlewareManifest } from "./interfaces.js";
import { AdapterMetadata } from "./interfaces.js";
import {
loadRouteManifest,
writeRouteManifest,
Expand Down Expand Up @@ -146,9 +146,12 @@ export async function validateNextConfigOverride(
* Modifies the app's route manifest (routes-manifest.json) to add Firebase App Hosting
* specific overrides (i.e headers).
*
* This function adds the following headers to all routes:
* It adds the following headers to all routes:
* - x-fah-adapter: The Firebase App Hosting adapter version used to build the app.
*
* It also adds the following headers to all routes for which middleware is enabled:
* - x-fah-middleware: When middleware is enabled.
*
* @param appPath The path to the app directory.
* @param distDir The path to the dist directory.
* @param adapterMetadata The adapter metadata.
Expand All @@ -158,38 +161,47 @@ export async function addRouteOverrides(
distDir: string,
adapterMetadata: AdapterMetadata,
) {
const middlewareManifest = loadMiddlewareManifest(appPath, distDir);
const routeManifest = loadRouteManifest(appPath, distDir);

// Add the adapter version to all routes
routeManifest.headers.push({
source: "/:path*",
headers: [
{
key: "x-fah-adapter",
value: `nextjs-${adapterMetadata.adapterVersion}`,
},
...(middlewareExists(middlewareManifest)
? [
{
key: "x-fah-middleware",
value: "true",
},
]
: []),
],
/*
NextJs converts the source string to a regex using path-to-regexp (https://github.com/pillarjs/path-to-regexp) at
build time: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/index.ts#L1273.
This regex is then used to match the route against the request path.
NextJs converts the source string to a regex using path-to-regexp (https://github.com/pillarjs/path-to-regexp) at
build time: https://github.com/vercel/next.js/blob/canary/packages/next/src/build/index.ts#L1273.
This regex is then used to match the route against the request path.

This regex was generated by building a sample NextJs app with the source string `/:path*` and then inspecting the
routes-manifest.json file.
*/
This regex was generated by building a sample NextJs app with the source string `/:path*` and then inspecting the
routes-manifest.json file.
*/
regex: "^(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))?(?:/)?$",
});

await writeRouteManifest(appPath, distDir, routeManifest);
}
// Add the middleware header to all routes for which middleware is enabled
const middlewareManifest = loadMiddlewareManifest(appPath, distDir);
const rootMiddleware = middlewareManifest.middleware["/"];
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamesdaniels My assumption here was that we only need to look at the root key as NextJs now only supports middleware at the root directory. It used to support nested middleware while in beta which is why I think the API is like this.

Similar logic in Opennext AWS adapter: https://github.com/opennextjs/opennextjs-aws/blob/73281c958d43d865fdded0d5d86dd82747365fee/packages/open-next/src/core/routing/util.ts#L160-L174

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's a good assumption

if (rootMiddleware?.matchers) {
console.log("Middleware detected, adding middleware headers to matching routes");

rootMiddleware.matchers.forEach((matcher) => {
routeManifest.headers.push({
source: matcher.originalSource,
headers: [
{
key: "x-fah-middleware",
value: "true",
},
],
regex: matcher.regexp,
});
});
}

function middlewareExists(middlewareManifest: MiddlewareManifest) {
return Object.keys(middlewareManifest.middleware).length > 0;
await writeRouteManifest(appPath, distDir, routeManifest);
}