Skip to content
Open
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
54 changes: 45 additions & 9 deletions src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@

const ENABLE_DRAFT_MODE_ROUTE = "/api/preview";

/**
* Shape of a single entry returned by the Website Builder's `GET /wb/redirects` endpoint.
* Declared locally on purpose: middleware runs on every request, so it stays free of runtime
* dependencies, and `@webiny/website-builder-sdk` (where this type lives) is not a direct
* dependency of this project.
*/
interface PublicRedirect {
id: string;
from: string;
to: string;
permanent: boolean;
}

export async function middleware(request: NextRequest) {
const { searchParams, pathname } = request.nextUrl;
// Check if the preview/editing flag is set.
Expand Down Expand Up @@ -57,23 +70,46 @@
}

// Check if there's a redirect defined for the requested page.
const redirectsUrl = new URL(
`/api/redirects?wb.tenant=${tenantId}&pathname=${encodeURIComponent(pathname)}`,
request.url,
);

//
// This queries the Website Builder API directly rather than fetching our own /api/redirects
// route. Fetching our own origin from middleware costs a second function invocation and a full
// network round trip on every request, and it breaks in ways that are hard to see:
//
// - Behind a local HTTPS proxy the certificate isn't trusted by the Edge runtime, so the
// request throws (SELF_SIGNED_CERT_IN_CHAIN) and every redirect silently stops working.
// - On a deployment protected by Vercel Authentication, the self-request carries no
// credentials and is answered with a 401 or an auth redirect instead of our route.
//
// Talking to the API directly avoids both: it is the only host we need to reach, and it has a
// real certificate and its own authentication.
try {
const redirectResponse = await fetch(redirectsUrl);
const response = await fetch(
`${process.env.NEXT_PUBLIC_WEBSITE_BUILDER_API_HOST}/wb/redirects`,
{
headers: {
"X-Tenant": tenantId,
Authorization: `Bearer ${process.env.NEXT_PUBLIC_WEBSITE_BUILDER_API_KEY}`,
},
},
);

if (!response.ok) {
throw new Error(`Redirects lookup responded with ${response.status}.`);
}

const redirects: PublicRedirect[] = await response.json();
const redirect = redirects.find((item) => item.from === pathname);

const { redirect } = await redirectResponse.json();
if (redirect) {
return NextResponse.redirect(
new URL(redirect.to, request.url),
redirect.permanent ? 308 : 307,
);
}
} catch {
// Do nothing. Most probably redirect was simply not found.
} catch (err) {
// A failed lookup must not take the page down, but it must not be silent either: swallowing it
// is indistinguishable from "no redirect is configured" and hides real API failures.
console.error(`[middleware] Redirect lookup failed for "${pathname}":`, err);
}

// For all other requests, continue as normal without any modifications.
Expand Down