Skip to content

Commit aa9068d

Browse files
authored
Add support for request & response clone (#209)
1 parent 069646b commit aa9068d

File tree

5 files changed

+139
-25
lines changed

5 files changed

+139
-25
lines changed

src/http/HttpRequest.ts

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,42 @@ import { fromNullableMapping } from '../converters/fromRpcNullable';
1212
import { nonNullProp } from '../utils/nonNull';
1313
import { extractHttpUserFromHeaders } from './extractHttpUserFromHeaders';
1414

15+
interface InternalHttpRequestInit extends RpcHttpData {
16+
undiciRequest?: uRequest;
17+
}
18+
1519
export class HttpRequest implements types.HttpRequest {
1620
readonly query: URLSearchParams;
1721
readonly params: HttpRequestParams;
1822

1923
#cachedUser?: HttpRequestUser | null;
2024
#uReq: uRequest;
21-
#body?: Buffer | string;
22-
23-
constructor(rpcHttp: RpcHttpData) {
24-
const url = nonNullProp(rpcHttp, 'url');
25-
26-
if (rpcHttp.body?.bytes) {
27-
this.#body = Buffer.from(rpcHttp.body?.bytes);
28-
} else if (rpcHttp.body?.string) {
29-
this.#body = rpcHttp.body.string;
25+
#init: InternalHttpRequestInit;
26+
27+
constructor(init: InternalHttpRequestInit) {
28+
this.#init = init;
29+
30+
if (init.undiciRequest) {
31+
this.#uReq = init.undiciRequest;
32+
} else {
33+
const url = nonNullProp(init, 'url');
34+
35+
let body: Buffer | string | undefined;
36+
if (init.body?.bytes) {
37+
body = Buffer.from(init.body?.bytes);
38+
} else if (init.body?.string) {
39+
body = init.body.string;
40+
}
41+
42+
this.#uReq = new uRequest(url, {
43+
body,
44+
method: nonNullProp(init, 'method'),
45+
headers: fromNullableMapping(init.nullableHeaders, init.headers),
46+
});
3047
}
3148

32-
this.#uReq = new uRequest(url, {
33-
body: this.#body,
34-
method: nonNullProp(rpcHttp, 'method'),
35-
headers: fromNullableMapping(rpcHttp.nullableHeaders, rpcHttp.headers),
36-
});
37-
38-
this.query = new URLSearchParams(fromNullableMapping(rpcHttp.nullableQuery, rpcHttp.query));
39-
this.params = fromNullableMapping(rpcHttp.nullableParams, rpcHttp.params);
49+
this.query = new URLSearchParams(fromNullableMapping(init.nullableQuery, init.query));
50+
this.params = fromNullableMapping(init.nullableParams, init.params);
4051
}
4152

4253
get url(): string {
@@ -86,4 +97,10 @@ export class HttpRequest implements types.HttpRequest {
8697
async text(): Promise<string> {
8798
return this.#uReq.text();
8899
}
100+
101+
clone(): HttpRequest {
102+
const newInit = structuredClone(this.#init);
103+
newInit.undiciRequest = this.#uReq.clone();
104+
return new HttpRequest(newInit);
105+
}
89106
}

src/http/HttpResponse.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,34 @@ import { ReadableStream } from 'stream/web';
88
import { FormData, Headers, Response as uResponse, ResponseInit as uResponseInit } from 'undici';
99
import { isDefined } from '../utils/nonNull';
1010

11+
interface InternalHttpResponseInit extends HttpResponseInit {
12+
undiciResponse?: uResponse;
13+
}
14+
1115
export class HttpResponse implements types.HttpResponse {
1216
readonly cookies: types.Cookie[];
1317
readonly enableContentNegotiation: boolean;
1418

1519
#uRes: uResponse;
20+
#init: InternalHttpResponseInit;
21+
22+
constructor(init?: InternalHttpResponseInit) {
23+
init ??= {};
24+
this.#init = init;
1625

17-
constructor(resInit?: HttpResponseInit) {
18-
const uResInit: uResponseInit = { status: resInit?.status, headers: resInit?.headers };
19-
if (isDefined(resInit?.jsonBody)) {
20-
this.#uRes = uResponse.json(resInit?.jsonBody, uResInit);
26+
if (init.undiciResponse) {
27+
this.#uRes = init.undiciResponse;
2128
} else {
22-
this.#uRes = new uResponse(resInit?.body, uResInit);
29+
const uResInit: uResponseInit = { status: init.status, headers: init.headers };
30+
if (isDefined(init.jsonBody)) {
31+
this.#uRes = uResponse.json(init.jsonBody, uResInit);
32+
} else {
33+
this.#uRes = new uResponse(init.body, uResInit);
34+
}
2335
}
2436

25-
this.cookies = resInit?.cookies || [];
26-
this.enableContentNegotiation = !!resInit?.enableContentNegotiation;
37+
this.cookies = init.cookies ?? [];
38+
this.enableContentNegotiation = !!init.enableContentNegotiation;
2739
}
2840

2941
get status(): number {
@@ -61,4 +73,10 @@ export class HttpResponse implements types.HttpResponse {
6173
async text(): Promise<string> {
6274
return this.#uRes.text();
6375
}
76+
77+
clone(): HttpResponse {
78+
const newInit = structuredClone(this.#init);
79+
newInit.undiciResponse = this.#uRes.clone();
80+
return new HttpResponse(newInit);
81+
}
6482
}

test/http/HttpRequest.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,37 @@ import { HttpRequest } from '../../src/http/HttpRequest';
1111
chai.use(chaiAsPromised);
1212

1313
describe('HttpRequest', () => {
14+
it('clone', async () => {
15+
const req = new HttpRequest({
16+
method: 'POST',
17+
url: 'http://localhost:7071/api/helloWorld',
18+
body: {
19+
string: 'body1',
20+
},
21+
headers: {
22+
a: 'b',
23+
},
24+
params: {
25+
c: 'd',
26+
},
27+
query: {
28+
e: 'f',
29+
},
30+
});
31+
const req2 = req.clone();
32+
expect(await req.text()).to.equal('body1');
33+
expect(await req2.text()).to.equal('body1');
34+
35+
expect(req.headers).to.not.equal(req2.headers);
36+
expect(req.headers).to.deep.equal(req2.headers);
37+
38+
expect(req.params).to.not.equal(req2.params);
39+
expect(req.params).to.deep.equal(req2.params);
40+
41+
expect(req.query).to.not.equal(req2.query);
42+
expect(req.query).to.deep.equal(req2.query);
43+
});
44+
1445
describe('formData', () => {
1546
const multipartContentType = 'multipart/form-data; boundary=----WebKitFormBoundaryeJGMO2YP65ZZXRmv';
1647
function createFormRequest(data: string, contentType: string = multipartContentType): HttpRequest {

test/http/HttpResponse.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import 'mocha';
5+
import * as chai from 'chai';
6+
import { expect } from 'chai';
7+
import * as chaiAsPromised from 'chai-as-promised';
8+
import { HttpResponse } from '../../src/http/HttpResponse';
9+
10+
chai.use(chaiAsPromised);
11+
12+
describe('HttpResponse', () => {
13+
it('clone', async () => {
14+
const res = new HttpResponse({
15+
body: 'body1',
16+
headers: {
17+
a: 'b',
18+
},
19+
cookies: [
20+
{
21+
name: 'name1',
22+
value: 'value1',
23+
},
24+
],
25+
});
26+
const res2 = res.clone();
27+
expect(await res.text()).to.equal('body1');
28+
expect(await res2.text()).to.equal('body1');
29+
30+
expect(res.headers).to.not.equal(res2.headers);
31+
expect(res.headers).to.deep.equal(res2.headers);
32+
33+
expect(res.cookies).to.not.equal(res2.cookies);
34+
expect(res.cookies).to.deep.equal(res2.cookies);
35+
});
36+
});

types/http.d.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ export declare class HttpRequest {
145145
* Returns a promise fulfilled with the body as a string
146146
*/
147147
readonly text: () => Promise<string>;
148+
149+
/**
150+
* Creates a copy of the request object, with special handling of the body.
151+
* [Learn more here](https://developer.mozilla.org/docs/Web/API/Request/clone)
152+
*/
153+
readonly clone: () => HttpRequest;
148154
}
149155

150156
/**
@@ -293,6 +299,12 @@ export declare class HttpResponse {
293299
* Returns a promise fulfilled with the body as a string
294300
*/
295301
readonly text: () => Promise<string>;
302+
303+
/**
304+
* Creates a copy of the response object, with special handling of the body.
305+
* [Learn more here](https://developer.mozilla.org/docs/Web/API/Response/clone)
306+
*/
307+
readonly clone: () => HttpResponse;
296308
}
297309

298310
/**
@@ -368,7 +380,7 @@ export interface HttpRequestBodyInit {
368380
bytes?: Uint8Array;
369381

370382
/**
371-
* The body as a buffer. You only need to specify one of the `bytes` or `string` properties
383+
* The body as a string. You only need to specify one of the `bytes` or `string` properties
372384
*/
373385
string?: string;
374386
}

0 commit comments

Comments
 (0)