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
11 changes: 7 additions & 4 deletions src/http/HttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class HttpRequest implements types.HttpRequest {
}

get body(): ReadableStream<any> | null {
return this.#nativeReq.body as ReadableStream<any> | null;
return this.#nativeReq.body;
}

get bodyUsed(): boolean {
Expand All @@ -91,10 +91,12 @@ export class HttpRequest implements types.HttpRequest {
}

async blob(): Promise<Blob> {
return this.#nativeReq.blob() as Promise<Blob>;
return this.#nativeReq.blob();
}

// eslint-disable-next-line deprecation/deprecation
async formData(): Promise<FormData> {
// eslint-disable-next-line deprecation/deprecation
return this.#nativeReq.formData();
}

Expand All @@ -107,7 +109,9 @@ export class HttpRequest implements types.HttpRequest {
}

clone(): HttpRequest {
const newInit = structuredClone(this.#init);
// Exclude nativeRequest from structuredClone since Request objects can't be cloned that way
const { nativeRequest: _nativeRequest, ...initWithoutNativeReq } = this.#init;
const newInit: InternalHttpRequestInit = structuredClone(initWithoutNativeReq);
newInit.nativeRequest = this.#nativeReq.clone();
return new HttpRequest(newInit);
}
Expand Down Expand Up @@ -146,7 +150,6 @@ export function createStreamRequest(
const nativeReq = new Request(url, {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
body: body as any,
// @ts-expect-error duplex is needed for streaming but not in all TypeScript versions
duplex: 'half',
method: nonNullProp(proxyReq, 'method'),
headers,
Expand Down
11 changes: 7 additions & 4 deletions src/http/HttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ export class HttpResponse implements types.HttpResponse {
}
this.#nativeRes = new Response(jsonBody, { ...resInit, headers: jsonHeaders });
} else {
// Cast to BodyInit to satisfy the native Response constructor
// Cast to any to satisfy the native Response constructor
// Our HttpResponseBodyInit type is compatible with what Node.js accepts
this.#nativeRes = new Response(init.body as BodyInit, resInit);
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.#nativeRes = new Response(init.body as any, resInit);
}
}

Expand All @@ -54,7 +55,7 @@ export class HttpResponse implements types.HttpResponse {
}

get body(): ReadableStream<any> | null {
return this.#nativeRes.body as ReadableStream<any> | null;
return this.#nativeRes.body;
}

get bodyUsed(): boolean {
Expand All @@ -66,10 +67,12 @@ export class HttpResponse implements types.HttpResponse {
}

async blob(): Promise<Blob> {
return this.#nativeRes.blob() as Promise<Blob>;
return this.#nativeRes.blob();
}

// eslint-disable-next-line deprecation/deprecation
async formData(): Promise<FormData> {
// eslint-disable-next-line deprecation/deprecation
return this.#nativeRes.formData();
}

Expand Down
2 changes: 1 addition & 1 deletion test/Types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as path from 'path';
describe('Public TypeScript types', () => {
for (const tsVersion of ['4']) {
it(`builds with TypeScript v${tsVersion}`, async function (this: Context) {
this.timeout(10 * 1000);
this.timeout(60 * 1000);
expect(await runTsBuild(tsVersion)).to.equal(2);
});
}
Expand Down
285 changes: 285 additions & 0 deletions test/http/HttpRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,291 @@ describe('HttpRequest', () => {
expect(req.query).to.deep.equal(req2.query);
});

it('clone with bytes body', async () => {
const bodyContent = 'test body content';
const req = new HttpRequest({
method: 'POST',
url: 'http://localhost:7071/api/helloWorld',
body: {
bytes: Buffer.from(bodyContent),
},
headers: {
'content-type': 'application/octet-stream',
},
params: {
id: '123',
},
query: {
filter: 'active',
},
});
const req2 = req.clone();
expect(await req.text()).to.equal(bodyContent);
expect(await req2.text()).to.equal(bodyContent);

expect(req.headers).to.not.equal(req2.headers);
expect(req.params).to.not.equal(req2.params);
expect(req.params).to.deep.equal(req2.params);
expect(req.query).to.not.equal(req2.query);
expect(req.query).to.deep.equal(req2.query);
});

describe('clone', () => {
it('cloned request has independent headers', () => {
const req = new HttpRequest({
method: 'GET',
url: 'http://localhost:7071/api/test',
headers: {
'x-custom-header': 'original',
'content-type': 'application/json',
},
});
const cloned = req.clone();

// Modify cloned headers
cloned.headers.set('x-custom-header', 'modified');
cloned.headers.set('x-new-header', 'new-value');

// Original should be unchanged
expect(req.headers.get('x-custom-header')).to.equal('original');
expect(req.headers.has('x-new-header')).to.be.false;
// Cloned should have modifications
expect(cloned.headers.get('x-custom-header')).to.equal('modified');
expect(cloned.headers.get('x-new-header')).to.equal('new-value');
});

it('cloned request preserves URL and method', () => {
const req = new HttpRequest({
method: 'PUT',
url: 'http://localhost:7071/api/resource/123?page=2&limit=10',
});
const cloned = req.clone();

expect(cloned.url).to.equal(req.url);
expect(cloned.method).to.equal('PUT');
});

it('clone with empty body (GET request)', () => {
const req = new HttpRequest({
method: 'GET',
url: 'http://localhost:7071/api/data',
headers: {
accept: 'application/json',
},
});
const cloned = req.clone();

expect(cloned.body).to.be.null;
expect(cloned.method).to.equal('GET');
expect(cloned.headers.get('accept')).to.equal('application/json');
});

it('cloned request bodies can be consumed independently', async () => {
const jsonBody = { name: 'test', value: 42 };
const req = new HttpRequest({
method: 'POST',
url: 'http://localhost:7071/api/json',
body: {
string: JSON.stringify(jsonBody),
},
headers: {
'content-type': 'application/json',
},
});

const cloned = req.clone();

// Consume original as text
const originalText = await req.text();
expect(originalText).to.equal(JSON.stringify(jsonBody));

// Consume clone as JSON
const clonedJson = await cloned.json();
expect(clonedJson).to.deep.equal(jsonBody);
});

it('clone preserves query parameters independently', () => {
const req = new HttpRequest({
method: 'GET',
url: 'http://localhost:7071/api/search',
query: {
q: 'test',
page: '1',
limit: '20',
},
});
const cloned = req.clone();

expect(cloned.query.get('q')).to.equal('test');
expect(cloned.query.get('page')).to.equal('1');
expect(cloned.query.get('limit')).to.equal('20');

// Verify they are different objects
expect(req.query).to.not.equal(cloned.query);

// Modify cloned query
cloned.query.set('page', '2');
expect(req.query.get('page')).to.equal('1');
expect(cloned.query.get('page')).to.equal('2');
});

it('clone preserves params independently', () => {
const req = new HttpRequest({
method: 'GET',
url: 'http://localhost:7071/api/users/123/posts/456',
params: {
userId: '123',
postId: '456',
},
});
const cloned = req.clone();

expect(cloned.params.userId).to.equal('123');
expect(cloned.params.postId).to.equal('456');
expect(req.params).to.not.equal(cloned.params);
});

it('clone with nativeRequest and params', () => {
const nativeReq = new Request('http://localhost:7071/api/items/123', {
method: 'GET',
});
const req = new HttpRequest({
nativeRequest: nativeReq,
params: { id: '123' },
} as any);

const cloned = req.clone();

expect(cloned.params.id).to.equal('123');
expect(cloned.url).to.equal('http://localhost:7071/api/items/123');
expect(cloned.method).to.equal('GET');
expect(req.params).to.not.equal(cloned.params);
});

it('clone with binary body preserves data correctly', async () => {
// Create binary data with various byte values
const binaryData = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0x80, 0x7f]);
const req = new HttpRequest({
method: 'POST',
url: 'http://localhost:7071/api/binary',
body: {
bytes: binaryData,
},
headers: {
'content-type': 'application/octet-stream',
},
});

const cloned = req.clone();

const originalBuffer = await req.arrayBuffer();
const clonedBuffer = await cloned.arrayBuffer();

expect(Buffer.from(originalBuffer)).to.deep.equal(binaryData);
expect(Buffer.from(clonedBuffer)).to.deep.equal(binaryData);
});

it('clone with large body', async () => {
// Create a larger body (100KB)
const largeContent = 'x'.repeat(100 * 1024);
const req = new HttpRequest({
method: 'POST',
url: 'http://localhost:7071/api/large',
body: {
string: largeContent,
},
});

const cloned = req.clone();

expect(await req.text()).to.equal(largeContent);
expect(await cloned.text()).to.equal(largeContent);
});

it('clone with special characters in params and query', () => {
// Note: HTTP headers only support ASCII, so we test unicode in params/query only
const req = new HttpRequest({
method: 'GET',
url: 'http://localhost:7071/api/special',
headers: {
'x-custom': 'ascii-value',
},
params: {
name: 'test-value',
},
query: {
search: 'query-value',
},
});

const cloned = req.clone();

expect(cloned.headers.get('x-custom')).to.equal('ascii-value');
expect(cloned.params.name).to.equal('test-value');
expect(cloned.query.get('search')).to.equal('query-value');
});

it('clone preserves all HTTP methods', () => {
const methods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'];

for (const method of methods) {
const req = new HttpRequest({
method,
url: 'http://localhost:7071/api/methods',
});
const cloned = req.clone();
expect(cloned.method).to.equal(method);
}
});

it('clone with nullable mappings', () => {
const req = new HttpRequest({
method: 'GET',
url: 'http://localhost:7071/api/nullable',
nullableHeaders: {
'x-nullable': { value: 'test-value' },
'x-null': { value: null },
},
nullableParams: {
id: { value: '123' },
},
nullableQuery: {
filter: { value: 'active' },
},
});

const cloned = req.clone();

expect(cloned.headers.get('x-nullable')).to.equal('test-value');
expect(cloned.params.id).to.equal('123');
expect(cloned.query.get('filter')).to.equal('active');
});

it('bodyUsed state is independent after clone', async () => {
const req = new HttpRequest({
method: 'POST',
url: 'http://localhost:7071/api/bodyused',
body: {
string: 'test content',
},
});

expect(req.bodyUsed).to.be.false;

const cloned = req.clone();
expect(cloned.bodyUsed).to.be.false;

// Consume original
await req.text();
expect(req.bodyUsed).to.be.true;
expect(cloned.bodyUsed).to.be.false;

// Consume clone
await cloned.text();
expect(cloned.bodyUsed).to.be.true;
});
});

describe('formData', () => {
const multipartContentType = 'multipart/form-data; boundary=----WebKitFormBoundaryeJGMO2YP65ZZXRmv';
function createFormRequest(data: string, contentType: string = multipartContentType): HttpRequest {
Expand Down
Loading
Loading