Skip to content

Commit 529988a

Browse files
author
Adrian Gracia
committed
SQC-707 Fix npx wrangler dev with Hyperdrive binding slow latency on
Windows - Update localhost to 127.0.0.1 address to fix issue with Windows defaulting to ipv6 lookup and timing out - Fix skipped Windows Hyperdrive tests which were failing due to dispose causing connection reset errors. It was causing the child process to crash but this is now fixed with calling node spawn and cleanly handling windows connections errors
1 parent af54c63 commit 529988a

File tree

4 files changed

+125
-82
lines changed

4 files changed

+125
-82
lines changed

‎.changeset/thick-showers-hammer.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"miniflare": minor
3+
---
4+
5+
Improve local Hyperdrive binding latency on Windows.

‎packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class HyperdriveProxyController {
5353
);
5454
});
5555
const port = await new Promise<number>((resolve, reject) => {
56-
server.listen(0, "localhost", () => {
56+
server.listen(0, "127.0.0.1", () => {
5757
const address = server.address() as net.AddressInfo;
5858
if (address && typeof address !== "string") {
5959
resolve(address.port);

‎packages/miniflare/src/plugins/hyperdrive/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const HYPERDRIVE_PLUGIN: Plugin<typeof HyperdriveInputOptionsSchema> = {
130130
services.push({
131131
name: `${HYPERDRIVE_PLUGIN_NAME}:${name}`,
132132
external: {
133-
address: `localhost:${proxyPort}`,
133+
address: `127.0.0.1:${proxyPort}`,
134134
tcp: {},
135135
},
136136
});

‎packages/wrangler/e2e/get-platform-proxy.test.ts‎

Lines changed: 118 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import child_process, { execSync } from "node:child_process";
1+
import { execSync, spawn } from "node:child_process";
22
import * as nodeNet from "node:net";
3-
import { promisify } from "node:util";
43
import dedent from "ts-dedent";
54
import { afterEach, beforeEach, describe, expect, it } from "vitest";
65
import { CLOUDFLARE_ACCOUNT_ID } from "./helpers/account-id";
@@ -369,6 +368,8 @@ describe("getPlatformProxy()", () => {
369368
let server: nodeNet.Server;
370369
let receivedData: string | null = null;
371370
beforeEach(async () => {
371+
// Reset data for each test
372+
receivedData = null;
372373
// Create server with connection handler already attached
373374
// Handle sslrequest packet for postgres tls handshake
374375
server = nodeNet.createServer((socket) => {
@@ -402,16 +403,53 @@ describe("getPlatformProxy()", () => {
402403
});
403404
});
404405

405-
it.skipIf(
406-
// in CI this test fails for windows because of ECONNRESET issues
407-
process.platform === "win32"
408-
)(
409-
"default hyperdrive can connect to a TCP socket via the hyperdrive connect",
410-
async () => {
411-
// set worker per test
412-
root = makeRoot();
413-
await seed(root, {
414-
"wrangler.toml": dedent`
406+
/**
407+
* Run nodejs script as child process with node spawn command.
408+
* Use spawn to avoid blocking the event loop.
409+
* Docs: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
410+
*/
411+
async function runInNodeAsSpawnChildProcess(
412+
scriptPath: string,
413+
cwd: string,
414+
timeoutMs: number = 5000
415+
) {
416+
return new Promise<void>((resolve, reject) => {
417+
const childProcess = spawn("node", [scriptPath], {
418+
cwd,
419+
stdio: "inherit",
420+
});
421+
422+
const timeout = setTimeout(() => {
423+
childProcess.kill();
424+
reject(new Error(`Timeout after ${timeoutMs}ms`));
425+
}, timeoutMs);
426+
427+
childProcess.on("exit", (code) => {
428+
clearTimeout(timeout);
429+
430+
// Windows: ignore libuv assertion failure exit code
431+
const isWindowsCleanupError =
432+
process.platform === "win32" && code === 3221226505;
433+
434+
if (code === 0 || code === null || isWindowsCleanupError) {
435+
resolve();
436+
} else {
437+
reject(code);
438+
}
439+
});
440+
441+
childProcess.on("error", (err) => {
442+
clearTimeout(timeout);
443+
reject(err);
444+
});
445+
});
446+
}
447+
448+
it("default hyperdrive can connect to a TCP socket via the hyperdrive connect", async () => {
449+
// set worker per test
450+
root = makeRoot();
451+
await seed(root, {
452+
"wrangler.toml": dedent`
415453
name = "hyperdrive-app"
416454
compatibility_date = "2025-09-06"
417455
compatibility_flags = ["nodejs_compat"]
@@ -421,7 +459,17 @@ describe("getPlatformProxy()", () => {
421459
id = "hyperdrive_id"
422460
localConnectionString = "postgresql://user:%[email protected]:${port}/some_db"
423461
`,
424-
"index.mjs": dedent/*javascript*/ `
462+
"index.mjs": dedent/*javascript*/ `
463+
// Windows socket cleanup error handler
464+
if (process.platform === 'win32') {
465+
process.on('uncaughtException', (err) => {
466+
if (err.code === 'ECONNRESET' && err.syscall === 'read') {
467+
process.exit(0);
468+
}
469+
throw err;
470+
});
471+
}
472+
425473
import { getPlatformProxy } from "${WRANGLER_IMPORT}";
426474
427475
const { env, dispose } = await getPlatformProxy();
@@ -436,39 +484,28 @@ describe("getPlatformProxy()", () => {
436484
437485
await dispose();
438486
`,
439-
"package.json": dedent`
487+
"package.json": dedent`
440488
{
441489
"name": "hyperdrive-app",
442490
"version": "0.0.0",
443491
"private": true
444492
}
445493
`,
446-
});
494+
});
447495

448-
// use async promise-based version of exec to avoid blocking the event loop.
449-
// docs: https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback
450-
const exec = promisify(child_process.exec);
451-
await exec("node index.mjs", {
452-
cwd: root,
453-
timeout: 5000, // 5 second timeout to prevent infinite hang
454-
});
455-
// Check that we received the expected data
456-
expect(receivedData).toMatchInlineSnapshot(
457-
`"test string sent using getPlatformProxy"`
458-
);
459-
}
460-
);
461-
462-
it.skipIf(
463-
// in CI this test fails for windows because of ECONNRESET issues
464-
process.platform === "win32"
465-
)(
466-
"sslmode - 'prefer' can connect to a TCP socket via the hyperdrive connect method over hyprdrive-proxy",
467-
async () => {
468-
// set worker per test
469-
root = makeRoot();
470-
await seed(root, {
471-
"wrangler.toml": dedent`
496+
await runInNodeAsSpawnChildProcess("index.mjs", root);
497+
498+
// Check that we received the expected data
499+
expect(receivedData).toMatchInlineSnapshot(
500+
`"test string sent using getPlatformProxy"`
501+
);
502+
});
503+
504+
it("sslmode - 'prefer' can connect to a TCP socket via the hyperdrive connect method over hyprdrive-proxy", async () => {
505+
// set worker per test
506+
root = makeRoot();
507+
await seed(root, {
508+
"wrangler.toml": dedent`
472509
name = "hyperdrive-app"
473510
compatibility_date = "2025-09-06"
474511
compatibility_flags = ["nodejs_compat"]
@@ -478,7 +515,16 @@ describe("getPlatformProxy()", () => {
478515
id = "hyperdrive_id"
479516
localConnectionString = "postgresql://user:%[email protected]:${port}/some_db?sslmode=prefer"
480517
`,
481-
"index.mjs": dedent/*javascript*/ `
518+
"index.mjs": dedent/*javascript*/ `
519+
// Windows socket cleanup error handler
520+
if (process.platform === 'win32') {
521+
process.on('uncaughtException', (err) => {
522+
if (err.code === 'ECONNRESET' && err.syscall === 'read') {
523+
process.exit(0);
524+
}
525+
throw err;
526+
});
527+
}
482528
import { getPlatformProxy } from "${WRANGLER_IMPORT}";
483529
484530
const { env, dispose } = await getPlatformProxy();
@@ -493,39 +539,28 @@ describe("getPlatformProxy()", () => {
493539
494540
await dispose();
495541
`,
496-
"package.json": dedent`
542+
"package.json": dedent`
497543
{
498544
"name": "hyperdrive-app",
499545
"version": "0.0.0",
500546
"private": true
501547
}
502548
`,
503-
});
549+
});
504550

505-
// use async promise-based version of exec to avoid blocking the event loop.
506-
// docs: https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback
507-
const exec = promisify(child_process.exec);
508-
await exec("node index.mjs", {
509-
cwd: root,
510-
timeout: 5000, // 5 second timeout to prevent infinite hang
511-
});
512-
// Check that we received the expected data
513-
expect(receivedData).toMatchInlineSnapshot(
514-
`"test string sent using getPlatformProxy"`
515-
);
516-
}
517-
);
518-
519-
it.skipIf(
520-
// in CI this test fails for windows because of ECONNRESET issues
521-
process.platform === "win32"
522-
)(
523-
"sslmode - 'require' fails hyperdrive connection method over hyperdrive-proxy",
524-
async () => {
525-
// set worker per test
526-
root = makeRoot();
527-
await seed(root, {
528-
"wrangler.toml": dedent`
551+
await runInNodeAsSpawnChildProcess("index.mjs", root);
552+
553+
// Check that we received the expected data
554+
expect(receivedData).toMatchInlineSnapshot(
555+
`"test string sent using getPlatformProxy"`
556+
);
557+
});
558+
559+
it("sslmode - 'require' fails hyperdrive connection method over hyperdrive-proxy", async () => {
560+
// set worker per test
561+
root = makeRoot();
562+
await seed(root, {
563+
"wrangler.toml": dedent`
529564
name = "hyperdrive-app"
530565
compatibility_date = "2025-09-06"
531566
compatibility_flags = ["nodejs_compat"]
@@ -535,7 +570,16 @@ describe("getPlatformProxy()", () => {
535570
id = "hyperdrive_id"
536571
localConnectionString = "postgresql://user:%[email protected]:${port}/some_db?sslmode=require"
537572
`,
538-
"index.mjs": dedent/*javascript*/ `
573+
"index.mjs": dedent/*javascript*/ `
574+
// Windows socket cleanup error handler
575+
if (process.platform === 'win32') {
576+
process.on('uncaughtException', (err) => {
577+
if (err.code === 'ECONNRESET' && err.syscall === 'read') {
578+
process.exit(0);
579+
}
580+
throw err;
581+
});
582+
}
539583
import { getPlatformProxy } from "${WRANGLER_IMPORT}";
540584
541585
const { env, dispose } = await getPlatformProxy();
@@ -550,24 +594,18 @@ describe("getPlatformProxy()", () => {
550594
551595
await dispose();
552596
`,
553-
"package.json": dedent`
597+
"package.json": dedent`
554598
{
555599
"name": "hyperdrive-app",
556600
"version": "0.0.0",
557601
"private": true
558602
}`,
559-
});
603+
});
560604

561-
// use async promise-based version of exec to avoid blocking the event loop.
562-
// docs: https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback
563-
const exec = promisify(child_process.exec);
564-
expect(
565-
await exec("node index.mjs", {
566-
cwd: root,
567-
timeout: 5000, // 5 second timeout to prevent infinite hang
568-
})
569-
).toThrowError(Error);
570-
}
571-
);
605+
await runInNodeAsSpawnChildProcess("index.mjs", root);
606+
607+
// Check that we did not receive data since sslmode=require should fail request
608+
expect(receivedData).toBeNull();
609+
});
572610
});
573611
});

0 commit comments

Comments
 (0)