Skip to content

Commit 5abadb6

Browse files
committed
Store pass through upstream request data in the model
1 parent 396fea5 commit 5abadb6

File tree

11 files changed

+529
-42
lines changed

11 files changed

+529
-42
lines changed

src/model/events/categorization.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { getHeaderValue } from '../../util/headers';
77
import { getBaseContentType } from './content-types';
88

99
import { HTKEventBase } from './event-base';
10-
import { HttpExchange, SuccessfulExchange } from '../http/exchange';
10+
import { ViewableHttpExchange, SuccessfulExchange } from '../http/exchange';
1111

1212
export const getMessageBaseAcceptTypes = (message: ExchangeMessage) =>
1313
(getHeaderValue(message.headers, 'accept')?.split(',') || [])
@@ -16,7 +16,7 @@ export const getMessageBaseAcceptTypes = (message: ExchangeMessage) =>
1616
export const getMessageBaseContentType = (message: ExchangeMessage) =>
1717
getBaseContentType(getHeaderValue(message.headers, 'content-type'));
1818

19-
const isMutatativeExchange = (exchange: HttpExchange) => _.includes([
19+
const isMutatativeExchange = (exchange: ViewableHttpExchange) => _.includes([
2020
'POST',
2121
'PATCH',
2222
'PUT',
@@ -182,7 +182,9 @@ const highlights = {
182182
pink: '#dd3a96'
183183
};
184184

185-
export function getSummaryColor(exchangeOrCategory: HttpExchange | EventCategory): string {
185+
export function getSummaryColor(
186+
exchangeOrCategory: ViewableHttpExchange | EventCategory
187+
): string {
186188
const category = typeof exchangeOrCategory === 'string' ?
187189
exchangeOrCategory : exchangeOrCategory.category;
188190

src/model/events/event-base.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { observable, computed } from 'mobx';
33
import {
44
FailedTlsConnection,
55
TlsTunnel,
6-
HttpExchange,
6+
ViewableHttpExchange,
77
RTCConnection,
88
RTCDataChannel,
99
RTCMediaTrack,
@@ -17,7 +17,7 @@ export abstract class HTKEventBase {
1717
abstract get id(): string;
1818

1919
// These can be overriden by subclasses to allow easy type narrowing:
20-
isHttp(): this is HttpExchange { return false; }
20+
isHttp(): this is ViewableHttpExchange { return false; }
2121
isWebSocket(): this is WebSocketStream { return false; }
2222

2323
isTlsFailure(): this is FailedTlsConnection { return false; }
@@ -33,10 +33,14 @@ export abstract class HTKEventBase {
3333
}
3434

3535
@observable
36-
public searchIndex: string = '';
36+
private _searchIndex: string = '';
37+
public get searchIndex(): string { return this._searchIndex; }
38+
public set searchIndex(value: string) { this._searchIndex = value; }
3739

3840
@observable
39-
public pinned: boolean = false;
41+
private _pinned: boolean = false;
42+
public get pinned(): boolean { return this._pinned; }
43+
public set pinned(value: boolean) { this._pinned = value; }
4044

4145
// Logic elsewhere can put values into these caches to cache calculations
4246
// about this event weakly, so they GC with the event.

src/model/events/events-store.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
InputRTCMediaStats,
2929
InputRTCMediaTrackClosed,
3030
InputRTCExternalPeerAttached,
31+
InputRuleEvent,
3132
TimingEvents
3233
} from '../../types';
3334

@@ -68,6 +69,7 @@ type EventTypesMap = {
6869
'tls-passthrough-opened': InputTlsPassthrough,
6970
'tls-passthrough-closed': InputTlsPassthrough,
7071
'client-error': InputClientError,
72+
'rule-event': InputRuleEvent
7173
} & {
7274
// MockRTC:
7375
[K in InputRTCEvent]: InputRTCEventData[K];
@@ -86,7 +88,8 @@ const mockttpEventTypes = [
8688
'tls-client-error',
8789
'tls-passthrough-opened',
8890
'tls-passthrough-closed',
89-
'client-error'
91+
'client-error',
92+
'rule-event'
9093
] as const;
9194

9295
const mockRTCEventTypes = [
@@ -260,6 +263,9 @@ export class EventsStore {
260263
case 'client-error':
261264
return this.addClientError(queuedEvent.event);
262265

266+
case 'rule-event':
267+
return this.addRuleEvent(queuedEvent.event);
268+
263269
case 'peer-connected':
264270
return this.addRTCPeerConnection(queuedEvent.event);
265271
case 'external-peer-attached':
@@ -494,6 +500,31 @@ export class EventsStore {
494500
this.events.push(exchange);
495501
}
496502

503+
@action
504+
private addRuleEvent(event: InputRuleEvent) {
505+
const exchange = _.find(this.exchanges, { id: event.requestId });
506+
507+
if (!exchange) {
508+
// Handle this later, once the request has arrived
509+
this.orphanedEvents[event.requestId] = { type: 'rule-event', event };
510+
return;
511+
};
512+
513+
switch (event.eventType) {
514+
case 'passthrough-request-head':
515+
exchange.updateFromUpstreamRequestHead(event.eventData);
516+
break;
517+
case 'passthrough-request-body':
518+
exchange.updateFromUpstreamRequestBody(event.eventData);
519+
break;
520+
// Only request events handled for now
521+
case 'passthrough-response-head':
522+
case 'passthrough-response-body':
523+
case 'passthrough-abort':
524+
break;
525+
}
526+
}
527+
497528
@action
498529
private addRTCPeerConnection(event: InputRTCPeerConnected) {
499530
this.events.push(new RTCConnection(event));

src/model/http/exchange.ts

Lines changed: 90 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
MockttpBreakpointedResponse,
1616
InputCompletedRequest,
1717
MockttpBreakpointResponseResult,
18+
InputRuleEventDataMap,
19+
RawHeaders
1820
} from "../../types";
1921
import {
2022
fakeBuffer,
@@ -25,8 +27,10 @@ import { UnreachableCheck } from '../../util/error';
2527
import { lazyObservablePromise, ObservablePromise, observablePromise } from "../../util/observable";
2628
import {
2729
asHeaderArray,
28-
getHeaderValue
30+
getHeaderValue,
31+
getHeaderValues
2932
} from '../../util/headers';
33+
import { ParsedUrl } from '../../util/url';
3034

3135
import { logError } from '../../errors';
3236

@@ -50,8 +54,14 @@ import {
5054
getResponseBreakpoint,
5155
getDummyResponseBreakpoint
5256
} from './exchange-breakpoint';
57+
import { UpstreamHttpExchange } from './upstream-exchange';
5358

54-
function tryParseUrl(request: InputRequest): (URL & { parseable: true }) | undefined {
59+
export type HttpVersion = 2 | 1;
60+
export function parseHttpVersion(version: string | undefined): HttpVersion {
61+
return version === '2.0' ? 2 : 1;
62+
}
63+
64+
function tryParseUrl(request: InputRequest): ParsedUrl | undefined {
5565
try {
5666
return Object.assign(
5767
new URL(request.url, `${request.protocol}://${request.hostname || 'unknown.invalid'}`),
@@ -64,7 +74,7 @@ function tryParseUrl(request: InputRequest): (URL & { parseable: true }) | undef
6474
}
6575
}
6676

67-
function getFallbackUrl(request: InputRequest): URL & { parseable: false } {
77+
function getFallbackUrl(request: InputRequest): ParsedUrl {
6878
try {
6979
return Object.assign(
7080
new URL("/[unparseable]", `${request.protocol}://${request.hostname || 'unknown.invalid'}`),
@@ -106,8 +116,8 @@ function addResponseMetadata(response: InputResponse): HtkResponse {
106116
export class HttpBody implements MessageBody {
107117

108118
constructor(
109-
message: InputMessage,
110-
headers: Headers
119+
message: InputMessage | { body: Uint8Array },
120+
headers: Headers | RawHeaders
111121
) {
112122
if (!('body' in message) || !message.body) {
113123
this._encoded = stringToBuffer("");
@@ -118,7 +128,7 @@ export class HttpBody implements MessageBody {
118128
this._decoded = message.body.decoded;
119129
}
120130

121-
this._contentEncoding = asHeaderArray(headers['content-encoding']);
131+
this._contentEncoding = asHeaderArray(getHeaderValues(headers, 'content-encoding'));
122132
}
123133

124134
private _contentEncoding: string[];
@@ -179,17 +189,61 @@ export class HttpBody implements MessageBody {
179189
}
180190
}
181191

182-
export type CompletedRequest = Omit<HttpExchange, 'request'> & {
192+
export type CompletedRequest = Omit<ViewableHttpExchange, 'request'> & {
183193
matchedRule: { id: string, handlerRype: HandlerClassKey } | false
184194
};
185-
export type CompletedExchange = Omit<HttpExchange, 'response'> & {
195+
export type CompletedExchange = Omit<ViewableHttpExchange, 'response'> & {
186196
response: HtkResponse | 'aborted'
187197
};
188-
export type SuccessfulExchange = Omit<HttpExchange, 'response'> & {
198+
export type SuccessfulExchange = Omit<ViewableHttpExchange, 'response'> & {
189199
response: HtkResponse
190200
};
191201

192-
export class HttpExchange extends HTKEventBase {
202+
/**
203+
* HttpExchanges actually come in two types: downstream (client input to Mockttp)
204+
* and upstream (Mockttp extra data for what we really forwarded - e.g. after transform).
205+
* The events actually stored in the event store's list are always downstream
206+
* exchanges, but in many cases elsewhere either can be provided.
207+
*
208+
* Both define the exact same interface, and various higher-layer components
209+
* may switch which is passed to the final view components depending on user
210+
* configuration. Any code that just reads exchange data (i.e. which doesn't
211+
* update it from events, create/import exchanges, handle breakpoints, etc)
212+
* should generally use the ViewableHttpExchange readonly interface where possible.
213+
*/
214+
export interface ViewableHttpExchange extends HTKEventBase {
215+
216+
get downstream(): HttpExchange;
217+
/**
218+
* Upstream is set if forwarded, but otherwise undefined
219+
*/
220+
get upstream(): UpstreamHttpExchange | undefined;
221+
222+
get request(): HtkRequest;
223+
get response(): HtkResponse | 'aborted' | undefined;
224+
get abortMessage(): string | undefined;
225+
get api(): ApiExchange | undefined;
226+
227+
get httpVersion(): 1 | 2;
228+
get matchedRule(): { id: string, handlerStepTypes: HandlerClassKey[] } | false | undefined;
229+
get tags(): string[];
230+
get timingEvents(): TimingEvents;
231+
232+
isHttp(): this is ViewableHttpExchange;
233+
isCompletedRequest(): this is CompletedRequest;
234+
isCompletedExchange(): this is CompletedExchange;
235+
isSuccessfulExchange(): this is SuccessfulExchange;
236+
hasRequestBody(): this is CompletedRequest;
237+
hasResponseBody(): this is SuccessfulExchange;
238+
239+
get requestBreakpoint(): RequestBreakpoint | undefined;
240+
get responseBreakpoint(): ResponseBreakpoint | undefined;
241+
242+
hideErrors: boolean;
243+
244+
}
245+
246+
export class HttpExchange extends HTKEventBase implements ViewableHttpExchange {
193247

194248
constructor(apiStore: ApiStore, request: InputRequest) {
195249
super();
@@ -217,9 +271,13 @@ export class HttpExchange extends HTKEventBase {
217271
this._apiMetadataPromise = apiStore.getApi(this.request);
218272
}
219273

220-
public readonly request: HtkRequest;
221274
public readonly id: string;
222275

276+
public readonly request: HtkRequest;
277+
278+
public readonly downstream = this;
279+
public upstream: UpstreamHttpExchange | undefined;
280+
223281
@observable
224282
// Undefined initially, defined for completed requests, false for 'not available'
225283
public matchedRule: { id: string, handlerStepTypes: HandlerClassKey[] } | false | undefined;
@@ -232,7 +290,7 @@ export class HttpExchange extends HTKEventBase {
232290

233291
@computed
234292
get httpVersion() {
235-
return this.request.httpVersion === '2.0' ? 2 : 1;
293+
return parseHttpVersion(this.request.httpVersion);
236294
}
237295

238296
isHttp(): this is HttpExchange {
@@ -261,7 +319,7 @@ export class HttpExchange extends HTKEventBase {
261319
}
262320

263321
@observable
264-
public readonly timingEvents: Partial<TimingEvents>; // May be {} if using an old server (<0.1.7)
322+
public readonly timingEvents: TimingEvents;
265323

266324
@observable.ref
267325
public response: HtkResponse | 'aborted' | undefined;
@@ -295,6 +353,20 @@ export class HttpExchange extends HTKEventBase {
295353
this.tags = _.union(this.tags, request.tags);
296354
}
297355

356+
updateFromUpstreamRequestHead(head: InputRuleEventDataMap['passthrough-request-head']) {
357+
if (!this.upstream) {
358+
this.upstream = new UpstreamHttpExchange(this);
359+
}
360+
this.upstream.updateWithRequestHead(head);
361+
}
362+
363+
updateFromUpstreamRequestBody(body: InputRuleEventDataMap['passthrough-request-body']) {
364+
if (!this.upstream) {
365+
this.upstream = new UpstreamHttpExchange(this);
366+
}
367+
this.upstream.updateWithRequestBody(body);
368+
}
369+
298370
markAborted(request: InputFailedRequest) {
299371
this.response = 'aborted';
300372
this.searchIndex += '\naborted';
@@ -357,6 +429,10 @@ export class HttpExchange extends HTKEventBase {
357429
this.response.cache.clear();
358430
this.response.body.cleanup();
359431
}
432+
433+
if (this.upstream) {
434+
this.upstream.cleanup();
435+
}
360436
}
361437

362438
// API metadata:
@@ -366,7 +442,7 @@ export class HttpExchange extends HTKEventBase {
366442

367443
// Parsed API info for this specific request, loaded & parsed lazily, only if it's used
368444
@observable.ref
369-
private _apiPromise = lazyObservablePromise(async () => {
445+
private _apiPromise = lazyObservablePromise(async (): Promise<ApiExchange | undefined> => {
370446
const apiMetadata = await this._apiMetadataPromise;
371447

372448
if (apiMetadata) {

0 commit comments

Comments
 (0)