1- import child_process , { execSync } from "node:child_process" ;
1+ import { execSync , spawn } from "node:child_process" ;
22import * as nodeNet from "node:net" ;
3- import { promisify } from "node:util" ;
43import dedent from "ts-dedent" ;
54import { afterEach , beforeEach , describe , expect , it } from "vitest" ;
65import { 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