diff --git a/.changeset/smooth-laws-pull.md b/.changeset/smooth-laws-pull.md new file mode 100644 index 000000000..738f86392 --- /dev/null +++ b/.changeset/smooth-laws-pull.md @@ -0,0 +1,9 @@ +--- +"@opennextjs/aws": patch +--- + +`InternalEvent#url` is now a full URL + +BREAKING CHANGE: `InternalEvent#url` was only composed of the path and search query before. + +Custom converters should be updated to populate the full URL instead. diff --git a/packages/open-next/src/adapters/edge-adapter.ts b/packages/open-next/src/adapters/edge-adapter.ts index e4cc718cf..3310dec20 100644 --- a/packages/open-next/src/adapters/edge-adapter.ts +++ b/packages/open-next/src/adapters/edge-adapter.ts @@ -8,10 +8,7 @@ import type { OpenNextHandlerOptions } from "types/overrides"; // We import it like that so that the edge plugin can replace it import { NextConfig } from "../adapters/config"; import { createGenericHandler } from "../core/createGenericHandler"; -import { - convertBodyToReadableStream, - convertToQueryString, -} from "../core/routing/util"; +import { convertBodyToReadableStream } from "../core/routing/util"; globalThis.__openNextAls = new AsyncLocalStorage(); @@ -25,13 +22,6 @@ const defaultHandler = async ( return runWithOpenNextRequestContext( { isISRRevalidation: false, waitUntil: options?.waitUntil }, async () => { - const host = internalEvent.headers.host - ? `https://${internalEvent.headers.host}` - : "http://localhost:3000"; - const initialUrl = new URL(internalEvent.rawPath, host); - initialUrl.search = convertToQueryString(internalEvent.query); - const url = initialUrl.toString(); - // @ts-expect-error - This is bundled const handler = await import("./middleware.mjs"); @@ -43,7 +33,7 @@ const defaultHandler = async ( i18n: NextConfig.i18n, trailingSlash: NextConfig.trailingSlash, }, - url, + url: internalEvent.url, body: convertBodyToReadableStream( internalEvent.method, internalEvent.body, diff --git a/packages/open-next/src/adapters/middleware.ts b/packages/open-next/src/adapters/middleware.ts index cb2729685..cd2129a6e 100644 --- a/packages/open-next/src/adapters/middleware.ts +++ b/packages/open-next/src/adapters/middleware.ts @@ -15,6 +15,7 @@ import { resolveQueue, resolveTagCache, } from "../core/resolve"; +import { constructNextUrl } from "../core/routing/util"; import routingHandler, { INTERNAL_HEADER_INITIAL_PATH, INTERNAL_HEADER_RESOLVED_ROUTES, @@ -90,7 +91,7 @@ const defaultHandler = async ( internalEvent: { ...result.internalEvent, rawPath: "/500", - url: "/500", + url: constructNextUrl(result.internalEvent.url, "/500"), method: "GET", }, // On error we need to rewrite to the 500 page which is an internal rewrite diff --git a/packages/open-next/src/core/requestHandler.ts b/packages/open-next/src/core/requestHandler.ts index b0769cbfd..9435f4006 100644 --- a/packages/open-next/src/core/requestHandler.ts +++ b/packages/open-next/src/core/requestHandler.ts @@ -13,7 +13,11 @@ import { runWithOpenNextRequestContext } from "utils/promise"; import type { OpenNextHandlerOptions } from "types/overrides"; import { debug, error, warn } from "../adapters/logger"; import { patchAsyncStorage } from "./patchAsyncStorage"; -import { convertRes, createServerResponse } from "./routing/util"; +import { + constructNextUrl, + convertRes, + createServerResponse, +} from "./routing/util"; import routingHandler, { INTERNAL_HEADER_INITIAL_PATH, INTERNAL_HEADER_RESOLVED_ROUTES, @@ -102,7 +106,7 @@ export async function openNextHandler( rawPath: "/500", method: "GET", headers: {}, - url: "/500", + url: constructNextUrl(internalEvent.url, "/500"), query: {}, cookies: {}, remoteAddress: "", @@ -146,12 +150,13 @@ export async function openNextHandler( const preprocessedEvent = routingResult.internalEvent; debug("preprocessedEvent", preprocessedEvent); + const { search, pathname, hash } = new URL(preprocessedEvent.url); const reqProps = { method: preprocessedEvent.method, - url: preprocessedEvent.url, + url: `${pathname}${search}${hash}`, //WORKAROUND: We pass this header to the serverless function to mimic a prefetch request which will not trigger revalidation since we handle revalidation differently // There is 3 way we can handle revalidation: - // 1. We could just let the revalidation go as normal, but due to race condtions the revalidation will be unreliable + // 1. We could just let the revalidation go as normal, but due to race conditions the revalidation will be unreliable // 2. We could alter the lastModified time of our cache to make next believe that the cache is fresh, but this could cause issues with stale data since the cdn will cache the stale data as if it was fresh // 3. OUR CHOICE: We could pass a purpose prefetch header to the serverless function to make next believe that the request is a prefetch request and not trigger revalidation (This could potentially break in the future if next changes the behavior of prefetch requests) headers: { ...headers, purpose: "prefetch" }, diff --git a/packages/open-next/src/core/routing/matcher.ts b/packages/open-next/src/core/routing/matcher.ts index 32a127bdd..6decb37b6 100644 --- a/packages/open-next/src/core/routing/matcher.ts +++ b/packages/open-next/src/core/routing/matcher.ts @@ -14,6 +14,7 @@ import { emptyReadableStream, toReadableStream } from "utils/stream"; import { debug } from "../../adapters/logger"; import { localizePath } from "./i18n"; import { + constructNextUrl, convertFromQueryString, convertToQueryString, escapeRegex, @@ -171,7 +172,7 @@ export function handleRewrites( event: InternalEvent, rewrites: T[], ) { - const { rawPath, headers, query, cookies } = event; + const { rawPath, headers, query, cookies, url } = event; const localizedRawPath = localizePath(event); const matcher = routeHasMatcher(headers, cookies, query); const computeHas = computeParamHas(headers, cookies, query); @@ -185,7 +186,7 @@ export function handleRewrites( }); let finalQuery = query; - let rewrittenUrl = rawPath; + let rewrittenUrl = url; const isExternalRewrite = isExternal(rewrite?.destination); debug("isExternalRewrite", isExternalRewrite); if (rewrite) { @@ -202,16 +203,12 @@ export function handleRewrites( // %2B does not get interpreted as a space in the URL thus breaking the query string. const encodePlusQueryString = queryString.replaceAll("+", "%20"); debug("urlParts", { pathname, protocol, hostname, queryString }); - const toDestinationPath = compile(escapeRegex(pathname ?? "") ?? ""); - const toDestinationHost = compile(escapeRegex(hostname ?? "") ?? ""); - const toDestinationQuery = compile( - escapeRegex(encodePlusQueryString ?? "") ?? "", - ); + const toDestinationPath = compile(escapeRegex(pathname)); + const toDestinationHost = compile(escapeRegex(hostname)); + const toDestinationQuery = compile(escapeRegex(encodePlusQueryString)); const params = { // params for the source - ...getParamsFromSource(match(escapeRegex(rewrite?.source) ?? ""))( - pathToUse, - ), + ...getParamsFromSource(match(escapeRegex(rewrite.source)))(pathToUse), // params for the has ...rewrite.has?.reduce((acc, cur) => { return Object.assign(acc, computeHas(cur)); @@ -232,20 +229,22 @@ export function handleRewrites( } rewrittenUrl = isExternalRewrite ? `${protocol}//${rewrittenHost}${rewrittenPath}` - : `${rewrittenPath}`; + : new URL(rewrittenPath, event.url).href; + // Should we merge the query params or use only the ones from the rewrite? finalQuery = { ...query, ...convertFromQueryString(rewrittenQuery), }; + rewrittenUrl += convertToQueryString(finalQuery); debug("rewrittenUrl", { rewrittenUrl, finalQuery, isUsingParams }); } return { internalEvent: { ...event, - rawPath: rewrittenUrl, - url: `${rewrittenUrl}${convertToQueryString(finalQuery)}`, + rawPath: new URL(rewrittenUrl).pathname, + url: rewrittenUrl, }, __rewrite: rewrite, isExternalRewrite, @@ -365,7 +364,10 @@ export function fixDataPage( ...internalEvent, rawPath: newPath, query, - url: `${newPath}${convertToQueryString(query)}`, + url: new URL( + `${newPath}${convertToQueryString(query)}`, + internalEvent.url, + ).href, }; } return internalEvent; @@ -397,7 +399,7 @@ export function handleFallbackFalse( event: { ...internalEvent, rawPath: "/404", - url: "/404", + url: constructNextUrl(internalEvent.url, "/404"), headers: { ...internalEvent.headers, "x-invoke-status": "404", diff --git a/packages/open-next/src/core/routing/middleware.ts b/packages/open-next/src/core/routing/middleware.ts index 70ef37f66..cca12a7b2 100644 --- a/packages/open-next/src/core/routing/middleware.ts +++ b/packages/open-next/src/core/routing/middleware.ts @@ -56,18 +56,9 @@ export async function handleMiddleware( const hasMatch = middleMatch.some((r) => r.test(normalizedPath)); if (!hasMatch) 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 = headers.host - ? `${protocol}//${headers.host}` - : "http://localhost:3000"; - - const initialUrl = new URL(normalizedPath, host); + const initialUrl = new URL(normalizedPath, internalEvent.url); initialUrl.search = convertToQueryString(internalEvent.query); - const url = initialUrl.toString(); + const url = initialUrl.href; const middleware = await middlewareLoader(); @@ -131,12 +122,7 @@ export async function handleMiddleware( // the redirected url and end the response. if (statusCode >= 300 && statusCode < 400) { resHeaders.location = - responseHeaders - .get("location") - ?.replace( - "http://localhost:3000", - `${protocol}//${internalEvent.headers.host}`, - ) ?? resHeaders.location; + responseHeaders.get("location") ?? resHeaders.location; // res.setHeader("Location", location); return { body: emptyReadableStream(), @@ -155,28 +141,24 @@ export async function handleMiddleware( let middlewareQueryString = internalEvent.query; let newUrl = internalEvent.url; if (rewriteUrl) { + newUrl = rewriteUrl; + rewritten = true; // If not a string, it should probably throw - if (isExternal(rewriteUrl, internalEvent.headers.host as string)) { - newUrl = rewriteUrl; - rewritten = true; + if (isExternal(newUrl, internalEvent.headers.host as string)) { isExternalRewrite = true; } else { const rewriteUrlObject = new URL(rewriteUrl); - newUrl = rewriteUrlObject.pathname; // Reset the query params if the middleware is a rewrite - if (middlewareQueryString.__nextDataReq) { - middlewareQueryString = { - __nextDataReq: middlewareQueryString.__nextDataReq, - }; - } else { - middlewareQueryString = {}; - } + middlewareQueryString = middlewareQueryString.__nextDataReq + ? { + __nextDataReq: middlewareQueryString.__nextDataReq, + } + : {}; rewriteUrlObject.searchParams.forEach((v: string, k: string) => { middlewareQueryString[k] = v; }); - rewritten = true; } } @@ -198,9 +180,7 @@ export async function handleMiddleware( return { responseHeaders: resHeaders, url: newUrl, - rawPath: rewritten - ? (newUrl ?? internalEvent.rawPath) - : internalEvent.rawPath, + rawPath: new URL(newUrl).pathname, type: internalEvent.type, headers: { ...internalEvent.headers, ...reqHeaders }, body: internalEvent.body, diff --git a/packages/open-next/src/core/routing/util.ts b/packages/open-next/src/core/routing/util.ts index 9e92f2dd7..0056bf6df 100644 --- a/packages/open-next/src/core/routing/util.ts +++ b/packages/open-next/src/core/routing/util.ts @@ -74,6 +74,21 @@ export function getUrlParts(url: string, isExternal: boolean) { }; } +/** + * Creates an URL to a Next page + * + * @param baseUrl Used to get the origin + * @param path The pathname + * @returns The Next URL considering the basePath + * + * @__PURE__ + */ +export function constructNextUrl(baseUrl: string, path: string) { + const nextBasePath = NextConfig.basePath; + const url = new URL(`${nextBasePath}${path}`, baseUrl); + return url.href; +} + /** * * @__PURE__ diff --git a/packages/open-next/src/core/routingHandler.ts b/packages/open-next/src/core/routingHandler.ts index b08966b63..e10bf131a 100644 --- a/packages/open-next/src/core/routingHandler.ts +++ b/packages/open-next/src/core/routingHandler.ts @@ -26,6 +26,7 @@ import { dynamicRouteMatcher, staticRouteMatcher, } from "./routing/routeMatcher"; +import { constructNextUrl } from "./routing/util"; export const MIDDLEWARE_HEADER_PREFIX = "x-middleware-response-"; export const MIDDLEWARE_HEADER_PREFIX_LEN = MIDDLEWARE_HEADER_PREFIX.length; @@ -174,7 +175,7 @@ export default async function routingHandler( internalEvent = { ...internalEvent, rawPath: "/404", - url: "/404", + url: constructNextUrl(internalEvent.url, "/404"), headers: { ...internalEvent.headers, "x-middleware-response-cache-control": diff --git a/packages/open-next/src/overrides/converters/aws-apigw-v1.ts b/packages/open-next/src/overrides/converters/aws-apigw-v1.ts index 826cfac48..07e909ac5 100644 --- a/packages/open-next/src/overrides/converters/aws-apigw-v1.ts +++ b/packages/open-next/src/overrides/converters/aws-apigw-v1.ts @@ -4,7 +4,7 @@ import type { Converter } from "types/overrides"; import { fromReadableStream } from "utils/stream"; import { debug } from "../../adapters/logger"; -import { removeUndefinedFromQuery } from "./utils"; +import { extractHostFromHeaders, removeUndefinedFromQuery } from "./utils"; function normalizeAPIGatewayProxyEventHeaders( event: APIGatewayProxyEvent, @@ -57,13 +57,14 @@ async function convertFromAPIGatewayProxyEvent( event: APIGatewayProxyEvent, ): Promise { const { path, body, httpMethod, requestContext, isBase64Encoded } = event; + const headers = normalizeAPIGatewayProxyEventHeaders(event); return { type: "core", method: httpMethod, rawPath: path, - url: path + normalizeAPIGatewayProxyEventQueryParams(event), + url: `https://${extractHostFromHeaders(headers)}${path}${normalizeAPIGatewayProxyEventQueryParams(event)}`, body: Buffer.from(body ?? "", isBase64Encoded ? "base64" : "utf8"), - headers: normalizeAPIGatewayProxyEventHeaders(event), + headers, remoteAddress: requestContext.identity.sourceIp, query: removeUndefinedFromQuery( event.multiValueQueryStringParameters ?? {}, diff --git a/packages/open-next/src/overrides/converters/aws-apigw-v2.ts b/packages/open-next/src/overrides/converters/aws-apigw-v2.ts index 31d3ecef7..61362a08f 100644 --- a/packages/open-next/src/overrides/converters/aws-apigw-v2.ts +++ b/packages/open-next/src/overrides/converters/aws-apigw-v2.ts @@ -9,7 +9,7 @@ import { fromReadableStream } from "utils/stream"; import { debug } from "../../adapters/logger"; import { convertToQuery } from "../../core/routing/util"; -import { removeUndefinedFromQuery } from "./utils"; +import { extractHostFromHeaders, removeUndefinedFromQuery } from "./utils"; // Not sure which one is really needed as this is not documented anywhere but server actions redirect are not working without this, // it causes a 500 error from cloudfront itself with a 'x-amzErrortype: InternalFailure' header @@ -75,13 +75,14 @@ async function convertFromAPIGatewayProxyEventV2( event: APIGatewayProxyEventV2, ): Promise { const { rawPath, rawQueryString, requestContext } = event; + const headers = normalizeAPIGatewayProxyEventV2Headers(event); return { type: "core", method: requestContext.http.method, rawPath, - url: rawPath + (rawQueryString ? `?${rawQueryString}` : ""), + url: `https://${extractHostFromHeaders(headers)}${rawPath}${rawQueryString ? `?${rawQueryString}` : ""}`, body: normalizeAPIGatewayProxyEventV2Body(event), - headers: normalizeAPIGatewayProxyEventV2Headers(event), + headers, remoteAddress: requestContext.http.sourceIp, query: removeUndefinedFromQuery(convertToQuery(rawQueryString)), cookies: diff --git a/packages/open-next/src/overrides/converters/aws-cloudfront.ts b/packages/open-next/src/overrides/converters/aws-cloudfront.ts index bf44749d3..35079d627 100644 --- a/packages/open-next/src/overrides/converters/aws-cloudfront.ts +++ b/packages/open-next/src/overrides/converters/aws-cloudfront.ts @@ -18,6 +18,7 @@ import { fromReadableStream } from "utils/stream"; import { debug } from "../../adapters/logger"; import { convertToQuery, convertToQueryString } from "../../core/routing/util"; +import { extractHostFromHeaders } from "./utils"; const cloudfrontBlacklistedHeaders = [ // Disallowed headers, see: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-function-restrictions-all.html#function-restrictions-disallowed-headers @@ -82,23 +83,29 @@ function normalizeCloudFrontRequestEventHeaders( async function convertFromCloudFrontRequestEvent( event: CloudFrontRequestEvent, ): Promise { - const { method, uri, querystring, body, headers, clientIp } = - event.Records[0].cf.request; - + const { + method, + uri, + querystring, + body, + headers: cfHeaders, + clientIp, + } = event.Records[0].cf.request; + const headers = normalizeCloudFrontRequestEventHeaders(cfHeaders); return { type: "core", method, rawPath: uri, - url: uri + (querystring ? `?${querystring}` : ""), + url: `https://${extractHostFromHeaders(headers)}${uri}${querystring ? `?${querystring}` : ""}`, body: Buffer.from( body?.data ?? "", body?.encoding === "base64" ? "base64" : "utf8", ), - headers: normalizeCloudFrontRequestEventHeaders(headers), + headers, remoteAddress: clientIp, query: convertToQuery(querystring), cookies: - headers.cookie?.reduce( + cfHeaders.cookie?.reduce( (acc, cur) => { const { key = "", value } = cur; acc[key] = value; diff --git a/packages/open-next/src/overrides/converters/edge.ts b/packages/open-next/src/overrides/converters/edge.ts index 6cfcf9b99..7ac5983a8 100644 --- a/packages/open-next/src/overrides/converters/edge.ts +++ b/packages/open-next/src/overrides/converters/edge.ts @@ -59,18 +59,7 @@ const converter: Converter = { }, convertTo: async (result) => { if ("internalEvent" in result) { - let url = result.internalEvent.url; - if (!result.isExternalRewrite) { - if (result.origin) { - url = `${result.origin.protocol}://${result.origin.host}${ - result.origin.port ? `:${result.origin.port}` : "" - }${url}`; - } else { - url = `https://${result.internalEvent.headers.host}${url}`; - } - } - - const request = new Request(url, { + const request = new Request(result.internalEvent.url, { body: result.internalEvent.body, method: result.internalEvent.method, headers: { diff --git a/packages/open-next/src/overrides/converters/node.ts b/packages/open-next/src/overrides/converters/node.ts index 67410b385..ef9cf9be5 100644 --- a/packages/open-next/src/overrides/converters/node.ts +++ b/packages/open-next/src/overrides/converters/node.ts @@ -3,6 +3,7 @@ import type { IncomingMessage } from "node:http"; import { parseCookies } from "http/util"; import type { InternalResult } from "types/open-next"; import type { Converter } from "types/overrides"; +import { extractHostFromHeaders } from "./utils"; const converter: Converter = { convertFrom: async (req: IncomingMessage) => { @@ -16,22 +17,23 @@ const converter: Converter = { }); }); - const url = new URL(req.url!, `http://${req.headers.host}`); + const headers = Object.fromEntries( + Object.entries(req.headers ?? {}) + .map(([key, value]) => [ + key.toLowerCase(), + Array.isArray(value) ? value.join(",") : value, + ]) + .filter(([key]) => key), + ); + const url = new URL(req.url!, `http://${extractHostFromHeaders(headers)}`); const query = Object.fromEntries(url.searchParams.entries()); return { type: "core", method: req.method ?? "GET", rawPath: url.pathname, - url: url.pathname + url.search, + url: url.href, body, - headers: Object.fromEntries( - Object.entries(req.headers ?? {}) - .map(([key, value]) => [ - key.toLowerCase(), - Array.isArray(value) ? value.join(",") : value, - ]) - .filter(([key]) => key), - ), + headers, remoteAddress: (req.headers["x-forwarded-for"] as string) ?? req.socket.remoteAddress ?? diff --git a/packages/open-next/src/overrides/converters/utils.ts b/packages/open-next/src/overrides/converters/utils.ts index 13b7e8102..176ce298b 100644 --- a/packages/open-next/src/overrides/converters/utils.ts +++ b/packages/open-next/src/overrides/converters/utils.ts @@ -9,3 +9,15 @@ export function removeUndefinedFromQuery( } return newQuery; } + +/** + * Extract the host from the headers (default to "on") + * + * @param headers + * @returns The host + */ +export function extractHostFromHeaders( + headers: Record, +): string { + return headers["x-forwarded-host"] ?? headers.host ?? "on"; +} diff --git a/packages/open-next/src/overrides/wrappers/cloudflare-node.ts b/packages/open-next/src/overrides/wrappers/cloudflare-node.ts index 51c82ace9..738c31afd 100644 --- a/packages/open-next/src/overrides/wrappers/cloudflare-node.ts +++ b/packages/open-next/src/overrides/wrappers/cloudflare-node.ts @@ -28,13 +28,7 @@ const handler: WrapperHandler = } const internalEvent = await converter.convertFrom(request); - - // TODO: - // The edge converter populate event.url with the url including the origin. - // This is required for middleware to keep track of the protocol (i.e. http with wrangler dev). - // However the server expects that the origin is not included. - const url = new URL(internalEvent.url); - (internalEvent.url as string) = url.href.slice(url.origin.length); + const url = new URL(request.url); const { promise: promiseResponse, resolve: resolveResponse } = Promise.withResolvers(); diff --git a/packages/open-next/src/types/open-next.ts b/packages/open-next/src/types/open-next.ts index c178eb180..e57829124 100644 --- a/packages/open-next/src/types/open-next.ts +++ b/packages/open-next/src/types/open-next.ts @@ -22,6 +22,7 @@ export type BaseEventOrResult = { export type InternalEvent = { readonly method: string; readonly rawPath: string; + // Full URL - starts with "https://on/" when the host is not available readonly url: string; readonly body?: Buffer; readonly headers: Record; diff --git a/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts index 22740ddf7..74b879d83 100644 --- a/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts +++ b/packages/tests-unit/tests/converters/aws-apigw-v1.test.ts @@ -87,7 +87,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -126,7 +126,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { test: "test1,test2", @@ -167,7 +167,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/?test=test", + url: "https://on/?test=test", body: Buffer.from('{"message":"Hello, world!"}'), headers: {}, remoteAddress: "::1", @@ -208,7 +208,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -251,7 +251,7 @@ describe("convertFrom", () => { type: "core", method: "GET", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from("Hello, world!"), headers: { "content-type": "application/json", diff --git a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts index 24ec0df02..350e8a6fc 100644 --- a/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts +++ b/packages/tests-unit/tests/converters/aws-apigw-v2.test.ts @@ -126,7 +126,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -163,7 +163,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -204,7 +204,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/?hello=world&foo=1&foo=2", + url: "https://on/?hello=world&foo=1&foo=2", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -246,7 +246,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", diff --git a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts index a91a113b2..aa5797145 100644 --- a/packages/tests-unit/tests/converters/aws-cloudfront.test.ts +++ b/packages/tests-unit/tests/converters/aws-cloudfront.test.ts @@ -231,7 +231,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -271,7 +271,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/?hello=world&foo=1&foo=2", + url: "https://on/?hello=world&foo=1&foo=2", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", @@ -316,7 +316,7 @@ describe("convertFrom", () => { type: "core", method: "POST", rawPath: "/", - url: "/", + url: "https://on/", body: Buffer.from('{"message":"Hello, world!"}'), headers: { "content-type": "application/json", diff --git a/packages/tests-unit/tests/converters/node.test.ts b/packages/tests-unit/tests/converters/node.test.ts index d77fd708f..c4c35d0f1 100644 --- a/packages/tests-unit/tests/converters/node.test.ts +++ b/packages/tests-unit/tests/converters/node.test.ts @@ -16,7 +16,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/", + url: "http://on/", rawPath: "/", method: "GET", headers: { @@ -44,7 +44,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/path", + url: "http://localhost/path", rawPath: "/path", method: "GET", headers: { @@ -71,7 +71,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/path", + url: "http://on/path", rawPath: "/path", method: "GET", headers: { @@ -99,7 +99,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/path", + url: "http://on/path", rawPath: "/path", method: "GET", headers: { @@ -128,7 +128,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/path?search=1", + url: "http://localhost/path?search=1", rawPath: "/path", method: "GET", headers: { @@ -161,7 +161,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/path", + url: "http://on/path", rawPath: "/path", method: "POST", headers: { @@ -195,7 +195,7 @@ describe("convertFrom", () => { expect(result).toEqual({ type: "core", - url: "/path", + url: "http://on/path", rawPath: "/path", method: "PUT", headers: { diff --git a/packages/tests-unit/tests/core/routing/matcher.test.ts b/packages/tests-unit/tests/core/routing/matcher.test.ts index c44a3ffc0..0876215ea 100644 --- a/packages/tests-unit/tests/core/routing/matcher.test.ts +++ b/packages/tests-unit/tests/core/routing/matcher.test.ts @@ -21,15 +21,16 @@ type PartialEvent = Partial< > & { body?: string }; function createEvent(event: PartialEvent): InternalEvent { - const [rawPath, qs] = (event.url ?? "/").split("?", 2); + const url = event.url ?? "https://on/"; + const { pathname, search } = new URL(url); return { type: "core", method: event.method ?? "GET", - rawPath, + rawPath: pathname, url: event.url ?? "/", body: Buffer.from(event.body ?? ""), headers: event.headers ?? {}, - query: convertFromQueryString(qs ?? ""), + query: convertFromQueryString(search.slice(1)), cookies: event.cookies ?? {}, remoteAddress: event.remoteAddress ?? "::1", }; @@ -56,7 +57,7 @@ describe("getNextConfigHeaders", () => { it("should return request headers for matching / route", () => { const event = createEvent({ - url: "/", + url: "https://on/", }); const result = getNextConfigHeaders(event, [ @@ -79,7 +80,7 @@ describe("getNextConfigHeaders", () => { it("should return empty request headers for matching / route with empty headers", () => { const event = createEvent({ - url: "/", + url: "https://on/", }); const result = getNextConfigHeaders(event, [ @@ -95,7 +96,7 @@ describe("getNextConfigHeaders", () => { it("should return request headers for matching /* route", () => { const event = createEvent({ - url: "/hello-world", + url: "https://on/hello-world", }); const result = getNextConfigHeaders(event, [ @@ -123,7 +124,7 @@ describe("getNextConfigHeaders", () => { it("should return request headers for matching /* route with has condition", () => { const event = createEvent({ - url: "/hello-world", + url: "https://on/hello-world", cookies: { match: "true", }, @@ -150,7 +151,7 @@ describe("getNextConfigHeaders", () => { it("should return request headers for matching /* route with missing condition", () => { const event = createEvent({ - url: "/hello-world", + url: "https://on/hello-world", cookies: { match: "true", }, @@ -177,7 +178,7 @@ describe("getNextConfigHeaders", () => { it("should return request headers for matching /* route with has and missing condition", () => { const event = createEvent({ - url: "/hello-world", + url: "https://on/hello-world", cookies: { match: "true", }, @@ -211,18 +212,18 @@ describe("getNextConfigHeaders", () => { describe("handleRedirects", () => { it("should redirect trailing slash by default", () => { const event = createEvent({ - url: "/api-route/", + url: "https://on/api-route/", }); const result = handleRedirects(event, []); expect(result.statusCode).toEqual(308); - expect(result.headers.Location).toEqual("/api-route"); + expect(result.headers.Location).toEqual("https://on/api-route"); }); it("should not redirect trailing slash when skipTrailingSlashRedirect is true", () => { const event = createEvent({ - url: "/api-route/", + url: "https://on/api-route/", }); NextConfig.skipTrailingSlashRedirect = true; @@ -233,7 +234,7 @@ describe("handleRedirects", () => { it("should redirect matching path", () => { const event = createEvent({ - url: "/api-route", + url: "https://on/api-route", }); const result = handleRedirects(event, [ @@ -246,12 +247,12 @@ describe("handleRedirects", () => { }, ]); - expect(result.headers.Location).toBe("/new/api-route"); + expect(result.headers.Location).toBe("https://on/new/api-route"); }); it("should redirect matching nested path", () => { const event = createEvent({ - url: "/api-route/secret", + url: "https://on/api-route/secret", }); const result = handleRedirects(event, [ @@ -264,12 +265,12 @@ describe("handleRedirects", () => { }, ]); - expect(result.headers.Location).toBe("/new/api-route/secret"); + expect(result.headers.Location).toBe("https://on/new/api-route/secret"); }); it("should not redirect unmatched path", () => { const event = createEvent({ - url: "/api-route", + url: "https://on/api-route", }); const result = handleRedirects(event, [ @@ -287,7 +288,7 @@ describe("handleRedirects", () => { it("should redirect with + character and query string", () => { const event = createEvent({ - url: "/foo", + url: "https://on/foo", }); const result = handleRedirects(event, [ @@ -302,7 +303,7 @@ describe("handleRedirects", () => { expect(result.statusCode).toEqual(308); expect(result.headers.Location).toEqual( - "/search?bar=hello+world&baz=new%2C+earth", + "https://on/search?bar=hello+world&baz=new%2C+earth", ); }); }); @@ -310,7 +311,7 @@ describe("handleRedirects", () => { describe("handleRewrites", () => { it("should not rewrite with empty rewrites", () => { const event = createEvent({ - url: "/foo?hellp=world", + url: "https://on/foo?hello=world", }); const result = handleRewrites(event, []); @@ -323,7 +324,7 @@ describe("handleRewrites", () => { it("should rewrite with params", () => { const event = createEvent({ - url: "/albums/foo/bar", + url: "https://on/albums/foo/bar", }); const rewrites = [ @@ -344,7 +345,7 @@ describe("handleRewrites", () => { internalEvent: { ...event, rawPath: "/rewrite/albums/foo/bar", - url: "/rewrite/albums/foo/bar", + url: "https://on/rewrite/albums/foo/bar", }, __rewrite: rewrites[1], isExternalRewrite: false, @@ -353,7 +354,7 @@ describe("handleRewrites", () => { it("should rewrite without params", () => { const event = createEvent({ - url: "/foo", + url: "https://on/foo", }); const rewrites = [ @@ -369,7 +370,7 @@ describe("handleRewrites", () => { internalEvent: { ...event, rawPath: "/bar", - url: "/bar", + url: "https://on/bar", }, __rewrite: rewrites[0], isExternalRewrite: false, @@ -378,7 +379,7 @@ describe("handleRewrites", () => { it("should rewrite externally", () => { const event = createEvent({ - url: "/albums/foo/bar", + url: "https://on/albums/foo/bar", }); const rewrites = [ @@ -393,7 +394,7 @@ describe("handleRewrites", () => { expect(result).toEqual({ internalEvent: { ...event, - rawPath: "https://external.com/search", + rawPath: "/search", url: "https://external.com/search?album=foo&song=bar", }, __rewrite: rewrites[0], @@ -403,7 +404,7 @@ describe("handleRewrites", () => { it("should rewrite with matching path with has condition", () => { const event = createEvent({ - url: "/albums/foo?has=true", + url: "https://on/albums/foo?has=true", }); const rewrites = [ @@ -426,7 +427,7 @@ describe("handleRewrites", () => { internalEvent: { ...event, rawPath: "/rewrite/albums/foo", - url: "/rewrite/albums/foo?has=true", + url: "https://on/rewrite/albums/foo?has=true", }, __rewrite: rewrites[0], isExternalRewrite: false, @@ -435,7 +436,7 @@ describe("handleRewrites", () => { it("should rewrite with matching path with missing condition", () => { const event = createEvent({ - url: "/albums/foo", + url: "https://on/albums/foo", headers: { has: "true", }, @@ -460,7 +461,7 @@ describe("handleRewrites", () => { internalEvent: { ...event, rawPath: "/rewrite/albums/foo", - url: "/rewrite/albums/foo", + url: "https://on/rewrite/albums/foo", }, __rewrite: rewrites[0], isExternalRewrite: false, @@ -471,7 +472,7 @@ describe("handleRewrites", () => { describe("fixDataPage", () => { it("should return 404 for data requests that don't match the buildId", () => { const event = createEvent({ - url: "/_next/data/xyz/test", + url: "https://on/_next/data/xyz/test", }); const response = fixDataPage(event, "abc"); @@ -481,7 +482,7 @@ describe("fixDataPage", () => { it("should not return 404 for data requests that don't match the buildId", () => { const event = createEvent({ - url: "/_next/data/abc/test", + url: "https://on/_next/data/abc/test", }); const response = fixDataPage(event, "abc"); @@ -494,7 +495,7 @@ describe("fixDataPage", () => { NextConfig.basePath = "/base"; const event = createEvent({ - url: "/base/_next/data/abc/test", + url: "https://on/base/_next/data/abc/test", }); const response = fixDataPage(event, "abc"); @@ -507,7 +508,7 @@ describe("fixDataPage", () => { it("should remove json extension from data requests and add __nextDataReq to query", () => { const event = createEvent({ - url: "/_next/data/abc/test/file.json?hello=world", + url: "https://on/_next/data/abc/test/file.json?hello=world", }); const response = fixDataPage(event, "abc"); @@ -515,7 +516,7 @@ describe("fixDataPage", () => { expect(response).toEqual({ ...event, rawPath: "/test/file", - url: "/test/file?hello=world&__nextDataReq=1", + url: "https://on/test/file?hello=world&__nextDataReq=1", }); }); @@ -523,7 +524,7 @@ describe("fixDataPage", () => { NextConfig.basePath = "/base"; const event = createEvent({ - url: "/base/_next/data/abc/test/file.json?hello=world", + url: "https://on/base/_next/data/abc/test/file.json?hello=world", }); const response = fixDataPage(event, "abc"); @@ -531,7 +532,7 @@ describe("fixDataPage", () => { expect(response).toEqual({ ...event, rawPath: "/test/file", - url: "/test/file?hello=world&__nextDataReq=1", + url: "https://on/test/file?hello=world&__nextDataReq=1", }); NextConfig.basePath = undefined; diff --git a/packages/tests-unit/tests/core/routing/middleware.test.ts b/packages/tests-unit/tests/core/routing/middleware.test.ts index 541c57579..8229e0b28 100644 --- a/packages/tests-unit/tests/core/routing/middleware.test.ts +++ b/packages/tests-unit/tests/core/routing/middleware.test.ts @@ -1,8 +1,5 @@ import { handleMiddleware } from "@opennextjs/aws/core/routing/middleware.js"; -import { - convertFromQueryString, - isExternal, -} from "@opennextjs/aws/core/routing/util.js"; +import { convertFromQueryString } from "@opennextjs/aws/core/routing/util.js"; import type { InternalEvent } from "@opennextjs/aws/types/open-next.js"; import { toReadableStream } from "@opennextjs/aws/utils/stream.js"; import { vi } from "vitest"; @@ -51,25 +48,16 @@ type PartialEvent = Partial< > & { body?: string }; function createEvent(event: PartialEvent): InternalEvent { - let rawPath: string; - let qs: string; - if (isExternal(event.url)) { - const url = new URL(event.url!); - rawPath = url.pathname; - qs = url.search; - } else { - const parts = (event.url ?? "/").split("?", 2); - rawPath = parts[0]; - qs = parts[1] ?? ""; - } + const url = event.url ?? "https://on/"; + const { pathname, search } = new URL(url); return { type: "core", method: event.method ?? "GET", - rawPath, - url: event.url ?? "/", + rawPath: pathname, + url, body: Buffer.from(event.body ?? ""), headers: event.headers ?? {}, - query: convertFromQueryString(qs), + query: convertFromQueryString(search.slice(1)), cookies: event.cookies ?? {}, remoteAddress: event.remoteAddress ?? "::1", }; @@ -142,7 +130,7 @@ describe("handleMiddleware", () => { expect(result).toEqual({ ...event, rawPath: "/rewrite", - url: "/rewrite", + url: "http://localhost/rewrite", responseHeaders: { "x-middleware-rewrite": "http://localhost/rewrite", }, @@ -152,7 +140,7 @@ describe("handleMiddleware", () => { it("should invoke middleware with rewrite with __nextDataReq", async () => { const event = createEvent({ - url: "/rewrite?__nextDataReq=1&key=value", + url: "https://on/rewrite?__nextDataReq=1&key=value", headers: { host: "localhost", }, @@ -168,7 +156,7 @@ describe("handleMiddleware", () => { expect(result).toEqual({ ...event, rawPath: "/rewrite", - url: "/rewrite", + url: "http://localhost/rewrite?newKey=value", responseHeaders: { "x-middleware-rewrite": "http://localhost/rewrite?newKey=value", }, @@ -196,7 +184,7 @@ describe("handleMiddleware", () => { expect(middlewareLoader).toHaveBeenCalled(); expect(result).toEqual({ ...event, - rawPath: "http://external/rewrite", + rawPath: "/rewrite", url: "http://external/rewrite", responseHeaders: { "x-middleware-rewrite": "http://external/rewrite", @@ -303,7 +291,7 @@ describe("handleMiddleware", () => { it("should default to https protocol", async () => { const event = createEvent({ - url: "/path", + url: "https://test.me/path", headers: { host: "test.me", },