Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions packages/php-wasm/web-service-worker/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,19 @@ export async function cloneRequest(
request: Request,
overrides: Record<string, any>
): Promise<Request> {
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,
Expand All @@ -182,6 +191,9 @@ 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.
...(body && { duplex: 'half' }),
...overrides,
});
}
Expand Down
76 changes: 59 additions & 17 deletions packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -58,16 +62,20 @@ 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!');
});
Expand Down Expand Up @@ -138,10 +146,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<void>((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 () => {
Expand Down Expand Up @@ -545,17 +574,30 @@ async function makeRequest({
}

async function bufferResponse(socket: TCPOverFetchWebsocket): Promise<string> {
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);
});
});
}
47 changes: 47 additions & 0 deletions packages/php-wasm/web/src/lib/test-utils/generate-certificate.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion packages/playground/cli/tests/run-cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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+(?:\.\d+|-RC\d+|-beta\d+)?\.zip$/
),
'Fetching SQLite integration plugin...',
'Booting WordPress...',
Expand Down