From 215a5a78cdbcd0768ac8143676a41766416fb79c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:46:04 +0000 Subject: [PATCH 1/7] Initial plan From 35b14fedc48bf6ab2cef4f671f9979054b6a8935 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:10:19 +0000 Subject: [PATCH 2/7] Convert test server to HTTPS with self-signed certificate - Updated tcp-over-fetch-websocket.spec.ts to use HTTPS server instead of HTTP - Created generate-certificate utility using selfsigned library - Added duplex: 'half' option to cloneRequest for Node.js compatibility - Set NODE_TLS_REJECT_UNAUTHORIZED=0 for test environment - Still debugging POST request timeouts Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com> --- .../php-wasm/web-service-worker/src/utils.ts | 4 ++ .../src/lib/tcp-over-fetch-websocket.spec.ts | 40 +++++++++++++--- .../lib/test-utils/generate-certificate.ts | 47 +++++++++++++++++++ 3 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 packages/php-wasm/web/src/lib/test-utils/generate-certificate.ts diff --git a/packages/php-wasm/web-service-worker/src/utils.ts b/packages/php-wasm/web-service-worker/src/utils.ts index 73e4dc35fa..6c82cf0114 100644 --- a/packages/php-wasm/web-service-worker/src/utils.ts +++ b/packages/php-wasm/web-service-worker/src/utils.ts @@ -182,6 +182,10 @@ export async function cloneRequest( cache: request.cache, redirect: request.redirect, integrity: request.integrity, + // In Node.js, duplex: 'half' is required when + // the body is provided for non-GET/HEAD requests. + // @ts-expect-error - duplex is not in the TypeScript types + ...(body && { duplex: 'half' }), ...overrides, }); } diff --git a/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts b/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts index 6b43c1b02c..5b0c296b73 100644 --- a/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts +++ b/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts @@ -3,9 +3,13 @@ import { RawBytesFetch, } from './tcp-over-fetch-websocket'; import express from 'express'; -import type http from 'http'; +import https from 'https'; import type { AddressInfo } from 'net'; import zlib from 'zlib'; +import { + generateCertificate, + cleanupCertificate, +} from './test-utils/generate-certificate'; const pygmalion = `PREFACE TO PYGMALION. @@ -58,16 +62,19 @@ least an ill-natured man: very much the opposite, I should say; but he would not suffer fools gladly.`; describe('TCPOverFetchWebsocket', () => { - let server: http.Server; + let server: https.Server; let host: string; let port: number; + let originalRejectUnauthorized: string | undefined; beforeAll(async () => { + // Allow self-signed certificates for testing + originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED; + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + const app = express(); - server = app.listen(0); - const address = server.address() as AddressInfo; - host = `127.0.0.1`; - port = address.port; + + // Set up all routes BEFORE creating the server app.get('/simple', (req, res) => { res.send('Hello, World!'); }); @@ -138,10 +145,31 @@ describe('TCPOverFetchWebsocket', () => { app.get('/error', (req, res) => { res.status(500).send('Internal Server Error'); }); + + // Now create and start the HTTPS server + const { cert, key } = generateCertificate(); + server = https.createServer({ cert, key }, app); + + // Wait for server to start listening + await new Promise((resolve) => { + server.listen(0, () => resolve()); + }); + + const address = server.address() as AddressInfo; + host = `127.0.0.1`; + port = address.port; }); afterAll(() => { server.close(); + cleanupCertificate(); + // Restore original NODE_TLS_REJECT_UNAUTHORIZED value + if (originalRejectUnauthorized !== undefined) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = + originalRejectUnauthorized; + } else { + delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + } }); it('should handle a simple HTTP request', async () => { diff --git a/packages/php-wasm/web/src/lib/test-utils/generate-certificate.ts b/packages/php-wasm/web/src/lib/test-utils/generate-certificate.ts new file mode 100644 index 0000000000..e776acb28d --- /dev/null +++ b/packages/php-wasm/web/src/lib/test-utils/generate-certificate.ts @@ -0,0 +1,47 @@ +import selfsigned from 'selfsigned'; + +let cachedCertificate: { cert: string; key: string; certPath: string } | null = + null; + +export function generateCertificate(): { + cert: string; + key: string; + certPath: string; +} { + // Return cached certificate if already generated + if (cachedCertificate) { + return cachedCertificate; + } + + const attrs = [{ name: 'commonName', value: 'localhost' }]; + const options = { + days: 365, + keySize: 2048, + algorithm: 'sha256', + extensions: [ + { + name: 'subjectAltName', + altNames: [ + { type: 2, value: 'localhost' }, // DNS name + { type: 7, ip: '127.0.0.1' }, // IP address + ], + }, + ], + }; + const pems = selfsigned.generate(attrs, options); + + // Cache the certificate for reuse + // Note: We don't create a certificate file because NODE_EXTRA_CA_CERTS + // can't be set dynamically. Instead, we rely on NODE_TLS_REJECT_UNAUTHORIZED=0 + cachedCertificate = { + cert: pems.cert, + key: pems.private, + certPath: '', // Not used, but kept for interface compatibility + }; + + return cachedCertificate; +} + +export function cleanupCertificate(): void { + cachedCertificate = null; +} From be6f8f89756da0cd77b7d1ca3dcf14eb32cd0199 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:17:01 +0000 Subject: [PATCH 3/7] Fix request body stream handling in cloneRequest - Modified cloneRequest to reuse body stream directly when not consumed - This fixes the hang that occurred when converting HTTP to HTTPS requests - Added proper error handling to bufferResponse in tests - All tcp-over-fetch-websocket tests now pass Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com> --- .../php-wasm/web-service-worker/src/utils.ts | 17 ++++++--- .../src/lib/tcp-over-fetch-websocket.spec.ts | 35 +++++++++++++------ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/php-wasm/web-service-worker/src/utils.ts b/packages/php-wasm/web-service-worker/src/utils.ts index 6c82cf0114..168138d634 100644 --- a/packages/php-wasm/web-service-worker/src/utils.ts +++ b/packages/php-wasm/web-service-worker/src/utils.ts @@ -166,10 +166,19 @@ export async function cloneRequest( request: Request, overrides: Record ): Promise { - const body = - ['GET', 'HEAD'].includes(request.method) || 'body' in overrides - ? undefined - : await request.blob(); + let body: Blob | ReadableStream | undefined; + + if (['GET', 'HEAD'].includes(request.method) || 'body' in overrides) { + body = undefined; + } else if (!request.bodyUsed && request.body) { + // If the body hasn't been consumed yet, we can reuse the stream directly + // This avoids the hang that occurs when trying to read from a stream + // that's still waiting for more data + body = request.body; + } else { + // Otherwise, we need to read the body as a blob + body = await request.blob(); + } return new Request(overrides['url'] || request.url, { body, diff --git a/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts b/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts index 5b0c296b73..491a69027d 100644 --- a/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts +++ b/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts @@ -573,17 +573,30 @@ async function makeRequest({ } async function bufferResponse(socket: TCPOverFetchWebsocket): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { let response = ''; - socket.clientDownstream.readable.pipeTo( - new WritableStream({ - write(chunk) { - response += new TextDecoder().decode(chunk); - }, - close() { - resolve(response); - }, - }) - ); + + // Add error listener + socket.on('error', (error) => { + reject(error); + }); + + socket.clientDownstream.readable + .pipeTo( + new WritableStream({ + write(chunk) { + response += new TextDecoder().decode(chunk); + }, + close() { + resolve(response); + }, + abort(error) { + reject(error); + }, + }) + ) + .catch((error) => { + reject(error); + }); }); } From 94747c8b56db70190f9845ecbab21da0e7c2a780 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 15 Nov 2025 00:35:22 +0000 Subject: [PATCH 4/7] Remove unused @ts-expect-error directive for duplex property TypeScript now recognizes the duplex property in Request options, so the error suppression is no longer needed. Co-authored-by: adamziel <205419+adamziel@users.noreply.github.com> --- packages/php-wasm/web-service-worker/src/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/php-wasm/web-service-worker/src/utils.ts b/packages/php-wasm/web-service-worker/src/utils.ts index 168138d634..d487d9a837 100644 --- a/packages/php-wasm/web-service-worker/src/utils.ts +++ b/packages/php-wasm/web-service-worker/src/utils.ts @@ -193,7 +193,6 @@ export async function cloneRequest( integrity: request.integrity, // In Node.js, duplex: 'half' is required when // the body is provided for non-GET/HEAD requests. - // @ts-expect-error - duplex is not in the TypeScript types ...(body && { duplex: 'half' }), ...overrides, }); From 2cb205374282e117f0d8bb909fd9eab06c06c5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 02:22:20 +0100 Subject: [PATCH 5/7] typecheck --- .../web/src/lib/tcp-over-fetch-websocket.spec.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts b/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts index 491a69027d..283a17585f 100644 --- a/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts +++ b/packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts @@ -69,8 +69,9 @@ describe('TCPOverFetchWebsocket', () => { beforeAll(async () => { // Allow self-signed certificates for testing - originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED; - process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; + originalRejectUnauthorized = + process.env['NODE_TLS_REJECT_UNAUTHORIZED']; + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; const app = express(); @@ -165,10 +166,10 @@ describe('TCPOverFetchWebsocket', () => { cleanupCertificate(); // Restore original NODE_TLS_REJECT_UNAUTHORIZED value if (originalRejectUnauthorized !== undefined) { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = originalRejectUnauthorized; } else { - delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; + delete process.env['NODE_TLS_REJECT_UNAUTHORIZED']; } }); From 25f1de745702bfc598d680faf00145fe53e5efe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 02:28:57 +0100 Subject: [PATCH 6/7] Adjust CLI tests output check --- packages/playground/cli/tests/run-cli.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/cli/tests/run-cli.spec.ts b/packages/playground/cli/tests/run-cli.spec.ts index 66982ddb0d..7742673ad0 100644 --- a/packages/playground/cli/tests/run-cli.spec.ts +++ b/packages/playground/cli/tests/run-cli.spec.ts @@ -556,7 +556,7 @@ describe.each(blueprintVersions)( 'Starting a PHP server...', 'Starting up workers', expect.stringMatching( - /^Resolved WordPress release URL: https:\/\/downloads\.w\.org\/release\/wordpress-\d+\.\d+\.\d+\.zip$/ + /^Resolved WordPress release URL: https:\/\/downloads\.w(ordpress)?\.org\/release\/wordpress-\d+\.\d+\.(?:RC|beta)]\d+\.zip$/ ), 'Fetching SQLite integration plugin...', 'Booting WordPress...', From 682202279e3fb0fc9d9c28179d8e3547c1177aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 18 Nov 2025 11:37:42 +0100 Subject: [PATCH 7/7] Adjust unit test expression --- packages/playground/cli/tests/run-cli.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/playground/cli/tests/run-cli.spec.ts b/packages/playground/cli/tests/run-cli.spec.ts index 7742673ad0..924540c003 100644 --- a/packages/playground/cli/tests/run-cli.spec.ts +++ b/packages/playground/cli/tests/run-cli.spec.ts @@ -556,7 +556,7 @@ describe.each(blueprintVersions)( 'Starting a PHP server...', 'Starting up workers', expect.stringMatching( - /^Resolved WordPress release URL: https:\/\/downloads\.w(ordpress)?\.org\/release\/wordpress-\d+\.\d+\.(?:RC|beta)]\d+\.zip$/ + /^Resolved WordPress release URL: https:\/\/downloads\.w(ordpress)?\.org\/release\/wordpress-\d+\.\d+(?:\.\d+|-RC\d+|-beta\d+)?\.zip$/ ), 'Fetching SQLite integration plugin...', 'Booting WordPress...',