Skip to content

Commit 3ddcf06

Browse files
committed
Add error field to aborted request events, when aborted by an error
This applies to requests that were failed because the upstream connection failed (more likely with the new error simulation option) or because of connections that were intentionally closed by a rule.
1 parent aabd24f commit 3ddcf06

File tree

8 files changed

+109
-36
lines changed

8 files changed

+109
-36
lines changed

src/admin/mockttp-admin-model.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ export function buildAdminServerModel(
9898
mockServer.on('abort', (evt) => {
9999
pubsub.publish(REQUEST_ABORTED_TOPIC, {
100100
requestAborted: Object.assign(evt, {
101-
// Backward compat: old clients expect this to be present. In future this can be removed
102-
// and abort events can switch from Request to InitiatedRequest in the schema.
101+
// Backward compat: old clients expect this to be present. In future this can be
102+
// removed and abort events can lose the 'body' in the schema.
103103
body: Buffer.alloc(0)
104104
})
105105
})

src/admin/mockttp-schema.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const MockttpSchema = gql`
2727
webSocketMessageReceived: WebSocketMessage!
2828
webSocketMessageSent: WebSocketMessage!
2929
webSocketClose: WebSocketClose!
30-
requestAborted: Request!
30+
requestAborted: AbortedRequest!
3131
failedTlsRequest: TlsRequest!
3232
failedClientRequest: ClientError!
3333
}
@@ -126,6 +126,29 @@ export const MockttpSchema = gql`
126126
body: Buffer!
127127
}
128128
129+
type AbortedRequest {
130+
id: ID!
131+
timingEvents: Json!
132+
tags: [String!]!
133+
matchedRuleId: ID
134+
135+
protocol: String!
136+
httpVersion: String!
137+
method: String!
138+
url: String!
139+
path: String!
140+
remoteIpAddress: String!
141+
remotePort: Int!
142+
hostname: String
143+
144+
headers: Json!
145+
rawHeaders: Json!
146+
147+
body: Buffer!
148+
149+
error: Json
150+
}
151+
129152
type Response {
130153
id: ID!
131154
timingEvents: Json!

src/client/mockttp-admin-request-builder.ts

+4
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ export class MockttpAdminRequestBuilder {
351351
352352
${this.schema.asOptionalField('Request', 'timingEvents')}
353353
${this.schema.asOptionalField('Request', 'tags')}
354+
${this.schema.asOptionalField('AbortedRequest', 'error')}
354355
}
355356
}`,
356357
'tls-client-error': gql`subscription OnTlsClientError {
@@ -421,6 +422,9 @@ export class MockttpAdminRequestBuilder {
421422
}
422423
} else if (event === 'websocket-message-received' || event === 'websocket-message-sent') {
423424
normalizeWebSocketMessage(data);
425+
} else if (event === 'abort') {
426+
normalizeHttpMessage(data, event);
427+
data.error = data.error ? JSON.parse(data.error) : undefined;
424428
} else {
425429
normalizeHttpMessage(data, event);
426430
}

src/mockttp.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
ClientError,
1818
RulePriority,
1919
WebSocketMessage,
20-
WebSocketClose
20+
WebSocketClose,
21+
AbortedRequest
2122
} from "./types";
2223
import type { RequestRuleData } from "./rules/requests/request-rule";
2324
import type { WebSocketRuleData } from "./rules/websockets/websocket-rule";
@@ -466,7 +467,7 @@ export interface Mockttp {
466467
*
467468
* @category Events
468469
*/
469-
on(event: 'abort', callback: (req: InitiatedRequest) => void): Promise<void>;
470+
on(event: 'abort', callback: (req: AbortedRequest) => void): Promise<void>;
470471

471472
/**
472473
* Subscribe to hear about requests that start a TLS handshake, but fail to complete it.

src/rules/requests/request-handlers.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,16 @@ export {
115115

116116
// An error that indicates that the handler is aborting the request.
117117
// This could be intentional, or an upstream server aborting the request.
118-
export class AbortError extends TypedError { }
118+
export class AbortError extends TypedError {
119+
120+
constructor(
121+
message: string,
122+
readonly code?: string
123+
) {
124+
super(message);
125+
}
126+
127+
}
119128

120129
function isSerializedBuffer(obj: any): obj is SerializedBuffer {
121130
return obj && obj.type === 'Buffer' && !!obj.data;
@@ -184,7 +193,7 @@ export class CallbackHandler extends CallbackHandlerDefinition {
184193

185194
if (outResponse === 'close') {
186195
(request as any).socket.end();
187-
throw new AbortError('Connection closed (intentionally)');
196+
throw new AbortError('Connection closed intentionally by rule');
188197
} else {
189198
await writeResponseFromCallback(outResponse, response);
190199
}
@@ -553,7 +562,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
553562
if (modifiedReq.response === 'close') {
554563
const socket: net.Socket = (<any> clientReq).socket;
555564
socket.end();
556-
throw new AbortError('Connection closed (intentionally)');
565+
throw new AbortError('Connection closed intentionally by rule');
557566
} else {
558567
// The callback has provided a full response: don't passthrough at all, just use it.
559568
await writeResponseFromCallback(modifiedReq.response, clientRes);
@@ -825,7 +834,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
825834
// Dump the real response data and kill the client socket:
826835
serverRes.resume();
827836
(clientRes as any).socket.end();
828-
throw new AbortError('Connection closed (intentionally)');
837+
throw new AbortError('Connection closed intentionally by rule');
829838
}
830839

831840
validateCustomHeaders(serverHeaders, modifiedRes?.headers);
@@ -946,7 +955,10 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
946955
} else {
947956
socket.destroy();
948957
}
949-
reject(new AbortError('Upstream connection failed'));
958+
959+
reject(new AbortError(`Upstream connection error: ${
960+
e.message ?? e
961+
}`, e.code));
950962
} else {
951963
e.statusCode = 502;
952964
e.statusMessage = 'Error communicating with upstream server';
@@ -1084,7 +1096,7 @@ export class CloseConnectionHandler extends CloseConnectionHandlerDefinition {
10841096
async handle(request: OngoingRequest) {
10851097
const socket: net.Socket = (<any> request).socket;
10861098
socket.end();
1087-
throw new AbortError('Connection closed (intentionally)');
1099+
throw new AbortError('Connection closed intentionally by rule');
10881100
}
10891101
}
10901102

src/server/mockttp-server.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ServerMockedEndpoint } from "./mocked-endpoint";
3333
import { createComboServer } from "./http-combo-server";
3434
import { filter } from "../util/promise";
3535
import { Mutable } from "../util/type-utils";
36-
import { isErrorLike } from "../util/error";
36+
import { ErrorLike, isErrorLike } from "../util/error";
3737
import { makePropertyWritable } from "../util/util";
3838

3939
import {
@@ -480,12 +480,18 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
480480
});
481481
}
482482

483-
private async announceAbortAsync(request: OngoingRequest) {
483+
private async announceAbortAsync(request: OngoingRequest, abortError?: ErrorLike) {
484484
setImmediate(() => {
485485
const req = buildInitiatedRequest(request);
486486
this.eventEmitter.emit('abort', Object.assign(req, {
487487
timingEvents: _.clone(req.timingEvents),
488-
tags: _.clone(req.tags)
488+
tags: _.clone(req.tags),
489+
error: abortError ? {
490+
name: abortError.name,
491+
code: abortError.code,
492+
message: abortError.message,
493+
stack: abortError.stack
494+
} : undefined
489495
}));
490496
});
491497
}
@@ -582,18 +588,18 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
582588
if (this.debug) console.log(`Handling request for ${rawRequest.url}`);
583589

584590
let result: 'responded' | 'aborted' | null = null;
585-
const abort = () => {
591+
const abort = (error?: Error) => {
586592
if (result === null) {
587593
result = 'aborted';
588594
request.timingEvents.abortedTimestamp = now();
589-
this.announceAbortAsync(request);
595+
this.announceAbortAsync(request, error);
590596
}
591597
}
592598
request.once('aborted', abort);
593599
// In Node 16+ we don't get an abort event in many cases, just closes, but we know
594600
// it's aborted because the response is closed with no other result being set.
595601
rawResponse.once('close', () => setImmediate(abort));
596-
request.once('error', () => setImmediate(abort));
602+
request.once('error', (error) => setImmediate(() => abort(error)));
597603

598604
this.announceInitialRequestAsync(request);
599605

@@ -606,7 +612,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
606612
response.id = request.id;
607613
response.on('error', (error) => {
608614
console.log('Response error:', this.debug ? error : error.message);
609-
abort();
615+
abort(error);
610616
});
611617

612618
try {
@@ -631,7 +637,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
631637
result = result || 'responded';
632638
} catch (e) {
633639
if (e instanceof AbortError) {
634-
abort();
640+
abort(e);
635641

636642
if (this.debug) {
637643
console.error("Failed to handle request due to abort:", e);
@@ -655,7 +661,7 @@ export class MockttpServer extends AbstractMockttp implements Mockttp {
655661
response.end((isErrorLike(e) && e.toString()) || e);
656662
result = result || 'responded';
657663
} catch (e) {
658-
abort();
664+
abort(e as Error);
659665
}
660666
}
661667
}

src/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ export interface InitiatedRequest extends Request {
139139
timingEvents: TimingEvents;
140140
}
141141

142+
export interface AbortedRequest extends InitiatedRequest {
143+
error?: {
144+
name?: string;
145+
code?: string;
146+
message?: string;
147+
stack?: string;
148+
};
149+
}
150+
142151
// Internal & external representation of a fully completed HTTP request
143152
export interface CompletedRequest extends Request {
144153
body: CompletedBody;

0 commit comments

Comments
 (0)