http.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { _nullishCoalesce } from '@sentry/utils';
  2. import * as http from 'http';
  3. import * as https from 'https';
  4. import { Readable } from 'stream';
  5. import { URL } from 'url';
  6. import { createGzip } from 'zlib';
  7. import { createTransport } from '@sentry/core';
  8. import { consoleSandbox } from '@sentry/utils';
  9. import { HttpsProxyAgent } from '../proxy/index.js';
  10. // Estimated maximum size for reasonable standalone event
  11. const GZIP_THRESHOLD = 1024 * 32;
  12. /**
  13. * Gets a stream from a Uint8Array or string
  14. * Readable.from is ideal but was added in node.js v12.3.0 and v10.17.0
  15. */
  16. function streamFromBody(body) {
  17. return new Readable({
  18. read() {
  19. this.push(body);
  20. this.push(null);
  21. },
  22. });
  23. }
  24. /**
  25. * Creates a Transport that uses native the native 'http' and 'https' modules to send events to Sentry.
  26. */
  27. function makeNodeTransport(options) {
  28. let urlSegments;
  29. try {
  30. urlSegments = new URL(options.url);
  31. } catch (e) {
  32. consoleSandbox(() => {
  33. // eslint-disable-next-line no-console
  34. console.warn(
  35. '[@sentry/node]: Invalid dsn or tunnel option, will not send any events. The tunnel option must be a full URL when used.',
  36. );
  37. });
  38. return createTransport(options, () => Promise.resolve({}));
  39. }
  40. const isHttps = urlSegments.protocol === 'https:';
  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. const nativeHttpModule = isHttps ? https : http;
  48. const keepAlive = options.keepAlive === undefined ? false : options.keepAlive;
  49. // TODO(v7): Evaluate if we can set keepAlive to true. This would involve testing for memory leaks in older node
  50. // versions(>= 8) as they had memory leaks when using it: #2555
  51. const agent = proxy
  52. ? (new HttpsProxyAgent(proxy) )
  53. : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 });
  54. const requestExecutor = createRequestExecutor(options, _nullishCoalesce(options.httpModule, () => ( nativeHttpModule)), agent);
  55. return createTransport(options, requestExecutor);
  56. }
  57. /**
  58. * Honors the `no_proxy` env variable with the highest priority to allow for hosts exclusion.
  59. *
  60. * @param transportUrl The URL the transport intends to send events to.
  61. * @param proxy The client configured proxy.
  62. * @returns A proxy the transport should use.
  63. */
  64. function applyNoProxyOption(transportUrlSegments, proxy) {
  65. const { no_proxy } = process.env;
  66. const urlIsExemptFromProxy =
  67. no_proxy &&
  68. no_proxy
  69. .split(',')
  70. .some(
  71. exemption => transportUrlSegments.host.endsWith(exemption) || transportUrlSegments.hostname.endsWith(exemption),
  72. );
  73. if (urlIsExemptFromProxy) {
  74. return undefined;
  75. } else {
  76. return proxy;
  77. }
  78. }
  79. /**
  80. * Creates a RequestExecutor to be used with `createTransport`.
  81. */
  82. function createRequestExecutor(
  83. options,
  84. httpModule,
  85. agent,
  86. ) {
  87. const { hostname, pathname, port, protocol, search } = new URL(options.url);
  88. return function makeRequest(request) {
  89. return new Promise((resolve, reject) => {
  90. let body = streamFromBody(request.body);
  91. const headers = { ...options.headers };
  92. if (request.body.length > GZIP_THRESHOLD) {
  93. headers['content-encoding'] = 'gzip';
  94. body = body.pipe(createGzip());
  95. }
  96. const req = httpModule.request(
  97. {
  98. method: 'POST',
  99. agent,
  100. headers,
  101. hostname,
  102. path: `${pathname}${search}`,
  103. port,
  104. protocol,
  105. ca: options.caCerts,
  106. },
  107. res => {
  108. res.on('data', () => {
  109. // Drain socket
  110. });
  111. res.on('end', () => {
  112. // Drain socket
  113. });
  114. res.setEncoding('utf8');
  115. // "Key-value pairs of header names and values. Header names are lower-cased."
  116. // https://nodejs.org/api/http.html#http_message_headers
  117. const retryAfterHeader = _nullishCoalesce(res.headers['retry-after'], () => ( null));
  118. const rateLimitsHeader = _nullishCoalesce(res.headers['x-sentry-rate-limits'], () => ( null));
  119. resolve({
  120. statusCode: res.statusCode,
  121. headers: {
  122. 'retry-after': retryAfterHeader,
  123. 'x-sentry-rate-limits': Array.isArray(rateLimitsHeader) ? rateLimitsHeader[0] : rateLimitsHeader,
  124. },
  125. });
  126. },
  127. );
  128. req.on('error', reject);
  129. body.pipe(req);
  130. });
  131. };
  132. }
  133. export { makeNodeTransport };
  134. //# sourceMappingURL=http.js.map