Skip to content

Commit 349cffc

Browse files
authored
Hooke up Mixed Mode client to browser rendering (#9181)
* Add Mixed Mode browser rendering plugin * run tests * Create happy-avocados-peel.md
1 parent 415520e commit 349cffc

File tree

6 files changed

+138
-2
lines changed

6 files changed

+138
-2
lines changed

.changeset/happy-avocados-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Add a mixed-mode-only browser rendering plugin

fixtures/mixed-mode-node-test/tests/startMixedModeSession.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,40 @@ describe("startMixedModeSession", () => {
7474
await mixedModeSession.ready;
7575
await mixedModeSession.dispose();
7676
});
77+
78+
test("Browser mixed mode binding", async () => {
79+
const mixedModeSession = await experimental_startMixedModeSession({
80+
BROWSER: {
81+
type: "browser",
82+
},
83+
});
84+
85+
const mf = new Miniflare({
86+
compatibilityDate: "2025-01-01",
87+
compatibilityFlags: ["nodejs_compat"],
88+
modules: true,
89+
script: /* javascript */ `
90+
export default {
91+
async fetch(request, env) {
92+
// Simulate acquiring a session
93+
const content = await env.BROWSER.fetch("http://fake.host/v1/acquire");
94+
return Response.json(await content.json());
95+
}
96+
}
97+
`,
98+
browserRendering: {
99+
binding: "BROWSER",
100+
mixedModeConnectionString: mixedModeSession.mixedModeConnectionString,
101+
},
102+
});
103+
104+
assert.match(
105+
await (await mf.dispatchFetch("http://example.com")).text(),
106+
/sessionId/
107+
);
108+
await mf.dispose();
109+
110+
await mixedModeSession.ready;
111+
await mixedModeSession.dispose();
112+
});
77113
});
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import assert from "node:assert";
2+
import SCRIPT_MIXED_MODE_CLIENT from "worker:shared/mixed-mode-client";
3+
import { z } from "zod";
4+
import { MixedModeConnectionString, Plugin, ProxyNodeBinding } from "../shared";
5+
6+
const BrowserRenderingSchema = z.object({
7+
binding: z.string(),
8+
mixedModeConnectionString: z.custom<MixedModeConnectionString>(),
9+
});
10+
11+
export const BrowserRenderingOptionsSchema = z.object({
12+
browserRendering: BrowserRenderingSchema.optional(),
13+
});
14+
15+
export const BROWSER_RENDERING_PLUGIN_NAME = "browser-rendering";
16+
17+
export const BROWSER_RENDERING_PLUGIN: Plugin<
18+
typeof BrowserRenderingOptionsSchema
19+
> = {
20+
options: BrowserRenderingOptionsSchema,
21+
async getBindings(options) {
22+
if (!options.browserRendering) {
23+
return [];
24+
}
25+
26+
assert(
27+
options.browserRendering.mixedModeConnectionString,
28+
"Workers Browser Rendering only supports Mixed Mode"
29+
);
30+
31+
return [
32+
{
33+
name: options.browserRendering.binding,
34+
service: {
35+
name: `${BROWSER_RENDERING_PLUGIN_NAME}:${options.browserRendering.binding}`,
36+
},
37+
},
38+
];
39+
},
40+
getNodeBindings(options: z.infer<typeof BrowserRenderingOptionsSchema>) {
41+
if (!options.browserRendering) {
42+
return {};
43+
}
44+
return {
45+
[options.browserRendering.binding]: new ProxyNodeBinding(),
46+
};
47+
},
48+
async getServices({ options }) {
49+
if (!options.browserRendering) {
50+
return [];
51+
}
52+
53+
return [
54+
{
55+
name: `${BROWSER_RENDERING_PLUGIN_NAME}:${options.browserRendering.binding}`,
56+
worker: {
57+
compatibilityDate: "2025-01-01",
58+
modules: [
59+
{
60+
name: "index.worker.js",
61+
esModule: SCRIPT_MIXED_MODE_CLIENT(),
62+
},
63+
],
64+
bindings: [
65+
{
66+
name: "mixedModeConnectionString",
67+
text: options.browserRendering.mixedModeConnectionString.href,
68+
},
69+
{
70+
name: "binding",
71+
text: options.browserRendering.binding,
72+
},
73+
],
74+
},
75+
},
76+
];
77+
},
78+
};

packages/miniflare/src/plugins/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
} from "./analytics-engine";
88
import { ASSETS_PLUGIN } from "./assets";
99
import { ASSETS_PLUGIN_NAME } from "./assets/constants";
10+
import {
11+
BROWSER_RENDERING_PLUGIN,
12+
BROWSER_RENDERING_PLUGIN_NAME,
13+
} from "./browser-rendering";
1014
import { CACHE_PLUGIN, CACHE_PLUGIN_NAME } from "./cache";
1115
import { CORE_PLUGIN, CORE_PLUGIN_NAME } from "./core";
1216
import { D1_PLUGIN, D1_PLUGIN_NAME } from "./d1";
@@ -38,6 +42,7 @@ export const PLUGINS = {
3842
[EMAIL_PLUGIN_NAME]: EMAIL_PLUGIN,
3943
[ANALYTICS_ENGINE_PLUGIN_NAME]: ANALYTICS_ENGINE_PLUGIN,
4044
[AI_PLUGIN_NAME]: AI_PLUGIN,
45+
[BROWSER_RENDERING_PLUGIN_NAME]: BROWSER_RENDERING_PLUGIN,
4146
};
4247
export type Plugins = typeof PLUGINS;
4348

@@ -91,7 +96,8 @@ export type WorkerOptions = z.input<typeof CORE_PLUGIN.options> &
9196
z.input<typeof PIPELINE_PLUGIN.options> &
9297
z.input<typeof SECRET_STORE_PLUGIN.options> &
9398
z.input<typeof ANALYTICS_ENGINE_PLUGIN.options> &
94-
z.input<typeof AI_PLUGIN.options>;
99+
z.input<typeof AI_PLUGIN.options> &
100+
z.input<typeof BROWSER_RENDERING_PLUGIN.options>;
95101

96102
export type SharedOptions = z.input<typeof CORE_PLUGIN.sharedOptions> &
97103
z.input<typeof CACHE_PLUGIN.sharedOptions> &
@@ -155,3 +161,4 @@ export * from "./secret-store";
155161
export * from "./email";
156162
export * from "./analytics-engine";
157163
export * from "./ai";
164+
export * from "./browser-rendering";

packages/miniflare/src/workers/shared/mixed-mode-client.worker.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ export default {
22
async fetch(request, env) {
33
const proxiedHeaders = new Headers();
44
for (const [name, value] of request.headers) {
5-
proxiedHeaders.set(`MF-Header-${name}`, value);
5+
// The `Upgrade` header needs to be special-cased to prevent:
6+
// TypeError: Worker tried to return a WebSocket in a response to a request which did not contain the header "Upgrade: websocket"
7+
if (name === "upgrade") {
8+
proxiedHeaders.set(name, value);
9+
} else {
10+
proxiedHeaders.set(`MF-Header-${name}`, value);
11+
}
612
}
713
proxiedHeaders.set("MF-URL", request.url);
814
proxiedHeaders.set("MF-Binding", env.binding);

packages/wrangler/templates/mixedMode/proxyServerWorker/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ export default {
77
for (const [name, value] of request.headers) {
88
if (name.startsWith("mf-header-")) {
99
originalHeaders.set(name.slice("mf-header-".length), value);
10+
} else if (name === "upgrade") {
11+
// The `Upgrade` header needs to be special-cased to prevent:
12+
// TypeError: Worker tried to return a WebSocket in a response to a request which did not contain the header "Upgrade: websocket"
13+
originalHeaders.set(name, value);
1014
}
1115
}
1216
return env[targetBinding].fetch(

0 commit comments

Comments
 (0)