Skip to content

Commit 8eede3f

Browse files
Improve miniflare test stability (#11642)
* update to latest ava * Display error `cause` in Miniflare tests * hide unwanted logging from Miniflare browser plugin tests * quieten Miniflare secrets-store plugin tests * quieten Miniflare Durable Object plugin tests * quieten other Miniflare tests * Retry removing the Browser Session temp directory * Give the Miniflare browser plugin tests a bit longer to complete These tests will need to download the Chrome browser in the background, which can take quite a few seconds sometimes. * silence Miniflare pipeline plugin tests * Silence Miniflare unsafe plugin tests * don't run miniflare tests files concurrently to flakes In particular the dev-registry tests appear to be particularly sensitive to concurrency issues, likely due to shared ports/sockets. * run the Miniflare dev-registry tests in series (rather than parallel) * fix typo * quieten unwanted logging from Miniflare dev-registry tests * no keep-alive * add changeset * no keep-alive * fix changeset * fix lockfile * fix changeset (again) * mark Miniflare browser tests as flaky
1 parent b0dbf1a commit 8eede3f

File tree

24 files changed

+850
-801
lines changed

24 files changed

+850
-801
lines changed

.changeset/floppy-jobs-shout.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Fix intermittent "Fetch failed" errors in Miniflare tests on Windows
6+
7+
Miniflare tests would occasionally fail with "Fetch failed" errors (particularly on Windows CI runners) due to race conditions between undici's Keep-Alive mechanism and the Miniflare server closing idle connections. Miniflare now configures the Dispatcher to prevent connection reuse and eliminate these race condition errors.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"toucan-js@4.0.0": "patches/toucan-js@4.0.0.patch",
8282
"postal-mime": "patches/postal-mime.patch",
8383
"youch@4.1.0-beta.10": "patches/youch@4.1.0-beta.10.patch",
84-
"@netlify/build-info": "patches/@netlify__build-info.patch"
84+
"@netlify/build-info": "patches/@netlify__build-info.patch",
85+
"concordance@5.0.4": "patches/concordance@5.0.4.patch"
8586
}
8687
}
8788
}

packages/miniflare/ava.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export default {
1212
environmentVariables: {
1313
MINIFLARE_ASSERT_BODIES_CONSUMED: "true",
1414
},
15+
concurrency: 1,
1516
};

packages/miniflare/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
"@types/ws": "^8.5.7",
7979
"@typescript-eslint/eslint-plugin": "catalog:default",
8080
"@typescript-eslint/parser": "catalog:default",
81-
"ava": "^6.0.1",
81+
"ava": "^6.4.1",
8282
"capnp-es": "^0.0.11",
8383
"capnweb": "^0.1.0",
8484
"chokidar": "^4.0.1",

packages/miniflare/src/http/fetch.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ export class DispatchFetchDispatcher extends undici.Dispatcher {
234234

235235
options.headers = headers;
236236

237+
// Sometimes, keep-alive connections can sometimes cause issues with sockets
238+
// disconnecting unexpectedly. To mitigate this, try to avoid keep-alive race
239+
// conditions by telling the runtime to close the connection immediately after
240+
// the request is complete
241+
options.reset = true;
242+
237243
// Dispatch with runtime dispatcher to avoid certificate errors if using
238244
// self-signed certificate
239245
return this.runtimeDispatcher.dispatch(options, handler);

packages/miniflare/src/plugins/browser-rendering/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,11 @@ export async function launchBrowser({
216216
pipe: false,
217217
onExit: async () => {
218218
await fs.promises
219-
.rm(tempUserData, { recursive: true, force: true })
219+
.rm(tempUserData, {
220+
recursive: true,
221+
force: true,
222+
maxRetries: 5, // In case of Windows file locks
223+
})
220224
.catch((e) => {
221225
log.debug(
222226
`Unable to remove Chrome user data directory: ${String(e)}`

packages/miniflare/src/shared/dev-registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ export class DevRegistry {
225225
type: "setup",
226226
runtimeEntryURL,
227227
fallbackServicePorts,
228+
logLevel: this.log.level,
228229
});
229230
}
230231

packages/miniflare/src/shared/dev-registry.worker.ts

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ interface ProxyAddress {
2121
path?: string;
2222
}
2323

24-
const log = new Log();
25-
2624
/**
2725
* A HTTP proxy server for the dev registry.
2826
* This runs in a separate thread to prevent a deadlock when using a node binding
2927
*/
3028
class ProxyServer {
29+
private log = new Log();
3130
private registry: Record<string, WorkerDefinition> = {};
3231
private runtimeEntryURL: string | null = null;
3332
private fallbackServicePorts: Record<string, Record<string, number>> = {};
@@ -46,6 +45,7 @@ class ProxyServer {
4645
case "setup":
4746
this.runtimeEntryURL = message.runtimeEntryURL;
4847
this.fallbackServicePorts = message.fallbackServicePorts;
48+
this.log = new Log(message.logLevel);
4949
break;
5050
default:
5151
// Ignore unknown message types
@@ -54,48 +54,41 @@ class ProxyServer {
5454
});
5555
}
5656

57-
public async start() {
58-
try {
59-
// Listen on a random port
60-
await new Promise<void>((resolve, reject) => {
61-
const server = http.createServer({
62-
// There might be no HOST header when proxying a fetch request made over service binding
63-
// e.g. env.MY_WORKER.fetch("https://example.com")
64-
requireHostHeader: false,
65-
// Disable request and headers timeout for long-lived WebSocket connections
66-
// Node.js's headersTimeout (default: min(60s, requestTimeout)) is checked periodically
67-
// by connectionsCheckingInterval (default: 30s), causing timeouts around 60-90s.
68-
// Setting both to 0 disables timeout enforcement for WebSocket proxying.
69-
requestTimeout: 0,
70-
headersTimeout: 0,
71-
});
72-
73-
server.on("request", this.handleRequest.bind(this));
74-
server.on("connect", this.handleConnect.bind(this));
75-
server.listen(0, "127.0.0.1", () => {
76-
const address = server.address();
57+
public start() {
58+
// Listen on a random port
59+
return new Promise<void>((resolve, reject) => {
60+
const server = http.createServer({
61+
// There might be no HOST header when proxying a fetch request made over service binding
62+
// e.g. env.MY_WORKER.fetch("https://example.com")
63+
requireHostHeader: false,
64+
// Disable request and headers timeout for long-lived WebSocket connections
65+
// Node.js's headersTimeout (default: min(60s, requestTimeout)) is checked periodically
66+
// by connectionsCheckingInterval (default: 30s), causing timeouts around 60-90s.
67+
// Setting both to 0 disables timeout enforcement for WebSocket proxying.
68+
requestTimeout: 0,
69+
headersTimeout: 0,
70+
});
7771

78-
if (!address || typeof address === "string") {
79-
reject(new Error("Failed to get server address"));
80-
return;
81-
}
72+
server.on("request", this.handleRequest.bind(this));
73+
server.on("connect", this.handleConnect.bind(this));
74+
server.listen(0, "127.0.0.1", () => {
75+
const address = server.address();
8276

83-
// Notify parent that server is ready with the port
84-
this.messagePort.postMessage({
85-
type: "ready",
86-
address: `${address.address}:${address.port}`,
87-
});
77+
if (!address || typeof address === "string") {
78+
reject(new Error("Failed to get server address"));
79+
return;
80+
}
8881

89-
resolve();
82+
// Notify parent that server is ready with the port
83+
this.messagePort.postMessage({
84+
type: "ready",
85+
address: `${address.address}:${address.port}`,
9086
});
91-
server.on("error", reject);
92-
});
93-
} catch (error) {
94-
this.messagePort.postMessage({
95-
type: "error",
96-
error: error instanceof Error ? error.message : String(error),
87+
88+
resolve();
9789
});
98-
}
90+
server.on("error", reject);
91+
});
9992
}
10093

10194
private handleRequest(
@@ -162,19 +155,21 @@ class ProxyServer {
162155

163156
// Errors on either side
164157
serverSocket.on("error", (error) => {
165-
log.error(error);
158+
this.log.error(error);
166159
clientSocket.destroy();
167160
});
168161
clientSocket.on("error", () => serverSocket.destroy());
169162
// Close the tunnel if the service is updated
170163
// This makes sure workerd will re-connect to the latest address
171164
this.subscribe(serviceName, () => {
172-
log.debug(`Closing tunnel as service "${serviceName}" was updated`);
165+
this.log.debug(
166+
`Closing tunnel as service "${serviceName}" was updated`
167+
);
173168
serverSocket.end();
174169
clientSocket.end();
175170
});
176171
} catch (e) {
177-
log.error(e instanceof Error ? e : new Error(`${e}`));
172+
this.log.error(e instanceof Error ? e : new Error(`${e}`));
178173
clientSocket.end();
179174
}
180175
}
@@ -308,7 +303,7 @@ class ProxyServer {
308303
});
309304

310305
upstream.on("error", (error) => {
311-
log.error(
306+
this.log.error(
312307
new Error(
313308
`Failed to proxy request to ${target.protocol}://${target.host}:${target.port}${target.path ?? ""}`,
314309
{
@@ -428,15 +423,15 @@ function runProxyServer() {
428423
throw new Error("This script must be run in a worker thread");
429424
}
430425

431-
const server = new ProxyServer(parentPort);
426+
const messagePort = parentPort;
427+
const server = new ProxyServer(messagePort);
432428

433429
// Start the server
434430
server.start().catch((error) => {
435-
log.error(
436-
new Error("ProxyServer: Failed to start proxy server:", {
437-
cause: error,
438-
})
439-
);
431+
messagePort.postMessage({
432+
type: "error",
433+
error: error instanceof Error ? error.message : String(error),
434+
});
440435
});
441436
}
442437

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import { WorkerEntrypoint } from "cloudflare:workers";
22

33
export default class Pipeline extends WorkerEntrypoint {
4-
async send(data: object[]): Promise<void> {
5-
console.log("Request received", data);
6-
}
4+
async send(_data: object[]): Promise<void> {}
75
}

0 commit comments

Comments
 (0)