Skip to content

Commit 2e8eb24

Browse files
fix startWorker not respecting auth options for remote bindings (#10122)
1 parent 773cca3 commit 2e8eb24

File tree

12 files changed

+395
-35
lines changed

12 files changed

+395
-35
lines changed

.changeset/sdw-auth.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix `startWorker` not respecting `auth` options for remote bindings
6+
7+
fix `startWorker` currently not taking into account the `auth` field
8+
that can be provided as part of the `dev` options when used in conjunction
9+
with remote bindings
10+
11+
example:
12+
13+
Given the following
14+
15+
```js
16+
import { unstable_startWorker } from "wrangler";
17+
18+
const worker = await unstable_startWorker({
19+
entrypoint: "./worker.js",
20+
bindings: {
21+
AI: {
22+
type: "ai",
23+
experimental_remote: true,
24+
},
25+
},
26+
dev: {
27+
experimentalRemoteBindings: true,
28+
auth: {
29+
accountId: "<ACCOUNT_ID>",
30+
apiToken: {
31+
apiToken: "<API_TOKEN>",
32+
},
33+
},
34+
},
35+
});
36+
37+
await worker.ready;
38+
```
39+
40+
`wrangler` will now use the provided `<ACCOUNT_ID>` and `<API_TOKEN>` to integrate with
41+
the remote AI binding instead of requiring the user to authenticate.

.changeset/whole-masks-sniff.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
fix incorrect TypeScript type for AI binding in the `startWorker` API

packages/wrangler/e2e/dev-env.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("switching runtimes", () => {
5252
remote: firstRemote,
5353
auth: {
5454
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
55-
apiToken: process.env.CLOUDFLARE_API_TOKEN,
55+
apiToken: {
56+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
57+
}
5658
},
5759
},
5860
});
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import assert from "node:assert";
2+
import path from "node:path";
3+
import dedent from "ts-dedent";
4+
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
5+
import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id";
6+
import { WranglerE2ETestHelper } from "./helpers/e2e-wrangler-test";
7+
import type { Worker } from "../src/api/startDevWorker";
8+
import type { MockInstance } from "vitest";
9+
10+
type Wrangler = Awaited<ReturnType<WranglerE2ETestHelper["importWrangler"]>>;
11+
12+
describe("startWorker - auth options", () => {
13+
let consoleErrorMock: MockInstance<typeof console.error>;
14+
15+
beforeAll(() => {
16+
consoleErrorMock = vi.spyOn(console, "error").mockImplementation(() => {});
17+
});
18+
19+
describe.skipIf(!CLOUDFLARE_ACCOUNT_ID)("with remote bindings", () => {
20+
let helper: WranglerE2ETestHelper;
21+
let wrangler: Wrangler;
22+
let startWorker: Wrangler["unstable_startWorker"];
23+
24+
beforeEach(async () => {
25+
helper = new WranglerE2ETestHelper();
26+
const aiWorkerScript = dedent`
27+
export default {
28+
async fetch(_request, env) {
29+
const messages = [
30+
{
31+
role: "user",
32+
content:
33+
"Respond with the exact text 'This is a response from Workers AI.'. Do not include any other text",
34+
},
35+
];
36+
37+
const content = await env.AI.run("@hf/thebloke/zephyr-7b-beta-awq", {
38+
messages,
39+
});
40+
41+
return new Response(content.response);
42+
},
43+
}
44+
`;
45+
await helper.seed({
46+
"src/index.js": aiWorkerScript,
47+
});
48+
wrangler = await helper.importWrangler();
49+
startWorker = wrangler.unstable_startWorker;
50+
});
51+
52+
test("starting a worker with startWorker with the valid auth information and updating it with invalid information", async (t) => {
53+
t.onTestFinished(async () => await worker?.dispose());
54+
55+
const validAuth = vi.fn(() => {
56+
assert(process.env.CLOUDFLARE_API_TOKEN);
57+
58+
return {
59+
accountId: CLOUDFLARE_ACCOUNT_ID,
60+
apiToken: {
61+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
62+
},
63+
};
64+
});
65+
66+
const worker = await startWorker({
67+
entrypoint: path.resolve(helper.tmpPath, "src/index.js"),
68+
bindings: {
69+
AI: {
70+
type: "ai",
71+
experimental_remote: true,
72+
},
73+
},
74+
dev: {
75+
experimentalRemoteBindings: true,
76+
auth: validAuth,
77+
},
78+
});
79+
80+
await assertValidWorkerAiResponse(worker);
81+
82+
expect(validAuth).toHaveBeenCalledOnce();
83+
84+
consoleErrorMock.mockReset();
85+
86+
const incorrectAuth = vi.fn(() => {
87+
return {
88+
accountId: CLOUDFLARE_ACCOUNT_ID,
89+
apiToken: {
90+
apiToken: "This is an incorrect API TOKEN!",
91+
},
92+
};
93+
});
94+
95+
await worker.patchConfig({
96+
dev: {
97+
experimentalRemoteBindings: true,
98+
auth: incorrectAuth,
99+
},
100+
});
101+
102+
await assertInvalidWorkerAiResponse(worker);
103+
104+
expect(incorrectAuth).toHaveBeenCalledOnce();
105+
});
106+
107+
test("starting a worker with startWorker with invalid auth information and updating it with valid auth information", async (t) => {
108+
t.onTestFinished(async () => await worker?.dispose());
109+
110+
const incorrectAuth = vi.fn(() => {
111+
return {
112+
accountId: CLOUDFLARE_ACCOUNT_ID,
113+
apiToken: {
114+
apiToken: "This is an incorrect API TOKEN!",
115+
},
116+
};
117+
});
118+
119+
const worker = await startWorker({
120+
entrypoint: path.resolve(helper.tmpPath, "src/index.js"),
121+
bindings: {
122+
AI: {
123+
type: "ai",
124+
experimental_remote: true,
125+
},
126+
},
127+
dev: {
128+
experimentalRemoteBindings: true,
129+
auth: incorrectAuth,
130+
},
131+
});
132+
133+
await assertInvalidWorkerAiResponse(worker);
134+
135+
expect(incorrectAuth).toHaveBeenCalledOnce();
136+
137+
consoleErrorMock.mockReset();
138+
139+
const validAuth = vi.fn(() => {
140+
assert(process.env.CLOUDFLARE_API_TOKEN);
141+
142+
return {
143+
accountId: CLOUDFLARE_ACCOUNT_ID,
144+
apiToken: {
145+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
146+
},
147+
};
148+
});
149+
150+
await worker.patchConfig({
151+
dev: {
152+
experimentalRemoteBindings: true,
153+
auth: validAuth,
154+
},
155+
});
156+
157+
await assertValidWorkerAiResponse(worker);
158+
159+
expect(validAuth).toHaveBeenCalledOnce();
160+
});
161+
162+
async function assertValidWorkerAiResponse(worker: Worker) {
163+
const responseText = await fetchTimedTextFromWorker(worker);
164+
165+
// We've fixed the auth information so now we can indeed get
166+
// a valid response from the worker
167+
expect(responseText).toBeTruthy();
168+
expect(responseText).toContain("This is a response from Workers AI.");
169+
170+
// And there should be no error regarding the Cloudflare API in the console
171+
expect(consoleErrorMock).not.toHaveBeenCalledWith(
172+
expect.stringMatching(
173+
/A request to the Cloudflare API \([^)]*\) failed\./
174+
)
175+
);
176+
}
177+
178+
async function assertInvalidWorkerAiResponse(worker: Worker) {
179+
const responseText = await fetchTimedTextFromWorker(worker);
180+
181+
// The remote connection is not established so we can't successfully
182+
// get a response from the worker
183+
expect(responseText).toBe(null);
184+
185+
// And in the console an appropriate error was logged
186+
expect(consoleErrorMock).toHaveBeenCalledWith(
187+
expect.stringMatching(
188+
/A request to the Cloudflare API \([^)]*\) failed\./
189+
)
190+
);
191+
}
192+
});
193+
194+
describe("without remote bindings (no auth is needed)", () => {
195+
test("starting a worker via startWorker without any remote bindings (doesn't cause wrangler to try to get the auth information)", async (t) => {
196+
t.onTestFinished(async () => await worker?.dispose());
197+
198+
const helper = new WranglerE2ETestHelper();
199+
const wrangler = await helper.importWrangler();
200+
const startWorker = wrangler.unstable_startWorker;
201+
202+
const simpleWorkerScript = dedent`
203+
export default {
204+
async fetch(_request, env) {
205+
return new Response('hello from a simple (local-only) worker');
206+
},
207+
}
208+
`;
209+
await helper.seed({
210+
"src/index.js": simpleWorkerScript,
211+
});
212+
213+
const someAuth = vi.fn(() => {
214+
return {
215+
accountId: "",
216+
apiToken: {
217+
apiToken: "",
218+
},
219+
};
220+
});
221+
222+
const worker = await startWorker({
223+
entrypoint: path.resolve(helper.tmpPath, "src/index.js"),
224+
dev: {
225+
experimentalRemoteBindings: true,
226+
auth: someAuth,
227+
},
228+
});
229+
230+
const response = await fetchTimedTextFromWorker(worker);
231+
232+
expect(response).toEqual("hello from a simple (local-only) worker");
233+
234+
expect(someAuth).not.toHaveBeenCalled();
235+
});
236+
});
237+
});
238+
239+
/**
240+
* Tries to fetch some text from a target worker, as part of this it also polls from the worker
241+
* trying multiple times to fetch from it.
242+
*
243+
* @param worker The worker in question
244+
* @returns The text from the worker's response, or null if not response could be obtained.
245+
*/
246+
async function fetchTimedTextFromWorker(
247+
worker: Worker
248+
): Promise<string | null> {
249+
let responseText: string | null = null;
250+
251+
try {
252+
await vi.waitFor(
253+
async () => {
254+
responseText = await (
255+
await worker.fetch("http://example.com", {
256+
signal: AbortSignal.timeout(500),
257+
})
258+
).text();
259+
},
260+
{ timeout: 10_000, interval: 700 }
261+
);
262+
} catch {
263+
return null;
264+
}
265+
266+
return responseText;
267+
}

0 commit comments

Comments
 (0)