/
http.ts
159 lines (138 loc) · 4.98 KB
/
http.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { createTransport } from '@sentry/core';
import {
BaseTransportOptions,
Transport,
TransportMakeRequestResponse,
TransportRequest,
TransportRequestExecutor,
} from '@sentry/types';
import * as http from 'http';
import * as https from 'https';
import { Readable } from 'stream';
import { URL } from 'url';
import { createGzip } from 'zlib';
import { HTTPModule } from './http-module';
export interface NodeTransportOptions extends BaseTransportOptions {
/** Define custom headers */
headers?: Record<string, string>;
/** Set a proxy that should be used for outbound requests. */
proxy?: string;
/** HTTPS proxy CA certificates */
caCerts?: string | Buffer | Array<string | Buffer>;
/** Custom HTTP module. Defaults to the native 'http' and 'https' modules. */
httpModule?: HTTPModule;
/** Allow overriding connection keepAlive, defaults to false */
keepAlive?: boolean;
}
// Estimated maximum size for reasonable standalone event
const GZIP_THRESHOLD = 1024 * 32;
/**
* Gets a stream from a Uint8Array or string
* Readable.from is ideal but was added in node.js v12.3.0 and v10.17.0
*/
function streamFromBody(body: Uint8Array | string): Readable {
return new Readable({
read() {
this.push(body);
this.push(null);
},
});
}
/**
* Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry.
*/
export function makeNodeTransport(options: NodeTransportOptions): Transport {
const urlSegments = new URL(options.url);
const isHttps = urlSegments.protocol === 'https:';
// Proxy prioritization: http => `options.proxy` | `process.env.http_proxy`
// Proxy prioritization: https => `options.proxy` | `process.env.https_proxy` | `process.env.http_proxy`
const proxy = applyNoProxyOption(
urlSegments,
options.proxy || (isHttps ? process.env.https_proxy : undefined) || process.env.http_proxy,
);
const nativeHttpModule = isHttps ? https : http;
const keepAlive = options.keepAlive === undefined ? false : options.keepAlive;
// TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node
// versions(>= 8) as they had memory leaks when using it: #2555
const agent = proxy
? (new (require('https-proxy-agent'))(proxy) as http.Agent)
: new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 });
const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent);
return createTransport(options, requestExecutor);
}
/**
* Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion.
*
* @param transportUrl The URL the transport intends to send events to.
* @param proxy The client configured proxy.
* @returns A proxy the transport should use.
*/
function applyNoProxyOption(transportUrlSegments: URL, proxy: string | undefined): string | undefined {
const { no_proxy } = process.env;
const urlIsExemptFromProxy =
no_proxy &&
no_proxy
.split(',')
.some(
exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption),
);
if (urlIsExemptFromProxy) {
return undefined;
} else {
return proxy;
}
}
/**
* Creates a RequestExecutor to be used with `createTransport`.
*/
function createRequestExecutor(
options: NodeTransportOptions,
httpModule: HTTPModule,
agent: http.Agent,
): TransportRequestExecutor {
const { hostname, pathname, port, protocol, search } = new URL(options.url);
return function makeRequest(request: TransportRequest): Promise<TransportMakeRequestResponse> {
return new Promise((resolve, reject) => {
let body = streamFromBody(request.body);
const headers: Record<string, string> = { ...options.headers };
if (request.body.length > GZIP_THRESHOLD) {
headers['content-encoding'] = 'gzip';
body = body.pipe(createGzip());
}
const req = httpModule.request(
{
method: 'POST',
agent,
headers,
hostname,
path: `${pathname}${search}`,
port,
protocol,
ca: options.caCerts,
},
res => {
res.on('data', () => {
// Drain socket
});
res.on('end', () => {
// Drain socket
});
res.setEncoding('utf8');
// "Key-value pairs of header names and values. Header names are lower-cased."
// https://nodejs.org/api/http.html#http_message_headers
const retryAfterHeader = res.headers['retry-after'] ?? null;
const rateLimitsHeader = res.headers['x-sentry-rate-limits'] ?? null;
resolve({
statusCode: res.statusCode,
headers: {
'retry-after': retryAfterHeader,
'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader,
},
});
},
);
req.on('error', reject);
body.pipe(req);
});
};
}