Skip to content

Commit

Permalink
feat(middleware): add restricted option
Browse files Browse the repository at this point in the history
  • Loading branch information
baptadn committed Nov 6, 2024
1 parent 26f9629 commit 8f3c9f7
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 50 deletions.
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ 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 true, redirection.io middleware will only redirect and not override the response
// Default: false
restricted: true,
});

export default middleware;
Expand All @@ -78,13 +81,29 @@ 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`) |
## Light mode

### Next.js
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.
Expand Down
2 changes: 2 additions & 0 deletions middleware.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
73 changes: 38 additions & 35 deletions middleware.js
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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) => {
Expand All @@ -22,10 +25,8 @@ export const createRedirectionIoMiddleware = (config) => {
return next();
}
// Avoid infinite loop
if (
request.headers.get("x-redirectionio-middleware") === "true" ||
request.headers.get("User-Agent") === "Vercel Edge Functions"
) {
if (request.headers.get("x-redirectionio-middleware") === "true" ||
request.headers.get("User-Agent") === "Vercel Edge Functions") {
return next();
}
const body = request.body ? await request.arrayBuffer() : null;
Expand All @@ -41,13 +42,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) {
Expand All @@ -71,7 +76,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);
Expand All @@ -87,14 +92,18 @@ 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 () {
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) {
Expand Down Expand Up @@ -137,12 +146,14 @@ function splitSetCookies(cookiesString) {
pos = nextStart;
cookiesStrings.push(cookiesString.substring(start, lastComma));
start = pos;
} else {
}
else {
// in param ',' or param separator ';',
// we continue from that comma
pos = lastComma + 1;
}
} else {
}
else {
pos += 1;
}
}
Expand All @@ -154,12 +165,7 @@ function splitSetCookies(cookiesString) {
}
function createRedirectionIORequest(request, ip) {
const urlObject = new URL(request.url);
const redirectionioRequest = new redirectionio.Request(
urlObject.pathname + urlObject.search,
urlObject.host,
urlObject.protocol.replace(":", ""),
request.method,
);
const redirectionioRequest = new redirectionio.Request(urlObject.pathname + urlObject.search, urlObject.host, urlObject.protocol.replace(":", ""), request.method);
request.headers.forEach((value, key) => {
redirectionioRequest.add_header(key, value);
});
Expand Down Expand Up @@ -195,7 +201,7 @@ function middlewareResponseToRequest(originalRequest, response, body) {
}
async function fetchRedirectionIOAction(redirectionIORequest) {
try {
const response = await Promise.race([
const response = (await Promise.race([
fetch("https://agent.redirection.io/" + REDIRECTIONIO_TOKEN + "/action", {
method: "POST",
body: redirectionIORequest.serialize().toString(),
Expand All @@ -206,13 +212,14 @@ async function fetchRedirectionIOAction(redirectionIORequest) {
cache: "no-store",
}),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), REDIRECTIONIO_TIMEOUT)),
]);
]));
const actionStr = await response.text();
if (actionStr === "") {
return redirectionio.Action.empty();
}
return new redirectionio.Action(actionStr);
} catch (e) {
}
catch (e) {
console.error(e);
return redirectionio.Action.empty();
}
Expand All @@ -224,7 +231,8 @@ async function proxy(request, action, fetchResponse) {
let response;
if (statusCodeBeforeResponse === 0) {
response = await fetchResponse(request, true);
} else {
}
else {
response = new Response("", {
status: Number(statusCodeBeforeResponse),
});
Expand All @@ -242,7 +250,8 @@ async function proxy(request, action, fetchResponse) {
for (const cookie of cookies) {
headerMap.add_header("set-cookie", cookie);
}
} else {
}
else {
headerMap.add_header(key, value);
}
});
Expand All @@ -268,7 +277,8 @@ async function proxy(request, action, fetchResponse) {
const { readable, writable } = new TransformStream();
createBodyFilter(response.body, writable, bodyFilter);
return [new Response(readable, response), backendStatusCode];
} catch (err) {
}
catch (err) {
console.error(err);
const response = await fetchResponse(request, true);
return [response, response.status];
Expand Down Expand Up @@ -315,15 +325,7 @@ async function log(response, backendStatusCode, redirectionioRequest, startTimes
return;
}
try {
const logAsJson = redirectionio.create_log_in_json(
redirectionioRequest,
response.status,
responseHeaderMap,
action,
"vercel-edge-middleware/" + REDIRECTIONIO_VERSION,
BigInt(startTimestamp),
clientIP ?? "",
);
const logAsJson = redirectionio.create_log_in_json(redirectionioRequest, response.status, responseHeaderMap, action, "vercel-edge-middleware/" + REDIRECTIONIO_VERSION, BigInt(startTimestamp), clientIP ?? "");
return await fetch("https://agent.redirection.io/" + REDIRECTIONIO_TOKEN + "/log", {
method: "POST",
body: logAsJson,
Expand All @@ -333,7 +335,8 @@ async function log(response, backendStatusCode, redirectionioRequest, startTimes
},
cache: "no-store",
});
} catch (err) {
}
catch (err) {
console.error(err);
}
}
39 changes: 30 additions & 9 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,6 +13,8 @@ const REDIRECTIONIO_TIMEOUT = process.env.REDIRECTIONIO_TIMEOUT ? parseInt(proce

const DEFAULT_CONFIG = {
matcherRegex: "^/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)$",
mode: "full",
logged: true,
};

type Middleware = (request: Request | NextRequest, context: RequestContext) => Response | Promise<Response>;
Expand All @@ -23,6 +25,8 @@ type CreateMiddlewareConfig = {
previousMiddleware?: Middleware;
nextMiddleware?: Middleware;
matcherRegex?: string | null;
mode?: "full" | "light";
logged?: boolean;
};

export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): Middleware => {
Expand Down Expand Up @@ -64,7 +68,7 @@ export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): M
middlewareRequest = middlewareResponseToRequest(middlewareRequest, response, body);
}

return handler(middlewareRequest, context, async (request, useFetch): Promise<Response> => {
return handler(middlewareRequest, context, config, async (request, useFetch): Promise<Response> => {
let response: Response | null = null;

if (config.nextMiddleware) {
Expand All @@ -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);
}

Expand Down Expand Up @@ -105,7 +114,12 @@ const defaultMiddleware = createRedirectionIoMiddleware({});

export default defaultMiddleware;

async function handler(request: Request, context: RequestContext, fetchResponse: FetchResponse): Promise<Response> {
async function handler(
request: Request,
context: RequestContext,
config: CreateMiddlewareConfig,
fetchResponse: FetchResponse,
): Promise<Response> {
if (!REDIRECTIONIO_TOKEN) {
console.warn("No REDIRECTIONIO_TOKEN environment variable found. Skipping redirection.io middleware.");

Expand All @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
2 changes: 2 additions & 0 deletions next.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type CreateMiddlewareConfig = {
previousMiddleware?: Middleware;
nextMiddleware?: Middleware;
matcherRegex?: string | null;
mode?: "full" | "light";
logged?: boolean;
};

export const createRedirectionIoMiddleware = (config: CreateMiddlewareConfig): Middleware => {
Expand All @@ -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) => {
Expand Down

0 comments on commit 8f3c9f7

Please sign in to comment.