Skip to content

Commit 785e856

Browse files
committed
Fix bug where transformed bodies threw an exception when viewed
Because of recent upstream changes, if the body was transformed, when you viewed it it generated an HttpBody in a format that subsequently threw an error during protobuf content detection.
1 parent 00127b7 commit 785e856

File tree

4 files changed

+116
-98
lines changed

4 files changed

+116
-98
lines changed

src/model/http/exchange.ts

+4-93
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,19 @@ import { observable, computed, action, runInAction, when } from 'mobx';
44
import {
55
HtkRequest,
66
HtkResponse,
7-
Headers,
8-
MessageBody,
97
InputRequest,
108
InputResponse,
119
InputFailedRequest,
1210
TimingEvents,
13-
InputMessage,
1411
MockttpBreakpointedRequest,
1512
MockttpBreakpointedResponse,
1613
InputCompletedRequest,
1714
MockttpBreakpointResponseResult,
18-
InputRuleEventDataMap,
19-
RawHeaders
15+
InputRuleEventDataMap
2016
} from "../../types";
21-
import {
22-
fakeBuffer,
23-
FakeBuffer,
24-
stringToBuffer,
25-
} from '../../util/buffer';
2617
import { UnreachableCheck } from '../../util/error';
27-
import { lazyObservablePromise, ObservablePromise, observablePromise } from "../../util/observable";
28-
import {
29-
asHeaderArray,
30-
getHeaderValue,
31-
getHeaderValues
32-
} from '../../util/headers';
18+
import { lazyObservablePromise } from "../../util/observable";
19+
import { getHeaderValue } from '../../util/headers';
3320
import { ParsedUrl } from '../../util/url';
3421

3522
import { logError } from '../../errors';
@@ -46,7 +33,7 @@ import { OpenApiExchange } from '../api/openapi';
4633
import { parseRpcApiExchange } from '../api/jsonrpc';
4734
import { ApiMetadata } from '../api/api-interfaces';
4835

49-
import { decodeBody } from '../../services/ui-worker-api';
36+
import { HttpBody } from './http-body';
5037
import {
5138
RequestBreakpoint,
5239
ResponseBreakpoint,
@@ -122,82 +109,6 @@ function addResponseMetadata(response: InputResponse): HtkResponse {
122109
}) as HtkResponse;
123110
}
124111

125-
export class HttpBody implements MessageBody {
126-
127-
constructor(
128-
message: InputMessage | { body: Uint8Array },
129-
headers: Headers | RawHeaders
130-
) {
131-
if (!('body' in message) || !message.body) {
132-
this._encoded = stringToBuffer("");
133-
} else if ('buffer' in message.body) {
134-
this._encoded = message.body.buffer;
135-
} else {
136-
this._encoded = fakeBuffer(message.body.encodedLength);
137-
this._decoded = message.body.decoded;
138-
}
139-
140-
this._contentEncoding = asHeaderArray(getHeaderValues(headers, 'content-encoding'));
141-
}
142-
143-
private _contentEncoding: string[];
144-
private _encoded: FakeBuffer | Buffer;
145-
get encoded() {
146-
return this._encoded;
147-
}
148-
149-
private _decoded: Buffer | undefined;
150-
151-
@observable
152-
decodingError: Error | undefined;
153-
154-
decodedPromise: ObservablePromise<Buffer | undefined> = lazyObservablePromise(async () => {
155-
// Exactly one of _encoded & _decoded is a buffer, never neither/both.
156-
if (this._decoded) return this._decoded;
157-
const encodedBuffer = this.encoded as Buffer;
158-
159-
// Temporarily change to a fake buffer, while the web worker takes the data to decode
160-
const encodedLength = encodedBuffer.byteLength;
161-
this._encoded = fakeBuffer(encodedLength);
162-
163-
try {
164-
const { decoded, encoded } = await decodeBody(encodedBuffer, this._contentEncoding);
165-
this._encoded = encoded;
166-
return decoded;
167-
} catch (e: any) {
168-
logError(e);
169-
170-
// In most cases, we get the encoded data back regardless, so recapture it here:
171-
if (e.inputBuffer) {
172-
this._encoded = e.inputBuffer;
173-
}
174-
runInAction(() => {
175-
this.decodingError = e;
176-
});
177-
178-
return undefined;
179-
}
180-
});
181-
182-
get decoded() {
183-
// We exclude 'Error' from the value - errors should always become undefined
184-
return this.decodedPromise.value as Buffer | undefined;
185-
}
186-
187-
// Must only be called when the exchange & body will no longer be used. Ensures that large data is
188-
// definitively unlinked, since some browser issues can result in exchanges not GCing immediately.
189-
// Important: for safety, this leaves the body in a *VALID* but reset state - not a totally blank one.
190-
cleanup() {
191-
const emptyBuffer = Buffer.from([]);
192-
193-
// Set to a valid state for an un-decoded but totally empty body.
194-
this._decoded = undefined;
195-
this._encoded = emptyBuffer;
196-
this.decodingError = undefined;
197-
this.decodedPromise = observablePromise(Promise.resolve(emptyBuffer));
198-
}
199-
}
200-
201112
export type CompletedRequest = Omit<ViewableHttpExchange, 'request'> & {
202113
matchedRule: { id: string, handlerRype: HandlerClassKey } | false
203114
};

src/model/http/http-body.ts

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as _ from 'lodash';
2+
import { observable, runInAction } from 'mobx';
3+
4+
import {
5+
Headers,
6+
MessageBody,
7+
InputMessage,
8+
RawHeaders
9+
} from "../../types";
10+
import {
11+
fakeBuffer,
12+
FakeBuffer,
13+
stringToBuffer,
14+
} from '../../util/buffer';
15+
import { lazyObservablePromise, ObservablePromise, observablePromise } from "../../util/observable";
16+
import {
17+
asHeaderArray,
18+
getHeaderValues
19+
} from '../../util/headers';
20+
21+
import { logError } from '../../errors';
22+
import { decodeBody } from '../../services/ui-worker-api';
23+
24+
25+
export class HttpBody implements MessageBody {
26+
27+
constructor(
28+
message: InputMessage | { body: Buffer },
29+
headers: Headers | RawHeaders
30+
) {
31+
if (!('body' in message) || !message.body) {
32+
this._encoded = stringToBuffer("");
33+
} else if (Buffer.isBuffer(message.body)) {
34+
this._encoded = message.body;
35+
} else if ('buffer' in message.body) {
36+
this._encoded = message.body.buffer;
37+
} else {
38+
this._encoded = fakeBuffer(message.body.encodedLength);
39+
this._decoded = message.body.decoded;
40+
}
41+
42+
this._contentEncoding = asHeaderArray(getHeaderValues(headers, 'content-encoding'));
43+
}
44+
45+
private _contentEncoding: string[];
46+
private _encoded: FakeBuffer | Buffer;
47+
get encoded() {
48+
return this._encoded;
49+
}
50+
51+
private _decoded: Buffer | undefined;
52+
53+
@observable
54+
decodingError: Error | undefined;
55+
56+
decodedPromise: ObservablePromise<Buffer | undefined> = lazyObservablePromise(async () => {
57+
// Exactly one of _encoded & _decoded is a buffer, never neither/both.
58+
if (this._decoded) return this._decoded;
59+
const encodedBuffer = this.encoded as Buffer;
60+
61+
// Temporarily change to a fake buffer, while the web worker takes the data to decode
62+
const encodedLength = encodedBuffer.byteLength;
63+
this._encoded = fakeBuffer(encodedLength);
64+
65+
try {
66+
const { decoded, encoded } = await decodeBody(encodedBuffer, this._contentEncoding);
67+
this._encoded = encoded;
68+
return decoded;
69+
} catch (e: any) {
70+
logError(e);
71+
72+
// In most cases, we get the encoded data back regardless, so recapture it here:
73+
if (e.inputBuffer) {
74+
this._encoded = e.inputBuffer;
75+
}
76+
runInAction(() => {
77+
this.decodingError = e;
78+
});
79+
80+
return undefined;
81+
}
82+
});
83+
84+
get decoded() {
85+
// We exclude 'Error' from the value - errors should always become undefined
86+
return this.decodedPromise.value as Buffer | undefined;
87+
}
88+
89+
// Must only be called when the exchange & body will no longer be used. Ensures that large data is
90+
// definitively unlinked, since some browser issues can result in exchanges not GCing immediately.
91+
// Important: for safety, this leaves the body in a *VALID* but reset state - not a totally blank one.
92+
cleanup() {
93+
const emptyBuffer = Buffer.from([]);
94+
95+
// Set to a valid state for an un-decoded but totally empty body.
96+
this._decoded = undefined;
97+
this._encoded = emptyBuffer;
98+
this.decodingError = undefined;
99+
this.decodedPromise = observablePromise(Promise.resolve(emptyBuffer));
100+
}
101+
}

src/model/http/upstream-exchange.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import {
1010

1111
import { getHeaderValue, rawHeadersToHeaders } from '../../util/headers';
1212
import { ParsedUrl } from '../../util/url';
13+
import { asBuffer } from '../../util/buffer';
1314

1415
import { getContentType } from '../events/content-types';
16+
import { HttpBody } from './http-body';
1517
import {
16-
HttpBody,
1718
ViewableHttpExchange,
1819
HttpExchange,
1920
parseHttpVersion,
@@ -185,7 +186,7 @@ export class UpstreamHttpExchange extends HTKEventBase implements ViewableHttpEx
185186
headers: rawHeadersToHeaders(rawHeaders),
186187
body: body ||
187188
downstreamResData.body ||
188-
new HttpBody({ body: new Uint8Array() }, rawHeaders),
189+
new HttpBody({ body: Buffer.alloc(0) }, rawHeaders),
189190

190191
// We don't support transforming trailers:
191192
trailers: downstreamResData.trailers || {},
@@ -237,7 +238,9 @@ export class UpstreamHttpExchange extends HTKEventBase implements ViewableHttpEx
237238
if (!upstreamRequestBody.overridden) return;
238239

239240
const headers = this.upstreamRequestData!.rawHeaders;
240-
this.upstreamRequestData!.body = new HttpBody({ body: upstreamRequestBody.rawBody! }, headers);
241+
this.upstreamRequestData!.body = new HttpBody({
242+
body: asBuffer(upstreamRequestBody.rawBody!)
243+
}, headers);
241244
}
242245

243246
updateWithResponseBody(upstreamResponseData: InputRuleEventDataMap['passthrough-response-body']) {
@@ -246,7 +249,9 @@ export class UpstreamHttpExchange extends HTKEventBase implements ViewableHttpEx
246249
const headers = this.upstreamResponseData!.rawHeaders;
247250
// We're storing a full copy of the response body here, but only in the case it was actually
248251
// overridden, so this shouldn't happen much.
249-
this.upstreamResponseData!.body = new HttpBody({ body: upstreamResponseData.rawBody! }, headers);
252+
this.upstreamResponseData!.body = new HttpBody({
253+
body: asBuffer(upstreamResponseData.rawBody!)
254+
}, headers);
250255
}
251256

252257
isHttp() {

test/unit/unit-test-helpers.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as dateFns from 'date-fns';
22
import { SourceIcons } from '../../src/icons';
3-
import { HttpExchange, HttpBody } from '../../src/model/http/exchange';
3+
import { HttpExchange } from '../../src/model/http/exchange';
4+
import { HttpBody } from '../../src/model/http/http-body';
45
import { FailedTlsConnection } from '../../src/model/tls/failed-tls-connection';
56
import { HtkRequest, HtkResponse } from '../../src/types';
67

0 commit comments

Comments
 (0)