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