Skip to content

Commit aabd24f

Browse files
committed
Add a simulateConnectionErrors option for passthrough rules
1 parent 70326a8 commit aabd24f

File tree

3 files changed

+66
-3
lines changed

3 files changed

+66
-3
lines changed

src/rules/requests/request-handler-definitions.ts

+30
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,31 @@ export interface PassThroughHandlerOptions {
534534
*/
535535
lookupOptions?: PassThroughLookupOptions;
536536

537+
/**
538+
* Whether to simulate connection errors back to the client.
539+
*
540+
* By default (in most cases - see below) when an upstream request fails
541+
* outright a 502 "Bad Gateway" response is sent to the downstream client,
542+
* explicitly indicating the failure and containing the error that caused
543+
* the issue in the response body.
544+
*
545+
* Only in the case of upstream connection reset errors is a connection reset
546+
* normally sent back downstream to existing clients (this behaviour exists
547+
* for backward compatibility, and will change to match other error behaviour
548+
* in a future version).
549+
*
550+
* When this option is set to `true`, low-level connection failures will
551+
* always trigger a downstream connection close/reset, rather than a 502
552+
* response.
553+
*
554+
* This includes DNS failures, TLS connection errors, TCP connection resets,
555+
* etc (but not HTTP non-200 responses, which are still proxied as normal).
556+
* This is less convenient for debugging in a testing environment or when
557+
* using a proxy intentionally, but can be more accurate when trying to
558+
* transparently proxy network traffic, errors and all.
559+
*/
560+
simulateConnectionErrors?: boolean;
561+
537562
/**
538563
* A set of data to automatically transform a request. This includes properties
539564
* to support many transformation common use cases.
@@ -724,6 +749,7 @@ export interface SerializedPassThroughData {
724749
extraCACertificates?: Array<{ cert: string } | { certPath: string }>;
725750
clientCertificateHostMap?: { [host: string]: { pfx: string, passphrase?: string } };
726751
lookupOptions?: PassThroughLookupOptions;
752+
simulateConnectionErrors?: boolean;
727753

728754
transformRequest?: Replace<RequestTransform, {
729755
'replaceBody'?: string, // Serialized as base64 buffer
@@ -792,6 +818,8 @@ export class PassThroughHandlerDefinition extends Serializable implements Reques
792818

793819
public readonly lookupOptions?: PassThroughLookupOptions;
794820

821+
public readonly simulateConnectionErrors: boolean;
822+
795823
// Used in subclass - awkwardly needs to be initialized here to ensure that its set when using a
796824
// handler built from a definition. In future, we could improve this (compose instead of inheritance
797825
// to better control handler construction?) but this will do for now.
@@ -823,6 +851,7 @@ export class PassThroughHandlerDefinition extends Serializable implements Reques
823851

824852
this.lookupOptions = options.lookupOptions;
825853
this.proxyConfig = options.proxyConfig;
854+
this.simulateConnectionErrors = !!options.simulateConnectionErrors;
826855

827856
this.clientCertificateHostMap = options.clientCertificateHostMap || {};
828857
this.extraCACertificates = options.trustAdditionalCAs || [];
@@ -932,6 +961,7 @@ export class PassThroughHandlerDefinition extends Serializable implements Reques
932961
} : {},
933962
proxyConfig: serializeProxyConfig(this.proxyConfig, channel),
934963
lookupOptions: this.lookupOptions,
964+
simulateConnectionErrors: this.simulateConnectionErrors,
935965
ignoreHostCertificateErrors: this.ignoreHostHttpsErrors,
936966
extraCACertificates: this.extraCACertificates.map((certObject) => {
937967
// We use toString to make sure that buffers always end up as

src/rules/requests/request-handlers.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -938,11 +938,15 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
938938
}
939939
clientRes.tags.push('passthrough-error:' + e.code);
940940

941-
if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED') {
941+
if (e.code === 'ECONNRESET' || e.code === 'ECONNREFUSED' || this.simulateConnectionErrors) {
942942
// The upstream socket closed: forcibly close the downstream stream to match
943943
const socket: net.Socket = (clientReq as any).socket;
944-
socket.destroy();
945-
reject(new AbortError('Upstream connection was reset'));
944+
if ('resetAndDestroy' in socket) {
945+
socket.resetAndDestroy();
946+
} else {
947+
socket.destroy();
948+
}
949+
reject(new AbortError('Upstream connection failed'));
946950
} else {
947951
e.statusCode = 502;
948952
e.statusMessage = 'Error communicating with upstream server';
@@ -1066,6 +1070,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
10661070
} : {},
10671071
forwarding: data.forwarding,
10681072
lookupOptions: data.lookupOptions,
1073+
simulateConnectionErrors: !!data.simulateConnectionErrors,
10691074
ignoreHostHttpsErrors: data.ignoreHostCertificateErrors,
10701075
trustAdditionalCAs: data.extraCACertificates,
10711076
clientCertificateHostMap: _.mapValues(data.clientCertificateHostMap,

test/integration/proxy.spec.ts

+28
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,34 @@ nodeOnly(() => {
894894
expect(response.statusCode).to.equal(500);
895895
});
896896

897+
it("should return a 502 if proxying fails", async () => {
898+
await server.forGet().thenPassThrough();
899+
900+
let response = await request.get(`http://invalid.example`, {
901+
resolveWithFullResponse: true,
902+
simple: false
903+
});
904+
905+
expect(response.statusCode).to.equal(502);
906+
});
907+
908+
it("should kill the connection if proxying fails with error simulation", async () => {
909+
await server.forGet().thenPassThrough({
910+
simulateConnectionErrors: true
911+
});
912+
913+
let result = await request.get(`http://invalid.example`, {
914+
resolveWithFullResponse: true,
915+
simple: false
916+
}).catch(e => e);
917+
918+
expect(result).to.be.instanceOf(Error);
919+
expect(result.message).to.be.oneOf([
920+
'Error: socket hang up',
921+
'Error: read ECONNRESET'
922+
]);
923+
});
924+
897925
describe("with an IPv6-only server", () => {
898926
if (!isLocalIPv6Available) return;
899927

0 commit comments

Comments
 (0)