diff --git a/README.md b/README.md index a23fed2..ea87534 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ const middleware = createRedirectionIoMiddleware({ // Optional: matcher to specify which routes should be ignored by redirection.io middleware // Default: "^/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)$" matcherRegex: "^/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)$", + // Optional: If light, redirection.io middleware will only redirect and not override the response + // Default: "full" + mode: "light", + // Optional: If true, redirection.io middleware will log information in Redirection.io + // Default: true + logged: true, }); export default middleware; @@ -78,13 +84,30 @@ createRedirectionIoMiddleware({ matcherRegex: null }); Here's a summary of the middleware options: -| Option | Type | Description | -| -------------------- | -------------- | ---------------------------------------------------------------------------- | -| `previousMiddleware` | Function | Middleware to be executed before redirection.io middleware | -| `nextMiddleware` | Function | Middleware to be executed after redirection.io middleware | -| `matcherRegex` | String or null | Regex to specify which routes should be handled by redirection.io middleware | +| Option | Type | Description | +| -------------------- | ----------------- | -------------------------------------------------------------------------------------------------------- | +| `previousMiddleware` | Function | Middleware to be executed before redirection.io middleware | +| `nextMiddleware` | Function | Middleware to be executed after redirection.io middleware | +| `matcherRegex` | String or null | Regex to specify which routes should be handled by redirection.io middleware | +| `mode` | `full` or `light` | If `light`, redirection.io middleware will only redirect and not override the response (default: `full`) | +| `logged` | Boolean | If true, redirection.io middleware will log information in Redirection.io (default: `true`) | -### Next.js +## Light mode + +The response rewriting features (e.g., SEO overrides, custom body, etc.) of redirection.io are currently not compatible with React Server Components (RSC). This is due to the fact that Vercel’s middleware implementation does not follow standard middleware protocols, requiring us to fetch requests, which is incompatible with both RSC and Vercel’s implementation. + +However, we provide a light mode that supports RSC by offering only the redirection functionality. To enable this mode, simply set the `mode` option to `light`. + +This allows you to implement redirection behavior without modifying response content, ensuring smooth operation with RSC. + +```typescript +const middleware = createRedirectionIoMiddleware({ + // … + mode: "light", +}); +``` + +## Next.js If you are using next.js middlewares, you can use the `createRedirectionIoMiddleware` method from `@redirection.io/vercel-middleware/next` which is compatible with `NextRequest` type. diff --git a/middleware.d.ts b/middleware.d.ts index 3007b30..8595c5d 100644 --- a/middleware.d.ts +++ b/middleware.d.ts @@ -5,6 +5,8 @@ type CreateMiddlewareConfig = { previousMiddleware?: Middleware; nextMiddleware?: Middleware; matcherRegex?: string | null; + mode?: "full" | "light"; + logged?: boolean; }; export declare const createRedirectionIoMiddleware: (config: CreateMiddlewareConfig) => Middleware; declare const defaultMiddleware: Middleware; diff --git a/middleware.js b/middleware.js index dd50300..490c014 100644 --- a/middleware.js +++ b/middleware.js @@ -1,6 +1,7 @@ import { next } from "@vercel/edge"; import { ipAddress } from "@vercel/functions"; import * as redirectionio from "@redirection.io/redirectionio"; +import { NextResponse } from "next/server"; const REDIRECTIONIO_TOKEN = process.env.REDIRECTIONIO_TOKEN || ""; const REDIRECTIONIO_INSTANCE_NAME = process.env.REDIRECTIONIO_INSTANCE_NAME || "redirection-io-vercel-middleware"; const REDIRECTIONIO_VERSION = "redirection-io-vercel-middleware/0.3.12"; @@ -10,6 +11,8 @@ const REDIRECTIONIO_ADD_HEADER_RULE_IDS = process.env.REDIRECTIONIO_ADD_HEADER_R const REDIRECTIONIO_TIMEOUT = process.env.REDIRECTIONIO_TIMEOUT ? parseInt(process.env.REDIRECTIONIO_TIMEOUT, 10) : 500; const DEFAULT_CONFIG = { matcherRegex: "^/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)$", + mode: "full", + logged: true, }; export const createRedirectionIoMiddleware = (config) => { return async (request, context) => { @@ -41,13 +44,17 @@ export const createRedirectionIoMiddleware = (config) => { } middlewareRequest = middlewareResponseToRequest(middlewareRequest, response, body); } - return handler(middlewareRequest, context, async (request, useFetch) => { + return handler(middlewareRequest, context, config, async (request, useFetch) => { let response = null; if (config.nextMiddleware) { response = await config.nextMiddleware(request, context); if (response.status !== 200) { return response; } + // If light mode, only return the response + if (config.mode === "light") { + return response; + } request = middlewareResponseToRequest(request, response, body); } if (!useFetch) { @@ -71,7 +78,7 @@ export const createRedirectionIoMiddleware = (config) => { }; const defaultMiddleware = createRedirectionIoMiddleware({}); export default defaultMiddleware; -async function handler(request, context, fetchResponse) { +async function handler(request, context, config, fetchResponse) { if (!REDIRECTIONIO_TOKEN) { console.warn("No REDIRECTIONIO_TOKEN environment variable found. Skipping redirection.io middleware."); return fetchResponse(request, false); @@ -87,14 +94,20 @@ async function handler(request, context, fetchResponse) { }); const url = new URL(request.url); const location = response.headers.get("Location"); - if (location && location.startsWith("/")) { + const hasLocation = location && location.startsWith("/"); + if (hasLocation) { response.headers.set("Location", url.origin + location); } - context.waitUntil( - (async function () { - await log(response, backendStatusCode, redirectionIORequest, startTimestamp, action, ip); - })(), - ); + if (config.logged) { + context.waitUntil( + (async function () { + await log(response, backendStatusCode, redirectionIORequest, startTimestamp, action, ip); + })(), + ); + } + if (config.mode === "light" && hasLocation) { + return NextResponse.redirect(url.origin + location, response.status); + } return response; } function splitSetCookies(cookiesString) { diff --git a/middleware.ts b/middleware.ts index b168236..502d8ba 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,7 +1,7 @@ import { next, RequestContext } from "@vercel/edge"; import { ipAddress } from "@vercel/functions"; import * as redirectionio from "@redirection.io/redirectionio"; -import type { NextRequest } from "next/server"; +import { NextResponse, type NextRequest } from "next/server"; const REDIRECTIONIO_TOKEN = process.env.REDIRECTIONIO_TOKEN || ""; const REDIRECTIONIO_INSTANCE_NAME = process.env.REDIRECTIONIO_INSTANCE_NAME || "redirection-io-vercel-middleware"; @@ -13,7 +13,9 @@ const REDIRECTIONIO_TIMEOUT = process.env.REDIRECTIONIO_TIMEOUT ? parseInt(proce const DEFAULT_CONFIG = { matcherRegex: "^/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)$", -}; + mode: "full", + logged: true, +} as const; type Middleware = (request: Request | NextRequest, context: RequestContext) => Response | Promise; @@ -23,6 +25,8 @@ type CreateMiddlewareConfig = { previousMiddleware?: Middleware; nextMiddleware?: Middleware; matcherRegex?: string | null; + mode?: "full" | "light"; + logged?: boolean; }; export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): Middleware => { @@ -64,7 +68,7 @@ export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): M middlewareRequest = middlewareResponseToRequest(middlewareRequest, response, body); } - return handler(middlewareRequest, context, async (request, useFetch): Promise => { + return handler(middlewareRequest, context, config, async (request, useFetch): Promise => { let response: Response | null = null; if (config.nextMiddleware) { @@ -74,6 +78,11 @@ export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): M return response; } + // If light mode, only return the response + if (config.mode === "light") { + return response; + } + request = middlewareResponseToRequest(request, response, body); } @@ -105,7 +114,12 @@ const defaultMiddleware = createRedirectionIoMiddleware({}); export default defaultMiddleware; -async function handler(request: Request, context: RequestContext, fetchResponse: FetchResponse): Promise { +async function handler( + request: Request, + context: RequestContext, + config: CreateMiddlewareConfig, + fetchResponse: FetchResponse, +): Promise { if (!REDIRECTIONIO_TOKEN) { console.warn("No REDIRECTIONIO_TOKEN environment variable found. Skipping redirection.io middleware."); @@ -127,16 +141,23 @@ async function handler(request: Request, context: RequestContext, fetchResponse: const url = new URL(request.url); const location = response.headers.get("Location"); + const hasLocation = location && location.startsWith("/"); - if (location && location.startsWith("/")) { + if (hasLocation) { response.headers.set("Location", url.origin + location); } - context.waitUntil( - (async function () { - await log(response, backendStatusCode, redirectionIORequest, startTimestamp, action, ip); - })(), - ); + if (config.logged) { + context.waitUntil( + (async function () { + await log(response, backendStatusCode, redirectionIORequest, startTimestamp, action, ip); + })(), + ); + } + + if (config.mode === "light" && hasLocation) { + return NextResponse.redirect(url.origin + location, response.status); + } return response; } diff --git a/next.d.ts b/next.d.ts index 4f3f4f7..b0422e4 100644 --- a/next.d.ts +++ b/next.d.ts @@ -4,6 +4,8 @@ type CreateMiddlewareConfig = { previousMiddleware?: Middleware; nextMiddleware?: Middleware; matcherRegex?: string | null; + mode?: "full" | "light"; + logged?: boolean; }; export declare const createRedirectionIoMiddleware: (config: CreateMiddlewareConfig) => Middleware; export {}; diff --git a/next.js b/next.js index 671249d..4353cdb 100644 --- a/next.js +++ b/next.js @@ -20,6 +20,8 @@ export const createRedirectionIoMiddleware = (config) => { previousMiddleware, nextMiddleware, ...(configMatcherRegex ? { matcherRegex: configMatcherRegex } : {}), + mode: config.mode ?? "full", + logged: config.logged ?? true, }); return async (req, context) => { const response = await edgeMiddleware(req, context); diff --git a/next.ts b/next.ts index 31ee6fb..a33ddb6 100644 --- a/next.ts +++ b/next.ts @@ -8,6 +8,8 @@ type CreateMiddlewareConfig = { previousMiddleware?: Middleware; nextMiddleware?: Middleware; matcherRegex?: string | null; + mode?: "full" | "light"; + logged?: boolean; }; export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): Middleware => { @@ -34,6 +36,8 @@ export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): M previousMiddleware, nextMiddleware, ...(configMatcherRegex ? { matcherRegex: configMatcherRegex } : {}), + mode: config.mode ?? "full", + logged: config.logged ?? true, }); return async (req: NextRequest, context: NextFetchEvent) => {