Skip to content

feat: add support for next15 geolocation #617

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

Merged
merged 2 commits into from
Nov 6, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/friendly-kangaroos-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@opennextjs/aws": patch
---

feat: add support for Next15 geolocation
31 changes: 13 additions & 18 deletions packages/open-next/src/core/routing/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,31 +114,26 @@ function convertMatch(
toDestination: PathFunction,
destination: string,
) {
if (match) {
const { params } = match;
const isUsingParams = Object.keys(params).length > 0;
if (isUsingParams) {
return toDestination(params);
} else {
return destination;
}
} else {
if (!match) {
return destination;
}

const { params } = match;
const isUsingParams = Object.keys(params).length > 0;
return isUsingParams ? toDestination(params) : destination;
}

export function addNextConfigHeaders(
export function getNextConfigHeaders(
event: InternalEvent,
configHeaders?: Header[] | undefined,
) {
const addedHeaders: Record<string, string | undefined> = {};
): Record<string, string | undefined> {
if (!configHeaders) {
return {};
}

if (!configHeaders) return addedHeaders;
const { rawPath, headers, query, cookies } = event;
const matcher = routeHasMatcher(headers, cookies, query);
const matcher = routeHasMatcher(event.headers, event.cookies, event.query);

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

const localizedRawPath = localizePath(event);

for (const {
Expand All @@ -149,7 +144,7 @@ export function addNextConfigHeaders(
source,
locale,
} of configHeaders) {
const path = locale === false ? rawPath : localizedRawPath;
const path = locale === false ? event.rawPath : localizedRawPath;
if (
new RegExp(regex).test(path) &&
checkHas(matcher, has) &&
Expand All @@ -163,7 +158,7 @@ export function addNextConfigHeaders(
const value = convertMatch(_match, compile(h.value), h.value);
requestHeaders[key] = value;
} catch {
debug("Error matching header ", h.key, " with value ", h.value);
debug(`Error matching header ${h.key} with value ${h.value}`);
requestHeaders[h.key] = h.value;
}
});
Expand Down
29 changes: 16 additions & 13 deletions packages/open-next/src/core/routing/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,38 +46,41 @@ export async function handleMiddleware(
internalEvent: InternalEvent,
middlewareLoader: MiddlewareLoader = defaultMiddlewareLoader,
): Promise<MiddlewareOutputEvent | InternalResult> {
const { query } = internalEvent;
const normalizedPath = localizePath(internalEvent);
const headers = internalEvent.headers;

// We bypass the middleware if the request is internal
if (headers["x-isr"]) return internalEvent;

// We only need the normalizedPath to check if the middleware should run
const normalizedPath = localizePath(internalEvent);
const hasMatch = middleMatch.some((r) => r.test(normalizedPath));
if (!hasMatch) return internalEvent;
// We bypass the middleware if the request is internal
if (internalEvent.headers["x-isr"]) return internalEvent;

// Retrieve the protocol:
// - In lambda, the url only contains the rawPath and the query - default to https
// - In cloudflare, the protocol is usually http in dev and https in production
const protocol = internalEvent.url.startsWith("http://") ? "http:" : "https:";

const host = internalEvent.headers.host
? `${protocol}//${internalEvent.headers.host}`
const host = headers.host
? `${protocol}//${headers.host}`
: "http://localhost:3000";

const initialUrl = new URL(normalizedPath, host);
initialUrl.search = convertToQueryString(query);
initialUrl.search = convertToQueryString(internalEvent.query);
const url = initialUrl.toString();

const middleware = await middlewareLoader();

const result: Response = await middleware.default({
// `geo` is pre Next 15.
geo: {
city: internalEvent.headers["x-open-next-city"],
country: internalEvent.headers["x-open-next-country"],
region: internalEvent.headers["x-open-next-region"],
latitude: internalEvent.headers["x-open-next-latitude"],
longitude: internalEvent.headers["x-open-next-longitude"],
city: headers["x-open-next-city"],
country: headers["x-open-next-country"],
region: headers["x-open-next-region"],
latitude: headers["x-open-next-latitude"],
longitude: headers["x-open-next-longitude"],
},
headers: internalEvent.headers,
headers,
method: internalEvent.method || "GET",
nextConfig: {
basePath: NextConfig.basePath,
Expand Down
32 changes: 28 additions & 4 deletions packages/open-next/src/core/routingHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import type { InternalEvent, InternalResult, Origin } from "types/open-next";
import { debug } from "../adapters/logger";
import { cacheInterceptor } from "./routing/cacheInterceptor";
import {
addNextConfigHeaders,
fixDataPage,
getNextConfigHeaders,
handleFallbackFalse,
handleRedirects,
handleRewrites,
} from "./routing/matcher";
import { handleMiddleware } from "./routing/middleware";

export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-";
export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length;
export interface MiddlewareOutputEvent {
internalEvent: InternalEvent;
isExternalRewrite: boolean;
Expand Down Expand Up @@ -57,23 +59,45 @@ const dynamicRegexp = RoutesManifest.routes.dynamic.map(
),
);

// Geolocation headers starting from Nextjs 15
// See https://github.com/vercel/vercel/blob/7714b1c/packages/functions/src/headers.ts
const geoHeaderToNextHeader = {
"x-open-next-city": "x-vercel-ip-city",
"x-open-next-country": "x-vercel-ip-country",
"x-open-next-region": "x-vercel-ip-country-region",
"x-open-next-latitude": "x-vercel-ip-latitude",
"x-open-next-longitude": "x-vercel-ip-longitude",
};

function applyMiddlewareHeaders(
eventHeaders: Record<string, string | string[]>,
middlewareHeaders: Record<string, string | string[] | undefined>,
setPrefix = true,
) {
const keyPrefix = setPrefix ? MIDDLEWARE_HEADER_PREFIX : "";
Object.entries(middlewareHeaders).forEach(([key, value]) => {
if (value) {
eventHeaders[`${setPrefix ? "x-middleware-response-" : ""}${key}`] =
Array.isArray(value) ? value.join(",") : value;
eventHeaders[keyPrefix + key] = Array.isArray(value)
? value.join(",")
: value;
}
});
}

export default async function routingHandler(
event: InternalEvent,
): Promise<InternalResult | MiddlewareOutputEvent> {
const nextHeaders = addNextConfigHeaders(event, ConfigHeaders);
// Add Next geo headers
for (const [openNextGeoName, nextGeoName] of Object.entries(
geoHeaderToNextHeader,
)) {
const value = event.headers[openNextGeoName];
if (value) {
event.headers[nextGeoName] = value;
}
}

const nextHeaders = getNextConfigHeaders(event, ConfigHeaders);

let internalEvent = fixDataPage(event, BuildId);
if ("statusCode" in internalEvent) {
Expand Down
5 changes: 3 additions & 2 deletions packages/open-next/src/overrides/converters/aws-cloudfront.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from "../../core/routing/util";
import type { MiddlewareOutputEvent } from "../../core/routingHandler";

const CloudFrontBlacklistedHeaders = [
const cloudfrontBlacklistedHeaders = [
// Disallowed headers, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-disallowed-headers
"connection",
"expect",
Expand Down Expand Up @@ -86,6 +86,7 @@ async function convertFromCloudFrontRequestEvent(
): Promise<InternalEvent> {
const { method, uri, querystring, body, headers, clientIp } =
event.Records[0].cf.request;

return {
type: "core",
method,
Expand Down Expand Up @@ -119,7 +120,7 @@ function convertToCloudfrontHeaders(
.map(([key, value]) => [key.toLowerCase(), value] as const)
.filter(
([key]) =>
!CloudFrontBlacklistedHeaders.some((header) =>
!cloudfrontBlacklistedHeaders.some((header) =>
typeof header === "string" ? header === key : header.test(key),
) &&
// Only remove read-only headers when directly responding in lambda@edge
Expand Down
25 changes: 23 additions & 2 deletions packages/open-next/src/overrides/wrappers/cloudflare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,20 @@ import type {

import type { MiddlewareOutputEvent } from "../../core/routingHandler";

const cfPropNameToHeaderName = {
city: "x-open-next-city",
country: "x-open-next-country",
region: "x-open-next-region",
latitude: "x-open-next-latitude",
longitude: "x-open-next-longitude",
};

const handler: WrapperHandler<
InternalEvent,
InternalResult | ({ type: "middleware" } & MiddlewareOutputEvent)
> =
async (handler, converter) =>
async (event: Request, env: Record<string, string>): Promise<Response> => {
async (request: Request, env: Record<string, string>): Promise<Response> => {
globalThis.process = process;

// Set the environment variables
Expand All @@ -22,7 +30,20 @@ const handler: WrapperHandler<
}
}

const internalEvent = await converter.convertFrom(event);
const internalEvent = await converter.convertFrom(request);

// Retrieve geo information from the cloudflare request
// See https://developers.cloudflare.com/workers/runtime-apis/request
// Note: This code could be moved to a cloudflare specific converter when one is created.
const cfProperties = (request as any).cf as Record<string, string | null>;
for (const [propName, headerName] of Object.entries(
cfPropNameToHeaderName,
)) {
const propValue = cfProperties[propName];
if (propValue !== null) {
internalEvent.headers[headerName] = propValue;
}
}

const response = await handler(internalEvent);

Expand Down
20 changes: 10 additions & 10 deletions packages/tests-unit/tests/core/routing/matcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NextConfig } from "@opennextjs/aws/adapters/config/index.js";
import {
addNextConfigHeaders,
fixDataPage,
getNextConfigHeaders,
handleRedirects,
handleRewrites,
} from "@opennextjs/aws/core/routing/matcher.js";
Expand Down Expand Up @@ -39,17 +39,17 @@ beforeEach(() => {
vi.resetAllMocks();
});

describe("addNextConfigHeaders", () => {
describe("getNextConfigHeaders", () => {
it("should return empty object for undefined configHeaders", () => {
const event = createEvent({});
const result = addNextConfigHeaders(event);
const result = getNextConfigHeaders(event);

expect(result).toEqual({});
});

it("should return empty object for empty configHeaders", () => {
const event = createEvent({});
const result = addNextConfigHeaders(event, []);
const result = getNextConfigHeaders(event, []);

expect(result).toEqual({});
});
Expand All @@ -59,7 +59,7 @@ describe("addNextConfigHeaders", () => {
url: "/",
});

const result = addNextConfigHeaders(event, [
const result = getNextConfigHeaders(event, [
{
source: "/",
regex: "^/$",
Expand All @@ -82,7 +82,7 @@ describe("addNextConfigHeaders", () => {
url: "/",
});

const result = addNextConfigHeaders(event, [
const result = getNextConfigHeaders(event, [
{
source: "/",
regex: "^/$",
Expand All @@ -98,7 +98,7 @@ describe("addNextConfigHeaders", () => {
url: "/hello-world",
});

const result = addNextConfigHeaders(event, [
const result = getNextConfigHeaders(event, [
{
source: "/(.*)",
regex: "^(?:/(.*))(?:/)?$",
Expand Down Expand Up @@ -129,7 +129,7 @@ describe("addNextConfigHeaders", () => {
},
});

const result = addNextConfigHeaders(event, [
const result = getNextConfigHeaders(event, [
{
source: "/(.*)",
regex: "^(?:/(.*))(?:/)?$",
Expand All @@ -156,7 +156,7 @@ describe("addNextConfigHeaders", () => {
},
});

const result = addNextConfigHeaders(event, [
const result = getNextConfigHeaders(event, [
{
source: "/(.*)",
regex: "^(?:/(.*))(?:/)?$",
Expand All @@ -183,7 +183,7 @@ describe("addNextConfigHeaders", () => {
},
});

const result = addNextConfigHeaders(event, [
const result = getNextConfigHeaders(event, [
{
source: "/(.*)",
regex: "^(?:/(.*))(?:/)?$",
Expand Down