Skip to content

Commit

Permalink
fix: next.config.js should use x-forwarded-host to resolve rewrites (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored May 24, 2024
1 parent 30243dc commit 0778362
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 97 deletions.
3 changes: 2 additions & 1 deletion packages/ui/docs-bundle/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ const nextConfig = {
assetPrefix,
rewrites: async () => {
const HAS_FERN_DOCS_PREVIEW = { type: "cookie", key: "_fern_docs_preview", value: "(?<host>.*)" };
const HAS_X_FORWARDED_HOST = { type: "header", key: "x-forwarded-host", value: "(?<host>.*)" };
const HAS_X_FERN_HOST = { type: "header", key: "x-fern-host", value: "(?<host>.*)" };
const HAS_HOST = { type: "host", value: "(?<host>.*)" };

// The order of the following array is important. The first match will be used.
const WITH_MATCHED_HOST = [HAS_FERN_DOCS_PREVIEW, HAS_X_FERN_HOST, HAS_HOST];
const WITH_MATCHED_HOST = [HAS_FERN_DOCS_PREVIEW, HAS_X_FORWARDED_HOST, HAS_X_FERN_HOST, HAS_HOST];

const HAS_FERN_TOKEN = { type: "cookie", key: "fern_token" };
const THREW_ERROR = { type: "query", key: "error", value: "true" };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FeatureFlags } from "@fern-ui/ui";
import { getAll } from "@vercel/edge-config";
import { NextRequest, NextResponse } from "next/server";
import { getXFernHostEdge } from "../../../utils/xFernHost";

export const runtime = "edge";

Expand All @@ -22,7 +23,7 @@ type FeatureFlag = (typeof FEATURE_FLAGS)[number];
type EdgeConfigResponse = Record<FeatureFlag, string[]>;

export default async function handler(req: NextRequest): Promise<NextResponse<FeatureFlags>> {
const domain = process.env.NEXT_PUBLIC_DOCS_DOMAIN ?? req.headers.get("x-fern-host") ?? req.nextUrl.host;
const domain = getXFernHostEdge(req);
return NextResponse.json(await getFeatureFlags(domain));
}

Expand Down
36 changes: 10 additions & 26 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/resolve-api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { resolveSidebarNodesRoot, visitSidebarNodeRaw } from "@fern-ui/fdr-utils";
import { buildUrl, resolveSidebarNodesRoot, visitSidebarNodeRaw } from "@fern-ui/fdr-utils";
import { ApiDefinitionResolver, REGISTRY_SERVICE, type ResolvedRootPackage } from "@fern-ui/ui";
import { NextApiHandler, NextApiResponse } from "next";
import { toValidPathname } from "../../../utils/toValidPathname";
import { getXFernHostNode } from "../../../utils/xFernHost";
import { getFeatureFlags } from "./feature-flags";

export const dynamic = "force-dynamic";
Expand All @@ -15,15 +17,9 @@ const resolveApiHandler: NextApiHandler = async (
return;
}

const xFernHost = process.env.NEXT_PUBLIC_DOCS_DOMAIN ?? req.headers["x-fern-host"];
if (typeof xFernHost === "string") {
req.headers.host = xFernHost;
res.setHeader("host", xFernHost);
} else {
res.status(400).json(null);
return;
}
const hostWithoutTrailingSlash = xFernHost.endsWith("/") ? xFernHost.slice(0, -1) : xFernHost;
const xFernHost = getXFernHostNode(req);
res.setHeader("host", xFernHost);

const fullUrl = req.url;

if (fullUrl == null) {
Expand All @@ -32,8 +28,10 @@ const resolveApiHandler: NextApiHandler = async (
}

const maybePathName = fullUrl.split("/api/fern-docs/resolve-api")[0] ?? "";
const pathname = maybePathName.startsWith("/") ? maybePathName : `/${maybePathName}`;
const url = `${hostWithoutTrailingSlash}${pathname}`;
const url = buildUrl({
host: xFernHost,
pathname: toValidPathname(maybePathName),
});
// eslint-disable-next-line no-console
console.log("[resolve-api] Loading docs for", url);
const docsResponse = await REGISTRY_SERVICE.docs.v2.read.getDocsForUrl({
Expand Down Expand Up @@ -89,17 +87,3 @@ const resolveApiHandler: NextApiHandler = async (
};

export default resolveApiHandler;

// function findApiSection(api: string, sidebarNodes: SidebarNode[]): SidebarNode.ApiSection | undefined {
// for (const node of sidebarNodes) {
// if (node.type === "apiSection" && node.api === api) {
// return node;
// } else if (node.type === "section") {
// const found = findApiSection(api, node.items);
// if (found != null) {
// return found;
// }
// }
// }
// return undefined;
// }
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { buildUrl, getAllUrlsFromDocsConfig } from "@fern-ui/fdr-utils";
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { loadWithUrl } from "../../../utils/loadWithUrl";
import { toValidPathname } from "../../../utils/toValidPathname";
import { getXFernHostNode } from "../../../utils/xFernHost";

export const config = {
maxDuration: 300,
Expand Down Expand Up @@ -43,14 +44,10 @@ const handler: NextApiHandler = async (
try {
// when we call res.revalidate() nextjs uses
// req.headers.host to make the network request
const xFernHost = req.headers["x-fern-host"] ?? req.headers["host"];
if (typeof xFernHost !== "string") {
return res.status(404).json({ successfulRevalidations: [], failedRevalidations: [] });
}
const hostWithoutTrailingSlash = xFernHost.endsWith("/") ? xFernHost.slice(0, -1) : xFernHost;
const xFernHost = getXFernHostNode(req);

const url = buildUrl({
host: hostWithoutTrailingSlash,
host: xFernHost,
pathname: toValidPathname(req.query.basePath),
});
// eslint-disable-next-line no-console
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { buildUrl, getAllUrlsFromDocsConfig } from "@fern-ui/fdr-utils";
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { loadWithUrl } from "../../../../utils/loadWithUrl";
import { toValidPathname } from "../../../../utils/toValidPathname";
import { cleanHost, getXFernHostNode } from "../../../../utils/xFernHost";

export const config = {
maxDuration: 300,
Expand Down Expand Up @@ -44,14 +45,10 @@ const handler: NextApiHandler = async (
try {
// when we call res.revalidate() nextjs uses
// req.headers.host to make the network request
const xFernHost = getHostFromBody(req.body) ?? req.headers["x-fern-host"] ?? req.headers["host"];
if (typeof xFernHost !== "string") {
return res.status(404).json({ successfulRevalidations: [], failedRevalidations: [] });
}
const hostWithoutTrailingSlash = xFernHost.endsWith("/") ? xFernHost.slice(0, -1) : xFernHost;
const xFernHost = getHostFromBody(req) ?? getXFernHostNode(req);

const url = buildUrl({
host: hostWithoutTrailingSlash,
host: xFernHost,
pathname: toValidPathname(req.query.basePath),
});
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -118,7 +115,7 @@ function getHostFromBody(body: unknown): string | undefined {
}

if (typeof body.host === "string") {
return body.host;
return cleanHost(body.host);
}

return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { buildUrl, getAllUrlsFromDocsConfig } from "@fern-ui/fdr-utils";
import { NextApiHandler, NextApiRequest, NextApiResponse } from "next";
import { loadWithUrl } from "../../../../utils/loadWithUrl";
import { toValidPathname } from "../../../../utils/toValidPathname";
import { getXFernHostNode } from "../../../../utils/xFernHost";

export const config = {
maxDuration: 300,
Expand Down Expand Up @@ -43,14 +44,10 @@ const handler: NextApiHandler = async (
try {
// when we call res.revalidate() nextjs uses
// req.headers.host to make the network request
const xFernHost = getHost(req.query.host) ?? req.headers["x-fern-host"] ?? req.headers["host"];
if (typeof xFernHost !== "string") {
return res.status(404).json({ successfulRevalidations: [], failedRevalidations: [] });
}
const hostWithoutTrailingSlash = xFernHost.endsWith("/") ? xFernHost.slice(0, -1) : xFernHost;
const xFernHost = getXFernHostNode(req, true);

const url = buildUrl({
host: hostWithoutTrailingSlash,
host: xFernHost,
pathname: toValidPathname(req.query.basePath),
});
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -115,11 +112,3 @@ const handler: NextApiHandler = async (
};

export default handler;

function getHost(maybeHost: string | string[] | undefined): string | undefined {
if (typeof maybeHost === "string") {
return maybeHost;
}

return undefined;
}
13 changes: 8 additions & 5 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/robots.txt.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { NextRequest, NextResponse } from "next/server";
import { getXFernHostEdge } from "../../../utils/xFernHost";

export const runtime = "edge";

export default async function GET(req: NextRequest): Promise<NextResponse> {
const xFernHost = process.env.NEXT_PUBLIC_DOCS_DOMAIN ?? req.headers.get("x-fern-host") ?? req.nextUrl.host;
const xFernHost = getXFernHostEdge(req);

const hostname = new URL(`https://${xFernHost}`).hostname; // strip basepath

if (hostname.includes(".docs.dev.buildwithfern.com") || hostname.includes(".docs.buildwithfern.com")) {
if (
xFernHost.includes(".docs.buildwithfern.com") ||
xFernHost.includes(".docs.dev.buildwithfern.com") ||
xFernHost.includes(".docs.staging.buildwithfern.com")
) {
return new NextResponse("User-Agent: *\nDisallow: /", { status: 200 });
}

return new NextResponse(`User-Agent: *\nSitemap: https://${hostname}/sitemap.xml`, { status: 200 });
return new NextResponse(`User-Agent: *\nSitemap: https://${xFernHost}/sitemap.xml`, { status: 200 });
}
29 changes: 5 additions & 24 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,19 @@ import { NextRequest, NextResponse } from "next/server";
import { loadWithUrl } from "../../../utils/loadWithUrl";
import { jsonResponse } from "../../../utils/serverResponse";
import { toValidPathname } from "../../../utils/toValidPathname";
import { getXFernHostEdge } from "../../../utils/xFernHost";

export const runtime = "edge";

function getHostFromUrl(url: string | undefined): string | undefined {
if (url == null) {
return undefined;
}
const urlObj = new URL(url);
return urlObj.host;
}

export default async function GET(req: NextRequest): Promise<NextResponse> {
if (req.method !== "GET") {
return new NextResponse(null, { status: 405 });
}

let xFernHost = req.headers.get("x-fern-host") ?? getHostFromUrl(req.nextUrl.href);

if (xFernHost != null && xFernHost.includes("localhost")) {
xFernHost = process.env.NEXT_PUBLIC_DOCS_DOMAIN;
}

const headers: Record<string, string> = {};

if (xFernHost != null) {
// when we call res.revalidate() nextjs uses
// req.headers.host to make the network request
xFernHost = xFernHost.endsWith("/") ? xFernHost.slice(0, -1) : xFernHost;
headers["x-fern-host"] = xFernHost;
} else {
return jsonResponse(400, [], headers);
}
const xFernHost = getXFernHostEdge(req);
const headers: Record<string, string> = {
"x-fern-host": xFernHost,
};

try {
const url = buildUrl({ host: xFernHost, pathname: toValidPathname(req.nextUrl.searchParams.get("basePath")) });
Expand Down
15 changes: 2 additions & 13 deletions packages/ui/docs-bundle/src/pages/api/fern-docs/sitemap.xml.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { notFoundResponse } from "../../../utils/serverResponse";
import { getXFernHostEdge } from "../../../utils/xFernHost";

export const runtime = "edge";
export const revalidate = 60 * 60 * 24;

function getHostFromUrl(url: string | undefined): string | undefined {
if (url == null) {
return undefined;
}
const urlObj = new URL(url);
return urlObj.host;
}

export default async function GET(req: NextRequest): Promise<NextResponse> {
const xFernHost = req.headers.get("x-fern-host") ?? getHostFromUrl(req.nextUrl.href);

if (xFernHost == null || Array.isArray(xFernHost)) {
return notFoundResponse();
}
const xFernHost = getXFernHostEdge(req);

const hostWithoutTrailingSlash = xFernHost.endsWith("/") ? xFernHost.slice(0, -1) : xFernHost;

Expand Down
91 changes: 91 additions & 0 deletions packages/ui/docs-bundle/src/utils/xFernHost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { NextApiRequest } from "next";
import type { NextRequest } from "next/server";

/**
* Notes:
*
* x-fern-host is always appended to the request header by cloudfront for all *.docs.buildwithfern.com requests.
* if the request is a rewrite from a custom domain, then x-forwarded-host is appended to the request header.
* prefer x-forwarded-host over x-fern-host.
*
* NEXT_PUBLIC_DOCS_DOMAIN is used for local development only.
* _fern_docs_preview is used for previewing the docs.
*/

export function getXFernHostEdge(req: NextRequest, useSearchParams = false): string {
const hosts = [
useSearchParams ? req.nextUrl.searchParams.get("host") : undefined,
process.env.NEXT_PUBLIC_DOCS_DOMAIN,
req.cookies.get("_fern_docs_preview")?.value,
req.headers.get("x-forwarded-host"),
req.headers.get("x-fern-host"),
req.nextUrl.host,
];

for (let host of hosts) {
host = cleanHost(host);
if (host != null) {
return host;
}
}

throw new Error("Could not determine xFernHost from request.");
}

export function getXFernHostNode(req: NextApiRequest, useSearchParams = false): string {
const hosts = [
useSearchParams ? req.query["host"] : undefined,
process.env.NEXT_PUBLIC_DOCS_DOMAIN,
req.cookies["_fern_docs_preview"],
req.headers["x-forwarded-host"],
req.headers["x-fern-host"],
req.headers.host,
];

for (let host of hosts) {
if (Array.isArray(host)) {
continue;
}

host = cleanHost(host);
if (host != null) {
return host;
}
}

throw new Error("Could not determine xFernHost from request.");
}

export function cleanHost(host: string | null | undefined): string | undefined {
if (typeof host !== "string") {
return undefined;
}

host = host.trim();

// host should not be localhost
if (host.includes("localhost")) {
return undefined;
}

// host should not be an ip address
if (host.match(/\d+\.\d+\.\d+\.\d+/)) {
return undefined;
}

// strip `http://` or `https://` from the host, if present
if (host.includes("://")) {
host = host.split("://")[1];
}

// strip trailing slash from the host, if present
if (host.endsWith("/")) {
host = host.slice(0, -1);
}

if (host === "") {
return undefined;
}

return host;
}

0 comments on commit 0778362

Please sign in to comment.