Skip to content

Commit

Permalink
Add trailer data to request & response subscription events
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry committed May 1, 2024
1 parent c3d88d1 commit 1e3f1cb
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 79 deletions.
2 changes: 2 additions & 0 deletions src/admin/mockttp-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const MockttpSchema = gql`
rawHeaders: Json!
body: Buffer!
rawTrailers: Json!
}
type AbortedRequest {
Expand Down Expand Up @@ -195,6 +196,7 @@ export const MockttpSchema = gql`
headers: Json!
rawHeaders: Json!
body: Buffer!
rawTrailers: Json!
}
type WebSocketMessage {
Expand Down
135 changes: 75 additions & 60 deletions src/client/mockttp-admin-request-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ function normalizeHttpMessage(message: any, event?: SubscribableEvent) {
message.rawHeaders = objectHeadersToRaw(message.headers);
}

if (message.rawTrailers) {
message.rawTrailers = JSON.parse(message.rawTrailers);
message.trailers = rawHeadersToObject(message.rawTrailers);
} else if (message.rawHeaders && message.body) { // HTTP events with bodies should have trailers
message.rawTrailers = [];
message.trailers = {};
}

if (message.body !== undefined) {
// Body is serialized as the raw encoded buffer in base64
message.body = buildBodyReader(Buffer.from(message.body, 'base64'), message.headers);
Expand Down Expand Up @@ -218,128 +226,134 @@ export class MockttpAdminRequestBuilder {
const query = {
'request-initiated': gql`subscription OnRequestInitiated {
requestInitiated {
id,
protocol,
method,
url,
path,
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')},
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')},
hostname,
id
protocol
method
url
path
${this.schema.asOptionalField('InitiatedRequest', 'remoteIpAddress')}
${this.schema.asOptionalField('InitiatedRequest', 'remotePort')}
hostname
${this.schema.typeHasField('InitiatedRequest', 'rawHeaders')
? 'rawHeaders'
: 'headers'
}
timingEvents,
httpVersion,
timingEvents
httpVersion
${this.schema.asOptionalField('InitiatedRequest', 'tags')}
}
}`,
request: gql`subscription OnRequest {
requestReceived {
id,
id
${this.schema.asOptionalField('Request', 'matchedRuleId')}
protocol,
method,
url,
path,
${this.schema.asOptionalField('Request', 'remoteIpAddress')},
${this.schema.asOptionalField('Request', 'remotePort')},
hostname,
protocol
method
url
path
${this.schema.asOptionalField('Request', 'remoteIpAddress')}
${this.schema.asOptionalField('Request', 'remotePort')}
hostname
${this.schema.typeHasField('Request', 'rawHeaders')
? 'rawHeaders'
: 'headers'
}
body,
body
${this.schema.asOptionalField('Request', 'rawTrailers')}
${this.schema.asOptionalField('Request', 'timingEvents')}
${this.schema.asOptionalField('Request', 'httpVersion')}
${this.schema.asOptionalField('Request', 'tags')}
}
}`,
response: gql`subscription OnResponse {
responseCompleted {
id,
statusCode,
statusMessage,
id
statusCode
statusMessage
${this.schema.typeHasField('Response', 'rawHeaders')
? 'rawHeaders'
: 'headers'
}
body,
body
${this.schema.asOptionalField('Response', 'rawTrailers')}
${this.schema.asOptionalField('Response', 'timingEvents')}
${this.schema.asOptionalField('Response', 'tags')}
}
}`,
'websocket-request': gql`subscription OnWebSocketRequest {
webSocketRequest {
id,
matchedRuleId,
protocol,
method,
url,
path,
remoteIpAddress,
remotePort,
hostname,
id
matchedRuleId
protocol
method
url
path
remoteIpAddress
remotePort
hostname
rawHeaders,
body,
rawHeaders
body
${this.schema.asOptionalField('Request', 'rawTrailers')}
timingEvents,
httpVersion,
timingEvents
httpVersion
tags
}
}`,
'websocket-accepted': gql`subscription OnWebSocketAccepted {
webSocketAccepted {
id,
statusCode,
statusMessage,
id
statusCode
statusMessage
rawHeaders,
body,
rawHeaders
body
${this.schema.asOptionalField('Response', 'rawTrailers')}
timingEvents,
timingEvents
tags
}
}`,
'websocket-message-received': gql`subscription OnWebSocketMessageReceived {
webSocketMessageReceived {
streamId,
direction,
content,
isBinary,
eventTimestamp,
streamId
direction
content
isBinary
eventTimestamp
timingEvents,
timingEvents
tags
}
}`,
'websocket-message-sent': gql`subscription OnWebSocketMessageSent {
webSocketMessageSent {
streamId,
direction,
content,
isBinary,
eventTimestamp,
streamId
direction
content
isBinary
eventTimestamp
timingEvents,
timingEvents
tags
}
}`,
'websocket-close': gql`subscription OnWebSocketClose {
webSocketClose {
streamId,
streamId
closeCode,
closeReason,
closeCode
closeReason
timingEvents,
timingEvents
tags
}
}`,
Expand Down Expand Up @@ -417,8 +431,8 @@ export class MockttpAdminRequestBuilder {
: 'headers'
}
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')},
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')},
${this.schema.asOptionalField('ClientErrorRequest', 'remoteIpAddress')}
${this.schema.asOptionalField('ClientErrorRequest', 'remotePort')}
}
response {
id
Expand All @@ -433,6 +447,7 @@ export class MockttpAdminRequestBuilder {
}
body
${this.schema.asOptionalField('Response', 'rawTrailers')}
}
}
}`,
Expand Down
4 changes: 3 additions & 1 deletion src/rules/requests/request-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ export class RequestRule implements RequestRule {
const initiatedRequest = buildInitiatedRequest(req);
return {
...initiatedRequest,
body: buildBodyReader(Buffer.from([]), req.headers)
body: buildBodyReader(Buffer.from([]), req.headers),
rawTrailers: [],
trailers: {}
};
})
);
Expand Down
16 changes: 15 additions & 1 deletion src/server/mockttp-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ import {
WebSocketMessage,
WebSocketClose,
TlsPassthroughEvent,
RuleEvent
RuleEvent,
RawTrailers
} from "../types";
import { DestroyableServer } from "destroyable-server";
import {
Expand Down Expand Up @@ -614,10 +615,21 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
makePropertyWritable(req, 'headers');
makePropertyWritable(req, 'rawHeaders');

let rawTrailers: RawTrailers | undefined;
Object.defineProperty(req, 'rawTrailers', {
get: () => rawTrailers,
set: (flatRawTrailers) => {
rawTrailers = flatRawTrailers
? pairFlatRawHeaders(flatRawTrailers)
: undefined;
}
});

return Object.assign(req, {
id,
headers,
rawHeaders,
rawTrailers, // Just makes the type happy - really managed by property above
remoteIpAddress: req.socket.remoteAddress,
remotePort: req.socket.remotePort,
timingEvents,
Expand Down Expand Up @@ -967,6 +979,8 @@ ${await this.suggestRule(request)}`
...commonParams,
headers: { 'connection': 'close' },
rawHeaders: [['Connection', 'close']],
trailers: {},
rawTrailers: [],
statusCode:
isHeaderOverflow
? 431
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export interface Trailers {
}

export type RawHeaders = Array<[key: string, value: string]>;
export type RawTrailers = RawHeaders; // Just a convenient alias

export interface Request {
id: string;
Expand Down Expand Up @@ -147,6 +148,7 @@ export interface TlsFailureTimingEvents extends TlsTimingEvents {
// Internal representation of an ongoing HTTP request whilst it's being processed
export interface OngoingRequest extends Request, EventEmitter {
body: OngoingBody;
rawTrailers?: RawHeaders;
}

export interface OngoingBody {
Expand Down Expand Up @@ -229,6 +231,8 @@ export interface AbortedRequest extends InitiatedRequest {
// Internal & external representation of a fully completed HTTP request
export interface CompletedRequest extends Request {
body: CompletedBody;
rawTrailers: RawTrailers;
trailers: Trailers;
}

export interface TimingEvents {
Expand All @@ -253,6 +257,7 @@ export interface OngoingResponse extends http.ServerResponse {
getHeaders(): Headers;
getRawHeaders(): RawHeaders;
body: OngoingBody;
getRawTrailers(): RawTrailers;
timingEvents: TimingEvents;
tags: string[];
}
Expand All @@ -264,6 +269,8 @@ export interface CompletedResponse {
headers: Headers;
rawHeaders: RawHeaders;
body: CompletedBody;
rawTrailers: RawTrailers;
trailers: Trailers;
timingEvents: TimingEvents;
tags: string[];
}
Expand Down

0 comments on commit 1e3f1cb

Please sign in to comment.