Skip to content

Commit

Permalink
Add support for request & response clone (#209)
Browse files Browse the repository at this point in the history
  • Loading branch information
ejizba authored Jan 11, 2024
1 parent 069646b commit aa9068d
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 25 deletions.
51 changes: 34 additions & 17 deletions src/http/HttpRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,42 @@ import { fromNullableMapping } from '../converters/fromRpcNullable';
import { nonNullProp } from '../utils/nonNull';
import { extractHttpUserFromHeaders } from './extractHttpUserFromHeaders';

interface InternalHttpRequestInit extends RpcHttpData {
undiciRequest?: uRequest;
}

export class HttpRequest implements types.HttpRequest {
readonly query: URLSearchParams;
readonly params: HttpRequestParams;

#cachedUser?: HttpRequestUser | null;
#uReq: uRequest;
#body?: Buffer | string;

constructor(rpcHttp: RpcHttpData) {
const url = nonNullProp(rpcHttp, 'url');

if (rpcHttp.body?.bytes) {
this.#body = Buffer.from(rpcHttp.body?.bytes);
} else if (rpcHttp.body?.string) {
this.#body = rpcHttp.body.string;
#init: InternalHttpRequestInit;

constructor(init: InternalHttpRequestInit) {
this.#init = init;

if (init.undiciRequest) {
this.#uReq = init.undiciRequest;
} else {
const url = nonNullProp(init, 'url');

let body: Buffer | string | undefined;
if (init.body?.bytes) {
body = Buffer.from(init.body?.bytes);
} else if (init.body?.string) {
body = init.body.string;
}

this.#uReq = new uRequest(url, {
body,
method: nonNullProp(init, 'method'),
headers: fromNullableMapping(init.nullableHeaders, init.headers),
});
}

this.#uReq = new uRequest(url, {
body: this.#body,
method: nonNullProp(rpcHttp, 'method'),
headers: fromNullableMapping(rpcHttp.nullableHeaders, rpcHttp.headers),
});

this.query = new URLSearchParams(fromNullableMapping(rpcHttp.nullableQuery, rpcHttp.query));
this.params = fromNullableMapping(rpcHttp.nullableParams, rpcHttp.params);
this.query = new URLSearchParams(fromNullableMapping(init.nullableQuery, init.query));
this.params = fromNullableMapping(init.nullableParams, init.params);
}

get url(): string {
Expand Down Expand Up @@ -86,4 +97,10 @@ export class HttpRequest implements types.HttpRequest {
async text(): Promise<string> {
return this.#uReq.text();
}

clone(): HttpRequest {
const newInit = structuredClone(this.#init);
newInit.undiciRequest = this.#uReq.clone();
return new HttpRequest(newInit);
}
}
32 changes: 25 additions & 7 deletions src/http/HttpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,34 @@ import { ReadableStream } from 'stream/web';
import { FormData, Headers, Response as uResponse, ResponseInit as uResponseInit } from 'undici';
import { isDefined } from '../utils/nonNull';

interface InternalHttpResponseInit extends HttpResponseInit {
undiciResponse?: uResponse;
}

export class HttpResponse implements types.HttpResponse {
readonly cookies: types.Cookie[];
readonly enableContentNegotiation: boolean;

#uRes: uResponse;
#init: InternalHttpResponseInit;

constructor(init?: InternalHttpResponseInit) {
init ??= {};
this.#init = init;

constructor(resInit?: HttpResponseInit) {
const uResInit: uResponseInit = { status: resInit?.status, headers: resInit?.headers };
if (isDefined(resInit?.jsonBody)) {
this.#uRes = uResponse.json(resInit?.jsonBody, uResInit);
if (init.undiciResponse) {
this.#uRes = init.undiciResponse;
} else {
this.#uRes = new uResponse(resInit?.body, uResInit);
const uResInit: uResponseInit = { status: init.status, headers: init.headers };
if (isDefined(init.jsonBody)) {
this.#uRes = uResponse.json(init.jsonBody, uResInit);
} else {
this.#uRes = new uResponse(init.body, uResInit);
}
}

this.cookies = resInit?.cookies || [];
this.enableContentNegotiation = !!resInit?.enableContentNegotiation;
this.cookies = init.cookies ?? [];
this.enableContentNegotiation = !!init.enableContentNegotiation;
}

get status(): number {
Expand Down Expand Up @@ -61,4 +73,10 @@ export class HttpResponse implements types.HttpResponse {
async text(): Promise<string> {
return this.#uRes.text();
}

clone(): HttpResponse {
const newInit = structuredClone(this.#init);
newInit.undiciResponse = this.#uRes.clone();
return new HttpResponse(newInit);
}
}
31 changes: 31 additions & 0 deletions test/http/HttpRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ import { HttpRequest } from '../../src/http/HttpRequest';
chai.use(chaiAsPromised);

describe('HttpRequest', () => {
it('clone', async () => {
const req = new HttpRequest({
method: 'POST',
url: 'http://localhost:7071/api/helloWorld',
body: {
string: 'body1',
},
headers: {
a: 'b',
},
params: {
c: 'd',
},
query: {
e: 'f',
},
});
const req2 = req.clone();
expect(await req.text()).to.equal('body1');
expect(await req2.text()).to.equal('body1');

expect(req.headers).to.not.equal(req2.headers);
expect(req.headers).to.deep.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('formData', () => {
const multipartContentType = 'multipart/form-data; boundary=----WebKitFormBoundaryeJGMO2YP65ZZXRmv';
function createFormRequest(data: string, contentType: string = multipartContentType): HttpRequest {
Expand Down
36 changes: 36 additions & 0 deletions test/http/HttpResponse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License.

import 'mocha';
import * as chai from 'chai';
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { HttpResponse } from '../../src/http/HttpResponse';

chai.use(chaiAsPromised);

describe('HttpResponse', () => {
it('clone', async () => {
const res = new HttpResponse({
body: 'body1',
headers: {
a: 'b',
},
cookies: [
{
name: 'name1',
value: 'value1',
},
],
});
const res2 = res.clone();
expect(await res.text()).to.equal('body1');
expect(await res2.text()).to.equal('body1');

expect(res.headers).to.not.equal(res2.headers);
expect(res.headers).to.deep.equal(res2.headers);

expect(res.cookies).to.not.equal(res2.cookies);
expect(res.cookies).to.deep.equal(res2.cookies);
});
});
14 changes: 13 additions & 1 deletion types/http.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,12 @@ export declare class HttpRequest {
* Returns a promise fulfilled with the body as a string
*/
readonly text: () => Promise<string>;

/**
* Creates a copy of the request object, with special handling of the body.
* [Learn more here](https://developer.mozilla.org/docs/Web/API/Request/clone)
*/
readonly clone: () => HttpRequest;
}

/**
Expand Down Expand Up @@ -293,6 +299,12 @@ export declare class HttpResponse {
* Returns a promise fulfilled with the body as a string
*/
readonly text: () => Promise<string>;

/**
* Creates a copy of the response object, with special handling of the body.
* [Learn more here](https://developer.mozilla.org/docs/Web/API/Response/clone)
*/
readonly clone: () => HttpResponse;
}

/**
Expand Down Expand Up @@ -368,7 +380,7 @@ export interface HttpRequestBodyInit {
bytes?: Uint8Array;

/**
* The body as a buffer. You only need to specify one of the `bytes` or `string` properties
* The body as a string. You only need to specify one of the `bytes` or `string` properties
*/
string?: string;
}

0 comments on commit aa9068d

Please sign in to comment.