diff --git a/bifrost/renderer/bifrost/onAfterRenderClient.ts b/bifrost/renderer/bifrost/onAfterRenderClient.ts index 2b497eb..7217780 100644 --- a/bifrost/renderer/bifrost/onAfterRenderClient.ts +++ b/bifrost/renderer/bifrost/onAfterRenderClient.ts @@ -2,10 +2,10 @@ import { PageContextClient } from "vike/types"; import "../../lib/type"; import { Turbolinks } from "../../lib/turbolinks"; -export default async function bifrostOnAfterRenderClient( +export default function bifrostOnAfterRenderClient( pageContext: PageContextClient ) { if (!pageContext.isHydration && !pageContext.errorWhileRendering) { - await Turbolinks._vikeAfterRender(pageContext._turbolinksVisit, false); + Turbolinks._vikeAfterRender(pageContext._turbolinksVisit, false); } } diff --git a/bifrost/renderer/wrapped/onBeforeRender.client.ts b/bifrost/renderer/wrapped/onBeforeRender.client.ts index c235001..929e845 100644 --- a/bifrost/renderer/wrapped/onBeforeRender.client.ts +++ b/bifrost/renderer/wrapped/onBeforeRender.client.ts @@ -51,7 +51,15 @@ export default async function wrappedOnBeforeRender( */ const resp = await fetch(pageContext.urlParsed.href, { headers: { ...pageContext.config.proxyHeaders, accept: "text/html" }, - }); + }).catch(() => {}); + + if (!resp) { + // hard reload. can happen on cors errors when redirected to external page + window.location.href = pageContext.urlParsed.href; + // stop vike rendering to let navigation happen + await new Promise(() => {}); + return; + } if (resp.redirected) { const parsedUrl = new URL(resp.url); diff --git a/tests/alb/index.mjs b/tests/alb/index.mjs index f3380cb..a56918c 100644 --- a/tests/alb/index.mjs +++ b/tests/alb/index.mjs @@ -21,6 +21,7 @@ const BIFROST_PATHS = [ "/redirect-page", "/auto-navigate", "/proxy-to", + "/cors-test", "/", ]; diff --git a/tests/e2e/specs/e2e.spec.ts b/tests/e2e/specs/e2e.spec.ts index fcad089..604977f 100644 --- a/tests/e2e/specs/e2e.spec.ts +++ b/tests/e2e/specs/e2e.spec.ts @@ -436,6 +436,30 @@ test.describe("redirects", () => { await customProxy.goBack(); }); + test("on client navigation, redirect to external url", async ({ page }) => { + const externalUrl = "http://localhost:5555/cors-test"; + ensureAllNetworkSucceeds(page); + const customProxy = new CustomProxyPage(page, { + title: "first page", + links: [ + { + redirectTo: { + url: externalUrl, + title: "external", + }, + }, + ], + }); + await customProxy.goto(); + await customProxy.clickLink("external", { + browserReload: true, + waitFor: 0, + }); + + // back button works as expected + await customProxy.goBack(); + }); + test("sets cookies along the way", async ({ page, context, baseURL }) => { ensureAllNetworkSucceeds(page); const customProxy = new CustomProxyPage(page, { @@ -1042,12 +1066,15 @@ test.describe("back button restoration", () => { await customProxy.goto(); const edit = page.getByRole("link").first(); - await page.evaluate((rRoot: any) => { - rRoot.appendChild(document.createTextNode("edit1")); - document.addEventListener("turbolinks:before-cache", () => { - rRoot.appendChild(document.createTextNode("edit2")); - }); - }, await edit.elementHandle()); + await page.evaluate( + (rRoot: any) => { + rRoot.appendChild(document.createTextNode("edit1")); + document.addEventListener("turbolinks:before-cache", () => { + rRoot.appendChild(document.createTextNode("edit2")); + }); + }, + await edit.elementHandle() + ); await expect(edit).toContainText("edit1"); await expect(edit).not.toContainText("edit2"); @@ -1065,12 +1092,15 @@ test.describe("back button restoration", () => { await page.goto("./vite-page"); await waitForTurbolinksInit(page); const edit = page.getByRole("link").first(); - await page.evaluate((rRoot: any) => { - rRoot.appendChild(document.createTextNode("edit1")); - document.addEventListener("turbolinks:before-cache", () => { - rRoot.appendChild(document.createTextNode("edit2")); - }); - }, await edit.elementHandle()); + await page.evaluate( + (rRoot: any) => { + rRoot.appendChild(document.createTextNode("edit1")); + document.addEventListener("turbolinks:before-cache", () => { + rRoot.appendChild(document.createTextNode("edit2")); + }); + }, + await edit.elementHandle() + ); await expect(edit).toContainText("edit1"); await expect(edit).not.toContainText("edit2"); diff --git a/tests/fake-backend/index.ts b/tests/fake-backend/index.ts index 491c1a1..f9bbd6c 100644 --- a/tests/fake-backend/index.ts +++ b/tests/fake-backend/index.ts @@ -27,7 +27,12 @@ app.get(["/custom", "/custom-:id"], async (req, res) => { } if ("redirectTo" in data) { res.status(302); - res.setHeader("location", `${publicUrl}${toPath(data.redirectTo)}`); + + res.setHeader( + "location", + ("url" in data.redirectTo && data.redirectTo.url) || + `${publicUrl}${toPath(data.redirectTo)}` + ); if (data.cookies) { for (const [key, val] of Object.entries(data.cookies)) { res.setHeader("set-cookie", key + "=" + val); @@ -80,7 +85,10 @@ app.get("/script-wrapped", async (req, res) => { res.setHeader("X-REACT-LAYOUT", "no_layout"); } res.setHeader("Content-Type", "text/html"); - res.status(200).type("text/html").send(""); + res + .status(200) + .type("text/html") + .send(""); }); app.get("/json-wrapped", async (req, res) => { diff --git a/tests/fake-backend/page-builder.ts b/tests/fake-backend/page-builder.ts index 7dbb15d..e5d5726 100644 --- a/tests/fake-backend/page-builder.ts +++ b/tests/fake-backend/page-builder.ts @@ -5,6 +5,7 @@ type LinkOptions = { }; export type PageDataOk = { endpoint?: string; + url?: string; title: string; bodyAttrs?: string; layout?: string; @@ -29,7 +30,7 @@ export function toPath(data: PageData) { } return ( `/${"endpoint" in data ? data!.endpoint : "custom"}?page=` + - encodeURI(JSON.stringify(data)) + encodeURIComponent(JSON.stringify(data)) ); } export enum Turbolinks { diff --git a/tests/vite/server/index.ts b/tests/vite/server/index.ts index 37f0c99..e3e6c60 100644 --- a/tests/vite/server/index.ts +++ b/tests/vite/server/index.ts @@ -55,6 +55,17 @@ async function startServer() { ); }); + app.get("/cors-test", async (req, res) => { + res.header("Access-Control-Allow-Origin", "http://localhost:5555"); + res.header("Access-Control-Allow-Credentials", "true"); + res + .status(200) + .type("text/html") + .send( + "