Skip to content

Commit 09ae474

Browse files
authored
(#59) Additional Diagnostics on Fastify Request
1 parent 3b17f00 commit 09ae474

File tree

3 files changed

+125
-54
lines changed

3 files changed

+125
-54
lines changed

bifrost-fastify/index.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ import proxy, { type FastifyHttpProxyOptions } from "@fastify/http-proxy";
1010
import accepts from "@fastify/accepts";
1111
import forwarded from "@fastify/forwarded";
1212
import type { GetLayout, WrappedServerOnly } from "@alignable/bifrost/config";
13-
import { PassThrough, Writable } from "stream";
1413
import { renderPage } from "vike/server";
1514
import { PageContextServer } from "vike/types";
1615
import { extractDomElements } from "./lib/extractDomElements";
1716
import { Http2ServerRequest } from "http2";
18-
import { IncomingMessage } from "http";
1917
import { text } from "node:stream/consumers";
2018
import { parse as parseContentType } from "fast-content-type-parse";
2119

@@ -32,7 +30,12 @@ type RenderedPageContext = Awaited<
3230

3331
declare module "fastify" {
3432
interface FastifyRequest {
35-
bifrostPageId?: string | null;
33+
/// Actual ProxyMode after processing backend server results, which can tell us to fallback to passthru or redirect
34+
bifrostProxyMode?: Vike.Config["proxyMode"];
35+
/// whether we sent proxy headers to legacy backend
36+
bifrostSentProxyHeaders?: boolean;
37+
bifrostProxyLayout?: Vike.ProxyLayoutInfo | null;
38+
/// Only set when proxy mode is false or wrapped
3639
vikePageContext?: Partial<PageContextServer> | null;
3740
getLayout: GetLayout | null;
3841
customPageContextInit: Partial<Omit<PageContextServer, "headers">> | null;
@@ -96,7 +99,9 @@ export const viteProxyPlugin: FastifyPluginAsync<
9699
);
97100
}
98101
await fastify.register(accepts);
99-
fastify.decorateRequest("bifrostPageId", null);
102+
fastify.decorateRequest("bifrostProxyMode", false);
103+
fastify.decorateRequest("bifrostProxyLayout", null);
104+
fastify.decorateRequest("bifrostSentProxyHeaders", false);
100105
fastify.decorateRequest("vikePageContext", null);
101106
fastify.decorateRequest("getLayout", null);
102107
fastify.decorateRequest("customPageContextInit", null);
@@ -121,16 +126,14 @@ export const viteProxyPlugin: FastifyPluginAsync<
121126

122127
const pageContext = await renderPage(pageContextInit);
123128

124-
// this does not handle getting the original pageId when errors are thrown: https://github.com/vikejs/vike/issues/1112
125-
req.bifrostPageId = pageContext.pageId;
126-
req.vikePageContext = pageContext;
127-
128-
const proxyMode = pageContext.config?.proxyMode;
129+
let proxyMode = pageContext.config?.proxyMode;
129130
if (!proxyMode) {
131+
req.vikePageContext = pageContext;
130132
req.log.info(`bifrost: rendering page ${pageContext.pageId}`);
131133
return replyWithPage(reply, pageContext);
132134
}
133135

136+
req.bifrostProxyMode = "passthru";
134137
switch (proxyMode) {
135138
case "passthru": {
136139
req.log.info(`bifrost: passthru proxy to backend`);
@@ -163,6 +166,7 @@ export const viteProxyPlugin: FastifyPluginAsync<
163166
// setting _bfproxy tells onResponse we're in wrapped mode
164167
(req.raw as RawRequestExtendedWithProxy)._bfproxy = true;
165168
req.getLayout = pageContext.config.getLayout;
169+
req.bifrostSentProxyHeaders = true;
166170
}
167171
} else {
168172
req.log.error(
@@ -216,6 +220,7 @@ export const viteProxyPlugin: FastifyPluginAsync<
216220
}
217221

218222
const proxyLayoutInfo = req.getLayout?.(reply.getHeaders());
223+
req.bifrostProxyLayout = proxyLayoutInfo;
219224
if (!proxyLayoutInfo) {
220225
return reply.send("stream" in res ? res.stream : res);
221226
}
@@ -254,6 +259,8 @@ export const viteProxyPlugin: FastifyPluginAsync<
254259
...req.customPageContextInit,
255260
};
256261
const pageContext = await renderPage(pageContextInit);
262+
req.vikePageContext = pageContext;
263+
req.bifrostProxyMode = "wrapped";
257264
return replyWithPage(reply, pageContext);
258265
},
259266
},

tests/e2e/specs/http.spec.ts

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { test, expect } from "@playwright/test";
1+
import { test, expect, APIResponse } from "@playwright/test";
2+
import { toPath } from "../../fake-backend/page-builder";
23

34
test.describe("requests", () => {
45
test("it proxies non-html requests", async ({ request }) => {
@@ -9,55 +10,111 @@ test.describe("requests", () => {
910
);
1011
});
1112

12-
test.describe("HEAD request", () => {
13-
test("returns headers for vite page", async ({ request }) => {
14-
const req = await request.head("./vite-page");
15-
expect(req.headers()).toMatchObject({
16-
"x-test-pageid": "/pages/vite-page",
17-
});
18-
expect(req.headers()).not.toMatchObject({
19-
"x-test-fake-backend": "1",
20-
});
21-
});
22-
23-
test("returns headers for proxied page", async ({ request }) => {
24-
const req = await request.head("./custom-incorrect");
25-
expect(req.headers()).toMatchObject({
26-
"x-test-pageid": "/pages/proxy/passthru",
27-
// hits old backend
28-
"x-test-fake-backend": "1",
29-
});
30-
});
31-
});
32-
3313
test.describe("onError", () => {
3414
test("returns header that we set in onError", async ({ request }) => {
3515
const req = await request.get("./broken-page");
3616
expect(req.headers()["x-test-onerror"]).toBe("true");
3717
});
3818
});
3919

40-
test.describe("req.bifrostPageId", () => {
41-
test("returns page id", async ({ request }) => {
20+
test.describe("diagnostics attached to req", () => {
21+
function diagnostics(req: APIResponse) {
22+
return {
23+
status: req.status(),
24+
pageId: req.headers()["x-test-pageid"],
25+
layout:
26+
req.headers()["x-test-layout"]?.split(",").filter(Boolean) || [],
27+
proxyMode:
28+
req.headers()["x-test-proxymode"] &&
29+
JSON.parse(req.headers()["x-test-proxymode"]),
30+
sentProxyHeaders: req.headers()["x-test-sent-proxy-headers"] === "1",
31+
};
32+
}
33+
34+
test("vite page sets pageId", async ({ request }) => {
4235
const req = await request.get("./vite-page");
43-
expect(req.headers()["x-test-pageid"]).toBe("/pages/vite-page");
36+
expect(diagnostics(req)).toEqual({
37+
status: 200,
38+
pageId: "/pages/vite-page",
39+
layout: [],
40+
proxyMode: false,
41+
sentProxyHeaders: false,
42+
});
4443
});
4544

46-
test("returns undefined when no route matches at all", async ({
47-
request,
48-
}) => {
45+
test("when no route matches at all", async ({ request }) => {
4946
const req = await request.get("./jsaidofjasidofjasoidf");
50-
expect(req.headers()["x-test-pageid"]).toBe(undefined);
47+
expect(diagnostics(req)).toEqual({
48+
status: 404,
49+
pageId: undefined,
50+
layout: [],
51+
proxyMode: undefined,
52+
sentProxyHeaders: false,
53+
});
5154
});
5255

53-
test("returns wrapped proxy route when hit", async ({ request }) => {
54-
const req = await request.get("./json-route");
55-
expect(req.headers()["x-test-pageid"]).toBe("/pages/proxy/wrapped");
56+
test("on passthru, undefined pageId and layout", async ({ request }) => {
57+
const req = await request.get(
58+
toPath({ endpoint: "custom-incorrect", title: "a" })
59+
);
60+
expect(diagnostics(req)).toEqual({
61+
status: 200,
62+
pageId: undefined,
63+
layout: [],
64+
proxyMode: "passthru",
65+
sentProxyHeaders: false,
66+
});
5667
});
5768

58-
test("returns passthru proxy route when hit", async ({ request }) => {
59-
const req = await request.get("./custom-incorrect");
60-
expect(req.headers()["x-test-pageid"]).toBe("/pages/proxy/passthru");
69+
test("wrapped with layout", async ({ request }) => {
70+
const req = await request.get(toPath({ title: "a" }));
71+
expect(diagnostics(req)).toEqual({
72+
status: 200,
73+
pageId: "/pages/proxy/wrapped",
74+
layout: ["main_nav"],
75+
proxyMode: "wrapped",
76+
sentProxyHeaders: true,
77+
});
78+
});
79+
80+
test("wrapped with no layout", async ({ request }) => {
81+
const req = await request.get(toPath({ title: "a", layout: "" }));
82+
expect(diagnostics(req)).toEqual({
83+
status: 200,
84+
pageId: undefined,
85+
layout: [],
86+
proxyMode: "passthru",
87+
sentProxyHeaders: true,
88+
});
89+
});
90+
91+
test("HEAD request on vite-page", async ({ request }) => {
92+
const req = await request.head("./vite-page");
93+
expect(diagnostics(req)).toEqual({
94+
status: 200,
95+
pageId: "/pages/vite-page",
96+
layout: [],
97+
proxyMode: false,
98+
sentProxyHeaders: false,
99+
});
100+
expect(req.headers()).not.toMatchObject({
101+
"x-test-fake-backend": "1",
102+
});
103+
});
104+
105+
test("HEAD request on proxied page", async ({ request }) => {
106+
const req = await request.head(
107+
toPath({ endpoint: "custom-incorrect", title: "a" })
108+
);
109+
expect(diagnostics(req)).toEqual({
110+
status: 200,
111+
pageId: undefined,
112+
layout: [],
113+
proxyMode: "passthru",
114+
sentProxyHeaders: false,
115+
});
116+
// hits old backend
117+
expect(req.headers()["x-test-fake-backend"]).toBe("1");
61118
});
62119

63120
test.skip("returns original page id on error pages", async ({
@@ -69,9 +126,7 @@ test.describe("requests", () => {
69126
});
70127

71128
test.describe("wrapped xhr", () => {
72-
test("passes through script response", async ({
73-
request,
74-
}) => {
129+
test("passes through script response", async ({ request }) => {
75130
const req = await request.get("/script-wrapped", {
76131
headers: { Accept: "text/html" },
77132
});
@@ -81,15 +136,13 @@ test.describe("requests", () => {
81136
);
82137
});
83138

84-
test("passes through JSON response", async ({
85-
request,
86-
}) => {
139+
test("passes through JSON response", async ({ request }) => {
87140
// important to set accept */* to allow the wrapped proxy
88141
const req = await request.get("/json-wrapped", {
89142
headers: { Accept: "application/json */*" },
90143
});
91144
expect(req.status()).toBe(200);
92145
expect(await req.json()).toEqual({ data: true });
93146
});
94-
})
147+
});
95148
});

tests/vite/server/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,21 @@ async function startServer() {
3838
}
3939

4040
app.addHook("onSend", async (req, reply) => {
41-
if (req.bifrostPageId) {
42-
// set header for testing
43-
reply.header("X-TEST-PAGEID", req.bifrostPageId);
41+
// set header for testing
42+
if (req.vikePageContext?.pageId) {
43+
reply.header("X-TEST-PAGEID", req.vikePageContext.pageId);
4444
}
45+
46+
const layoutInfo = Object.keys(req.bifrostProxyLayout || {}).join(",");
47+
if (layoutInfo) reply.header("X-TEST-LAYOUT", layoutInfo);
48+
49+
if (req.bifrostProxyMode !== undefined)
50+
reply.header("X-TEST-PROXYMODE", JSON.stringify(req.bifrostProxyMode));
51+
52+
reply.header(
53+
"X-TEST-SENT-PROXY-HEADERS",
54+
req.bifrostSentProxyHeaders ? "1" : "0"
55+
);
4556
});
4657

4758
app.register(viteProxyPlugin, {

0 commit comments

Comments
 (0)