Skip to content

Commit ff659f0

Browse files
committed
feat: add support for next15 geolocation
Next15 moves the geolocation information from a geo property on the request to `x-vercel-ip` headers. `@vercel/functions` has a `geolocation` helper function to access those.
1 parent c5aa622 commit ff659f0

File tree

3 files changed

+52
-20
lines changed

3 files changed

+52
-20
lines changed

.changeset/friendly-kangaroos-give.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@opennextjs/aws": patch
3+
---
4+
5+
feat: add support for Next15 geolocation

packages/open-next/src/core/routing/middleware.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,38 +46,45 @@ export async function handleMiddleware(
4646
internalEvent: InternalEvent,
4747
middlewareLoader: MiddlewareLoader = defaultMiddlewareLoader,
4848
): Promise<MiddlewareOutputEvent | InternalResult> {
49-
const { query } = internalEvent;
49+
// We bypass the middleware if the request is internal
50+
if (internalEvent.headers["x-isr"]) return internalEvent;
51+
5052
const normalizedPath = localizePath(internalEvent);
5153
// We only need the normalizedPath to check if the middleware should run
5254
const hasMatch = middleMatch.some((r) => r.test(normalizedPath));
5355
if (!hasMatch) return internalEvent;
54-
// We bypass the middleware if the request is internal
55-
if (internalEvent.headers["x-isr"]) return internalEvent;
5656

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

62-
const host = internalEvent.headers.host
63-
? `${protocol}//${internalEvent.headers.host}`
62+
const headers = internalEvent.headers;
63+
64+
const host = headers.host
65+
? `${protocol}//${headers.host}`
6466
: "http://localhost:3000";
6567

6668
const initialUrl = new URL(normalizedPath, host);
67-
initialUrl.search = convertToQueryString(query);
69+
initialUrl.search = convertToQueryString(internalEvent.query);
6870
const url = initialUrl.toString();
6971

7072
const middleware = await middlewareLoader();
7173

7274
const result: Response = await middleware.default({
75+
// `geo` is pre Next 15.
76+
// `x-open-next` are checked first for backward compatibility.
7377
geo: {
74-
city: internalEvent.headers["x-open-next-city"],
75-
country: internalEvent.headers["x-open-next-country"],
76-
region: internalEvent.headers["x-open-next-region"],
77-
latitude: internalEvent.headers["x-open-next-latitude"],
78-
longitude: internalEvent.headers["x-open-next-longitude"],
78+
city: headers["x-open-next-city"] ?? headers["x-vercel-ip-city"],
79+
country: headers["x-open-next-country"] ?? headers["x-vercel-ip-country"],
80+
region:
81+
headers["x-open-next-region"] ?? headers["x-vercel-ip-country-region"],
82+
latitude:
83+
headers["x-open-next-latitude"] ?? headers["x-vercel-ip-latitude"],
84+
longitude:
85+
headers["x-open-next-longitude"] ?? headers["x-vercel-ip-longitude"],
7986
},
80-
headers: internalEvent.headers,
87+
headers,
8188
method: internalEvent.method || "GET",
8289
nextConfig: {
8390
basePath: NextConfig.basePath,

packages/open-next/src/overrides/converters/aws-cloudfront.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from "../../core/routing/util";
2222
import type { MiddlewareOutputEvent } from "../../core/routingHandler";
2323

24-
const CloudFrontBlacklistedHeaders = [
24+
const cloudfrontBlacklistedHeaders = [
2525
// Disallowed headers, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-disallowed-headers
2626
"connection",
2727
"expect",
@@ -65,15 +65,27 @@ const cloudfrontReadOnlyHeaders = [
6565
"via",
6666
];
6767

68+
const cloudfrontHeaderToNextHeader: Record<string, string> = {
69+
"x-open-next-city": "x-vercel-ip-city",
70+
"x-open-next-country": "x-vercel-ip-country",
71+
"x-open-next-region": "x-vercel-ip-country-region",
72+
"x-open-next-latitude": "x-vercel-ip-latitude",
73+
"x-open-next-longitude": "x-vercel-ip-longitude",
74+
};
75+
6876
function normalizeCloudFrontRequestEventHeaders(
6977
rawHeaders: CloudFrontHeaders,
7078
): Record<string, string> {
7179
const headers: Record<string, string> = {};
7280

73-
for (const [key, values] of Object.entries(rawHeaders)) {
81+
for (const [rawKey, values] of Object.entries(rawHeaders)) {
7482
for (const { value } of values) {
7583
if (value) {
76-
headers[key.toLowerCase()] = value;
84+
const key = rawKey.toLowerCase();
85+
headers[key] = value;
86+
if (key in cloudfrontHeaderToNextHeader) {
87+
headers[cloudfrontHeaderToNextHeader[key]] = value;
88+
}
7789
}
7890
}
7991
}
@@ -84,8 +96,16 @@ function normalizeCloudFrontRequestEventHeaders(
8496
async function convertFromCloudFrontRequestEvent(
8597
event: CloudFrontRequestEvent,
8698
): Promise<InternalEvent> {
87-
const { method, uri, querystring, body, headers, clientIp } =
88-
event.Records[0].cf.request;
99+
const {
100+
method,
101+
uri,
102+
querystring,
103+
body,
104+
headers: rawHeaders,
105+
clientIp,
106+
} = event.Records[0].cf.request;
107+
const headers = normalizeCloudFrontRequestEventHeaders(rawHeaders);
108+
89109
return {
90110
type: "core",
91111
method,
@@ -95,11 +115,11 @@ async function convertFromCloudFrontRequestEvent(
95115
body?.data ?? "",
96116
body?.encoding === "base64" ? "base64" : "utf8",
97117
),
98-
headers: normalizeCloudFrontRequestEventHeaders(headers),
118+
headers,
99119
remoteAddress: clientIp,
100120
query: convertToQuery(querystring),
101121
cookies:
102-
headers.cookie?.reduce((acc, cur) => {
122+
rawHeaders.cookie?.reduce((acc, cur) => {
103123
const { key, value } = cur;
104124
return { ...acc, [key ?? ""]: value };
105125
}, {}) ?? {},
@@ -119,7 +139,7 @@ function convertToCloudfrontHeaders(
119139
.map(([key, value]) => [key.toLowerCase(), value] as const)
120140
.filter(
121141
([key]) =>
122-
!CloudFrontBlacklistedHeaders.some((header) =>
142+
!cloudfrontBlacklistedHeaders.some((header) =>
123143
typeof header === "string" ? header === key : header.test(key),
124144
) &&
125145
// Only remove read-only headers when directly responding in lambda@edge

0 commit comments

Comments
 (0)