Skip to content

Commit 88b96f6

Browse files
fix: Add protocol detection for get/request calls without explict protocol (#3950)
You can call `get`/`request` on `http`/`https` without providing an explicit protocol in the request options. In this case the protocol is automatically set based on which module you made the call to. Previously calls like this would result in breadcrumbs and traces without a protocol in the URL creating downstream issues for consumers of Breadcrumbs as the missing protocol results in URL throwing parsing errors. We now attempt to get the protocol by either extracting it from the `agent` passed as part of the request options, or by extracting it from the `globalAgent` on which ever http module is pass in `this`.
1 parent 295d263 commit 88b96f6

File tree

3 files changed

+99
-1
lines changed

3 files changed

+99
-1
lines changed

packages/node/src/integrations/http.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ function _createWrappedRequestMethodFactory(
9494
// eslint-disable-next-line @typescript-eslint/no-this-alias
9595
const httpModule = this;
9696

97-
const requestArgs = normalizeRequestArgs(args);
97+
const requestArgs = normalizeRequestArgs(this, args);
9898
const requestOptions = requestArgs[0];
9999
const requestUrl = extractUrl(requestOptions);
100100

packages/node/src/integrations/utils/http.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getCurrentHub } from '@sentry/core';
22
import * as http from 'http';
3+
import * as https from 'https';
34
import { URL } from 'url';
45

56
/**
@@ -122,6 +123,7 @@ export function urlToOptions(url: URL): RequestOptions {
122123
* @returns Equivalent args of the form [ RequestOptions ] or [ RequestOptions, RequestCallback ].
123124
*/
124125
export function normalizeRequestArgs(
126+
httpModule: typeof http | typeof https,
125127
requestArgs: RequestMethodArgs,
126128
): [RequestOptions] | [RequestOptions, RequestCallback] {
127129
let callback, requestOptions;
@@ -145,6 +147,17 @@ export function normalizeRequestArgs(
145147
requestOptions = { ...requestOptions, ...requestArgs[1] };
146148
}
147149

150+
// Figure out the protocol if it's currently missing
151+
if (requestOptions.protocol === undefined) {
152+
// Worst case we end up populating protocol with undefined, which it already is
153+
/* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
154+
requestOptions.protocol =
155+
(requestOptions.agent as any)?.protocol ||
156+
(requestOptions._defaultAgent as any)?.protocol ||
157+
(httpModule.globalAgent as any)?.protocol;
158+
/* eslint-enable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any */
159+
}
160+
148161
// return args in standardized form
149162
if (callback) {
150163
return [requestOptions, callback];

packages/node/test/integrations/http.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import * as sentryCore from '@sentry/core';
22
import { Hub } from '@sentry/hub';
33
import * as hubModule from '@sentry/hub';
44
import { addExtensionMethods, Span, TRACEPARENT_REGEXP, Transaction } from '@sentry/tracing';
5+
import { parseSemver } from '@sentry/utils';
56
import * as http from 'http';
7+
import * as https from 'https';
68
import * as nock from 'nock';
79

10+
import { Breadcrumb } from '../../src';
811
import { NodeClient } from '../../src/client';
912
import { Http as HttpIntegration } from '../../src/integrations/http';
1013

14+
const NODE_VERSION = parseSemver(process.versions.node);
15+
1116
describe('tracing', () => {
1217
function createTransactionOnScope() {
1318
const hub = new Hub(
@@ -91,3 +96,83 @@ describe('tracing', () => {
9196
expect(sentryTraceHeader).not.toBeDefined();
9297
});
9398
});
99+
100+
describe('default protocols', () => {
101+
function captureBreadcrumb(key: string): Promise<Breadcrumb> {
102+
const hub = new Hub();
103+
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
104+
105+
let resolve: (value: Breadcrumb | PromiseLike<Breadcrumb>) => void;
106+
const p = new Promise<Breadcrumb>(r => {
107+
resolve = r;
108+
});
109+
hub.bindClient(
110+
new NodeClient({
111+
dsn: 'https://[email protected]/12312012',
112+
integrations: [new HttpIntegration({ breadcrumbs: true })],
113+
beforeBreadcrumb: (b: Breadcrumb) => {
114+
if ((b.data?.url as string).includes(key)) {
115+
resolve(b);
116+
}
117+
return b;
118+
},
119+
}),
120+
);
121+
122+
return p;
123+
}
124+
125+
it('http module', async () => {
126+
const key = 'catrunners';
127+
const p = captureBreadcrumb(key);
128+
129+
nock(`http://${key}.ingest.sentry.io`)
130+
.get('/api/123122332/store/')
131+
.reply(200);
132+
133+
http.get({
134+
host: `${key}.ingest.sentry.io`,
135+
path: '/api/123122332/store/',
136+
});
137+
138+
const b = await p;
139+
expect(b.data?.url).toEqual(expect.stringContaining('http://'));
140+
});
141+
142+
it('https module', async () => {
143+
const key = 'catcatchers';
144+
const p = captureBreadcrumb(key);
145+
146+
let nockProtocol = 'https:';
147+
// NOTE: Prior to Node 9, `https` used internals of `http` module, so
148+
// the integration doesn't patch the `https` module. However this then
149+
// causes issues with nock, because nock will patch the `https` module
150+
// regardless (if it asked to mock a https:// url), preventing the
151+
// request from reaching the integrations patch of the `http` module.
152+
// The result is test failures in Node v8 and lower.
153+
//
154+
// We can work around this by telling giving nock a http:// url, so it
155+
// only patches the `http` module, then Nodes usage of the `http` module
156+
// in the `https` module results in both nock's and the integrations
157+
// patch being called. All this relies on nock not properly checking
158+
// the agent passed to `http.get` / `http.request`, thus resulting in it
159+
// intercepting a https:// request with http:// mock. It's a safe bet
160+
// because the latest versions of nock no longer support Node v8 and lower,
161+
// so won't bother dealing with this old Node edge case.
162+
if (NODE_VERSION.major && NODE_VERSION.major < 9) {
163+
nockProtocol = 'http:';
164+
}
165+
nock(`${nockProtocol}://${key}.ingest.sentry.io`)
166+
.get('/api/123122332/store/')
167+
.reply(200);
168+
169+
https.get({
170+
host: `${key}.ingest.sentry.io`,
171+
path: '/api/123122332/store/',
172+
timeout: 300,
173+
});
174+
175+
const b = await p;
176+
expect(b.data?.url).toEqual(expect.stringContaining('https://'));
177+
});
178+
});

0 commit comments

Comments
 (0)