Skip to content

Commit a1f2b75

Browse files
authored
[TCP Proxy] Produce the correct request path in parseHttpRequest() (#2852)
Solves a TCP<->fetch() issue where query strings were being double-encoded and duplicated. When PHP tried to request `/core/version-check/1.7/?channel=beta` via file_get_contents(), parseHttpRequest() would source an invalid URL for fetch(): `/core/version-check/1.7/%3Fchannel=beta?channel=beta` This is caused by a redundant and incorrect assignment `url.pathname = parsedHeaders.path;` that this PR removes. ## Testing instructions CI
1 parent 24ed901 commit a1f2b75

File tree

2 files changed

+144
-1
lines changed

2 files changed

+144
-1
lines changed

packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.spec.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,150 @@ describe('RawBytesFetch', () => {
368368
);
369369
expect(decodedRequestBody).toEqual(encodedBodyBytes);
370370
});
371+
372+
it('parseHttpRequest should handle a path and query string', async () => {
373+
const requestBytes = `GET /core/version-check/1.7/?channel=beta HTTP/1.1\r\nHost: playground.internal\r\n\r\n`;
374+
const request = await RawBytesFetch.parseHttpRequest(
375+
new ReadableStream({
376+
start(controller) {
377+
controller.enqueue(new TextEncoder().encode(requestBytes));
378+
controller.close();
379+
},
380+
}),
381+
'playground.internal',
382+
'http'
383+
);
384+
expect(request.url).toEqual(
385+
'http://playground.internal/core/version-check/1.7/?channel=beta'
386+
);
387+
});
388+
389+
it('parseHttpRequest should handle a simple path without query string', async () => {
390+
const requestBytes = `GET /api/users HTTP/1.1\r\nHost: example.com\r\n\r\n`;
391+
const request = await RawBytesFetch.parseHttpRequest(
392+
new ReadableStream({
393+
start(controller) {
394+
controller.enqueue(new TextEncoder().encode(requestBytes));
395+
controller.close();
396+
},
397+
}),
398+
'example.com',
399+
'http'
400+
);
401+
expect(request.url).toEqual('http://example.com/api/users');
402+
});
403+
404+
it('parseHttpRequest should handle root path', async () => {
405+
const requestBytes = `GET / HTTP/1.1\r\nHost: example.com\r\n\r\n`;
406+
const request = await RawBytesFetch.parseHttpRequest(
407+
new ReadableStream({
408+
start(controller) {
409+
controller.enqueue(new TextEncoder().encode(requestBytes));
410+
controller.close();
411+
},
412+
}),
413+
'example.com',
414+
'https'
415+
);
416+
expect(request.url).toEqual('https://example.com/');
417+
});
418+
419+
it('parseHttpRequest should handle URL-encoded characters in path', async () => {
420+
const requestBytes = `GET /search/hello%20world HTTP/1.1\r\nHost: example.com\r\n\r\n`;
421+
const request = await RawBytesFetch.parseHttpRequest(
422+
new ReadableStream({
423+
start(controller) {
424+
controller.enqueue(new TextEncoder().encode(requestBytes));
425+
controller.close();
426+
},
427+
}),
428+
'example.com',
429+
'http'
430+
);
431+
expect(request.url).toEqual('http://example.com/search/hello%20world');
432+
});
433+
434+
it('parseHttpRequest should handle URL-encoded characters in query string', async () => {
435+
const requestBytes = `GET /search?q=hello+world&filter=a%26b HTTP/1.1\r\nHost: example.com\r\n\r\n`;
436+
const request = await RawBytesFetch.parseHttpRequest(
437+
new ReadableStream({
438+
start(controller) {
439+
controller.enqueue(new TextEncoder().encode(requestBytes));
440+
controller.close();
441+
},
442+
}),
443+
'example.com',
444+
'http'
445+
);
446+
expect(request.url).toEqual(
447+
'http://example.com/search?q=hello+world&filter=a%26b'
448+
);
449+
});
450+
451+
it('parseHttpRequest should handle empty query parameter values', async () => {
452+
const requestBytes = `GET /api?key1=&key2=value2 HTTP/1.1\r\nHost: example.com\r\n\r\n`;
453+
const request = await RawBytesFetch.parseHttpRequest(
454+
new ReadableStream({
455+
start(controller) {
456+
controller.enqueue(new TextEncoder().encode(requestBytes));
457+
controller.close();
458+
},
459+
}),
460+
'example.com',
461+
'http'
462+
);
463+
expect(request.url).toEqual('http://example.com/api?key1=&key2=value2');
464+
});
465+
466+
it('parseHttpRequest should handle path with hash fragment', async () => {
467+
// Note: Hash fragments are typically not sent in HTTP requests,
468+
// but if they are, the URL constructor should handle them
469+
const requestBytes = `GET /page#section HTTP/1.1\r\nHost: example.com\r\n\r\n`;
470+
const request = await RawBytesFetch.parseHttpRequest(
471+
new ReadableStream({
472+
start(controller) {
473+
controller.enqueue(new TextEncoder().encode(requestBytes));
474+
controller.close();
475+
},
476+
}),
477+
'example.com',
478+
'http'
479+
);
480+
expect(request.url).toEqual('http://example.com/page#section');
481+
});
482+
483+
it('parseHttpRequest should handle path with query and hash', async () => {
484+
const requestBytes = `GET /page?param=value#section HTTP/1.1\r\nHost: example.com\r\n\r\n`;
485+
const request = await RawBytesFetch.parseHttpRequest(
486+
new ReadableStream({
487+
start(controller) {
488+
controller.enqueue(new TextEncoder().encode(requestBytes));
489+
controller.close();
490+
},
491+
}),
492+
'example.com',
493+
'http'
494+
);
495+
expect(request.url).toEqual(
496+
'http://example.com/page?param=value#section'
497+
);
498+
});
499+
500+
it('parseHttpRequest should preserve Host header over default host', async () => {
501+
const requestBytes = `GET /api HTTP/1.1\r\nHost: custom.host.com\r\n\r\n`;
502+
const request = await RawBytesFetch.parseHttpRequest(
503+
new ReadableStream({
504+
start(controller) {
505+
controller.enqueue(new TextEncoder().encode(requestBytes));
506+
controller.close();
507+
},
508+
}),
509+
'default.host.com', // Different from Host header
510+
'https'
511+
);
512+
// Should use the Host header, not the default host parameter
513+
expect(request.url).toEqual('https://custom.host.com/api');
514+
});
371515
});
372516

373517
type MakeRequestOptions = {

packages/php-wasm/web/src/lib/tcp-over-fetch-websocket.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,6 @@ export class RawBytesFetch {
686686
*/
687687
const hostname = parsedHeaders.headers.get('Host') ?? host;
688688
const url = new URL(parsedHeaders.path, protocol + '://' + hostname);
689-
url.pathname = parsedHeaders.path;
690689

691690
return new Request(url.toString(), {
692691
method: parsedHeaders.method,

0 commit comments

Comments
 (0)