Skip to content
Merged
Show file tree
Hide file tree
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
22 changes: 12 additions & 10 deletions bifrost-fastify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,28 @@ export const viteProxyPlugin: FastifyPluginAsync<
async function replyWithPage(
reply: FastifyReply<RouteGenericInterface, RawServerBase>,
pageContext: RenderedPageContext,
statusCodeOverride?: number
statusCodeFromProxy?: number
): Promise<FastifyReply> {
const { httpResponse } = pageContext;

if (
onError &&
httpResponse?.statusCode === 500 &&
pageContext.errorWhileRendering
) {
onError(pageContext.errorWhileRendering, pageContext);
}

if (!httpResponse) {
return reply.code(404).type("text/html").send("Not Found");
}

const { statusCode, headers, getBody } = httpResponse;

if (onError && statusCode === 500 && pageContext.errorWhileRendering) {
onError(pageContext.errorWhileRendering, pageContext);
}

return (
reply
.status(statusCodeOverride ?? statusCode)
// If there is an error on the Bifrost side, we use the bifrost statusCode, otherwise prefer the proxy's code
.status(
pageContext.errorWhileRendering
? statusCode
: (statusCodeFromProxy ?? statusCode)
)
.headers(Object.fromEntries(headers))
// This disables any possibility of real streaming. To re-enable streaming we should adopt vike-photon and rewrite wrapped proxy as a Vike middleware.
// Why not pipe? Because Vike gives us `pipe` which sends data into a Writable, but Fastify's reply.send only accepts a ReadableStream. Passthrough can convert but causes race conditions
Expand Down
2 changes: 2 additions & 0 deletions bifrost/lib/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface WrappedServerOnly {
// https://vike.dev/pageContext.json#avoid-pagecontext-json-requests
// Instead, we nest them inside wrappedServerOnly and move them to top-level pageContext in onBeforeRenderHtml
proxyLayoutInfo: Vike.ProxyLayoutInfo;
// Marker to verify that render succeeded
renderedBody?: boolean;
}

declare global {
Expand Down
8 changes: 4 additions & 4 deletions bifrost/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@
"peerDependencies": {
"react": ">=18",
"typescript": ">=4.7",
"vike": ">=0.4.253",
"vike-react": ">=0.6.18"
"vike": ">=0.4.255",
"vike-react": ">=0.6.21"
},
"devDependencies": {
"@types/jsdom": "^21.1.2",
Expand All @@ -58,9 +58,9 @@
"tsup": "^7.1.0",
"turbolinks": "5.3.0-beta.1",
"typescript": "^5.0.4",
"vike-react": "0.6.18",
"vike-react": "0.6.21",
"vite": "^6.3.5",
"vike": "0.4.253",
"vike": "0.4.255",
"cross-env": "^7.0.3"
}
}
4 changes: 3 additions & 1 deletion bifrost/renderer/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default {
getLayout: { env: { server: true, client: true } },
layoutHeaders: { env: { server: true, client: false } },
proxyHeaders: { env: { server: true, client: true } },
onWrappedReactRenderTimeout: { env: { server: false, client: true} },
onWrappedReactRenderTimeout: { env: { server: false, client: true } },
proxyMode: {
env: { server: true, client: true, config: true },
effect({ configDefinedAt, configValue }) {
Expand All @@ -46,6 +46,8 @@ export default {
"import:@alignable/bifrost/__internal/renderer/wrapped/onRenderHtml:default",
onBeforeRenderHtml:
"import:@alignable/bifrost/__internal/renderer/wrapped/onBeforeRenderHtml:default",
onAfterRenderHtml:
"import:@alignable/bifrost/__internal/renderer/wrapped/onAfterRenderHtml:default",
onBeforeRender:
"import:@alignable/bifrost/__internal/renderer/wrapped/onBeforeRender.client:default",
onBeforeRenderClient:
Expand Down
5 changes: 5 additions & 0 deletions bifrost/renderer/wrapped/Page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export default function Page() {
? pageContext._turbolinksProxy?.body?.innerHTML
: pageContext._wrappedServerOnly?.bodyInnerHtml;

// Set marker so we can render error if failed to render body due to error in surrounding Layout
if (bodyHtml && !pageContext.isClientSide && pageContext._wrappedServerOnly) {
pageContext._wrappedServerOnly.renderedBody = true;
}

if (bodyHtml) {
return (
<div
Expand Down
17 changes: 17 additions & 0 deletions bifrost/renderer/wrapped/onAfterRenderHtml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PageContextServer } from "vike/types";
import { render } from "vike/abort";

export default function wrappedOnAfterRenderHtml(
pageContext: PageContextServer
) {
if (
pageContext._wrappedServerOnly &&
!pageContext._wrappedServerOnly.renderedBody
) {
// We set wrapped serverOnly but we never rendered the body
throw render(
500,
"proxied-body not found in DOM after SSR. This likely means the Layout threw during SSR (e.g. accessing `window` or `document`). Fix the SSR error in your Layout component."
);
}
}
2 changes: 1 addition & 1 deletion bifrost/renderer/wrapped/onBeforeRender.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default async function wrappedOnBeforeRender(
throw redirect(resp.url);
}
}
if (!resp.ok) {
if (!resp.ok || !resp.headers.get("content-type")?.includes("text/html")) {
await hardNavigate(resp.url);
}
}
Expand Down
1 change: 1 addition & 0 deletions bifrost/renderer/wrapped/onBeforeRenderClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default instrument("wrappedOnBeforeRenderClient", async function wrappedO
))();
}

// This should be caught by onAfterRenderHtml, but just in case, check again on client side
Comment thread
reinabo marked this conversation as resolved.
const proxiedBody = document.getElementById("proxied-body");
if (!proxiedBody) {
throw new Error(
Expand Down
47 changes: 21 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 43 additions & 9 deletions tests/e2e/specs/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,15 +278,18 @@ test("wrapped proxy shows error page when layout has SSR error", async ({
page,
}) => {
await page.goto(
toPath({ title: "SSR Error", layout: "ssr_error", content: "proxied content here" }),
toPath({
title: "SSR Error",
layout: "ssr_error",
content: "proxied content here",
}),
{ waitUntil: "networkidle" }
);

await expect(page).toHaveTitle("Error");
await expect(page.getByText("500 Internal Server Error")).toHaveCount(1);
});


// If passToClient is misconfigured we will end up sending proxy content in HTML and the JSON hydration blob, doubling page size.
// Being able to configure this is why we chose VPS over next.js or remix which always serialize all props
test("on SSR of proxied page, proxy content is only sent once", async ({
Expand Down Expand Up @@ -431,6 +434,19 @@ test.describe("client navigation", () => {
expect(page.url().endsWith("vite-page")).toBeTruthy();
});
});

test("to route with header but no body", async ({ page, baseURL }) => {
const customProxy = new CustomProxyPage(page, {
title: "a",
content: "<a href='/json-wrapped'>json wrapped</a>",
});
await customProxy.goto();

await ensureBrowserNavigation(page, async () => {
await page.getByText("json wrapped").click();
await expect(page.locator("body")).toContainText(`{"data":true}`);
});
});
});

test.describe("redirects", () => {
Expand Down Expand Up @@ -1788,11 +1804,23 @@ test.describe("diagnostic events", () => {
await customProxy.clickLink("second page");

const events = await getDiagnosticEvents(page);
expect(events).toContainEqual({ type: "start", fnName: "_vikeBeforeRender" });
expect(events).toContainEqual({
type: "start",
fnName: "_vikeBeforeRender",
});
expect(events).toContainEqual({ type: "end", fnName: "_vikeBeforeRender" });
expect(events).toContainEqual({ type: "start", fnName: "_waitForHeadScripts" });
expect(events).toContainEqual({ type: "end", fnName: "_waitForHeadScripts" });
expect(events).toContainEqual({ type: "start", fnName: "_vikeAfterRender" });
expect(events).toContainEqual({
type: "start",
fnName: "_waitForHeadScripts",
});
expect(events).toContainEqual({
type: "end",
fnName: "_waitForHeadScripts",
});
expect(events).toContainEqual({
type: "start",
fnName: "_vikeAfterRender",
});
expect(events).toContainEqual({ type: "end", fnName: "_vikeAfterRender" });
});

Expand Down Expand Up @@ -1838,8 +1866,14 @@ test.describe("diagnostic events", () => {
events.findIndex((e) => e.type === type && e.fnName === fnName);

// beforeRender starts and ends before afterRender
expect(idx("start", "_vikeBeforeRender")).toBeLessThan(idx("end", "_vikeBeforeRender"));
expect(idx("end", "_vikeBeforeRender")).toBeLessThan(idx("start", "_vikeAfterRender"));
expect(idx("start", "_vikeAfterRender")).toBeLessThan(idx("end", "_vikeAfterRender"));
expect(idx("start", "_vikeBeforeRender")).toBeLessThan(
idx("end", "_vikeBeforeRender")
);
expect(idx("end", "_vikeBeforeRender")).toBeLessThan(
idx("start", "_vikeAfterRender")
);
expect(idx("start", "_vikeAfterRender")).toBeLessThan(
idx("end", "_vikeAfterRender")
);
});
});
17 changes: 17 additions & 0 deletions tests/e2e/specs/http.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,23 @@ test.describe("requests", () => {
});
});

test("wrapped with error in layout", async ({ request }) => {
const req = await request.get(toPath(
{
title: "SSR Error",
layout: "ssr_error",
content: "proxied content here",
}
));
expect(diagnostics(req)).toEqual({
status: 500,
pageId: "/pages/_error",
layout: ["ssr_error"],
proxyMode: "wrapped",
sentProxyHeaders: true,
});
});

test("HEAD request on vite-page", async ({ request }) => {
const req = await request.head("./vite-page");
expect(diagnostics(req)).toEqual({
Expand Down
4 changes: 2 additions & 2 deletions tests/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"@vitejs/plugin-react": "^4.7.0",
"fastify": "^5.6.2",
"uuid": "^9.0.0",
"vike": "0.4.253",
"vike-react": "^0.6.18",
"vike": "0.4.255",
"vike-react": "^0.6.21",
"vite": "^6.3.5"
},
"devDependencies": {
Expand Down
Loading