Skip to content

Commit d33efcd

Browse files
committed
Fire passthrough-abort events for failing upstream requests
1 parent 27b48bb commit d33efcd

File tree

2 files changed

+101
-4
lines changed

2 files changed

+101
-4
lines changed

src/rules/requests/request-handlers.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
766766
...caConfig
767767
}, (serverRes) => (async () => {
768768
serverRes.on('error', (e: any) => {
769-
e.causedByUpstreamError = true;
769+
reportUpstreamAbort(e)
770770
reject(e);
771771
});
772772

@@ -1042,7 +1042,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
10421042
overridden: true,
10431043
rawBody: upstreamBody
10441044
});
1045-
});
1045+
}).catch((e) => reportUpstreamAbort(e));
10461046
} else {
10471047
options.emitEventCallback('passthrough-response-body', {
10481048
overridden: false
@@ -1110,6 +1110,27 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
11101110
serverReq.abort();
11111111
}
11121112

1113+
// If the upstream fails, for any reason, we need to fire an event to any rule
1114+
// listeners who might be present (although only the first time)
1115+
let reportedUpstreamError = false;
1116+
function reportUpstreamAbort(e: ErrorLike & { causedByUpstreamError?: true }) {
1117+
e.causedByUpstreamError = true;
1118+
1119+
if (!options.emitEventCallback) return;
1120+
1121+
if (reportedUpstreamError) return;
1122+
reportedUpstreamError = true;
1123+
1124+
options.emitEventCallback('passthrough-abort', {
1125+
error: {
1126+
name: e.name,
1127+
code: e.code,
1128+
message: e.message,
1129+
stack: e.stack
1130+
}
1131+
});
1132+
}
1133+
11131134
// Handle the case where the downstream connection is prematurely closed before
11141135
// fully sending the request or receiving the response.
11151136
clientReq.on('aborted', abortUpstream);
@@ -1122,7 +1143,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
11221143
});
11231144

11241145
serverReq.on('error', (e: any) => {
1125-
e.causedByUpstreamError = true;
1146+
reportUpstreamAbort(e);
11261147
reject(e);
11271148
});
11281149

test/integration/subscriptions/rule-events.spec.ts

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as _ from 'lodash';
22
import * as WebSocket from 'isomorphic-ws';
3+
import { PassThrough } from 'stream';
34

45
import {
56
getLocal,
@@ -10,7 +11,8 @@ import {
1011
delay,
1112
expect,
1213
fetch,
13-
isNode
14+
isNode,
15+
nodeOnly
1416
} from "../../test-utils";
1517

1618
describe("Rule event susbcriptions", () => {
@@ -227,6 +229,80 @@ describe("Rule event susbcriptions", () => {
227229
expect(responseBodyEvent.rawBody.toString('utf8')).to.equal('Original response body');
228230
});
229231

232+
233+
it("should fire abort event if upstream body response fails", async () => {
234+
await remoteServer.forAnyRequest().thenCloseConnection();
235+
const forwardingRule = await server.forAnyRequest().thenForwardTo(remoteServer.url);
236+
237+
const ruleEvents: RuleEvent<any>[] = [];
238+
await server.on('rule-event', (e) => ruleEvents.push(e));
239+
240+
await fetch(server.url).catch(() => {});
241+
242+
await delay(100);
243+
expect(ruleEvents.length).to.equal(3);
244+
245+
const requestId = (await forwardingRule.getSeenRequests())[0].id;
246+
ruleEvents.forEach((event) => {
247+
expect(event.ruleId).to.equal(forwardingRule.id);
248+
expect(event.requestId).to.equal(requestId);
249+
});
250+
251+
expect(ruleEvents.map(e => e.eventType)).to.deep.equal([
252+
'passthrough-request-head',
253+
'passthrough-request-body',
254+
'passthrough-abort'
255+
]);
256+
257+
const responseAbortEvent = ruleEvents[2].eventData;
258+
expect(responseAbortEvent.error.name).to.equal('Error');
259+
expect(responseAbortEvent.error.message).to.equal('socket hang up');
260+
});
261+
262+
nodeOnly(() => {
263+
it("should fire abort event if upstream body response fails", async () => {
264+
const stream = new PassThrough();
265+
await remoteServer.forAnyRequest().thenStream(200, stream);
266+
const forwardingRule = await server.forAnyRequest().thenForwardTo(remoteServer.url, {
267+
transformResponse: {
268+
replaceBody: 'replaced body'
269+
}
270+
});
271+
272+
const ruleEvents: RuleEvent<any>[] = [];
273+
await server.on('rule-event', (e) => ruleEvents.push(e));
274+
275+
const response = await fetch(server.url);
276+
expect(response.status).to.equal(200);
277+
278+
stream.emit('error', new Error()); // Hard-fail part way through the body response
279+
await delay(10);
280+
281+
expect(ruleEvents.length).to.equal(4);
282+
283+
const requestId = (await forwardingRule.getSeenRequests())[0].id;
284+
ruleEvents.forEach((event) => {
285+
expect(event.ruleId).to.equal(forwardingRule.id);
286+
expect(event.requestId).to.equal(requestId);
287+
});
288+
289+
expect(ruleEvents.map(e => e.eventType)).to.deep.equal([
290+
'passthrough-request-head',
291+
'passthrough-request-body',
292+
'passthrough-response-head',
293+
'passthrough-abort'
294+
]);
295+
296+
const responseHeadEvent = ruleEvents[2].eventData;
297+
expect(responseHeadEvent.statusCode).to.equal(200); // <-- Original status
298+
299+
const responseAbortEvent = ruleEvents[3].eventData;
300+
expect(responseAbortEvent.error.name).to.equal('Error');
301+
expect(responseAbortEvent.error.code).to.equal('ECONNRESET');
302+
expect(responseAbortEvent.error.message).to.equal('aborted');
303+
});
304+
});
305+
230306
it("should fire for proxied websockets", async () => {
231307
await remoteServer.forAnyWebSocket().thenPassivelyListen();
232308
const forwardingRule = await server.forAnyWebSocket().thenForwardTo(remoteServer.url);

0 commit comments

Comments
 (0)