Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit 511b21c

Browse files
authored
fix: handle Node 10 style http requests (#1233)
* fix: handle Node 10 style http requests In Node 10 and above and libraries which use the `request(url, options, cb?)` overload of the request function such as `got` 10.x, request tracing fails. Environment details ------------------- - OS: any - Node.js version: 10.x - npm version: 6.x - `@google-cloud/trace-agent` version: 4.5.2 Steps to reproduce ------------------ 1. Use `got` version `10.x` or manually invoke a request with a separate URL and options argument 2. In a root span, try to do a `got.post` or any other request as above and log the `x-cloud-trace-header` in the server receiving the request 3. Observe that the request isn't traced and the trace header is absent Bug --- The bug is in three parts: 1. If condition filters out valid requests: https://github.com/googleapis/cloud-trace-nodejs/blob/ac7e886c178ca9c34502e9baa9eb190d23104347/src/plugins/plugin-http.ts#L95-L113 When got 10 makes a request, it sends three arguments: url (as an object), options (as an object), and callback (as a function). This if condition blocks tracing all requests from got 10. 2. `Object.assign` does not work for URL properties because URL properties are not enumerable. https://github.com/googleapis/cloud-trace-nodejs/blob/ac7e886c178ca9c34502e9baa9eb190d23104347/src/plugins/plugin-http.ts#L114-L126 If the if condition is changed above so that we can fall through to line 125, we hit a second bug. This last `options = Object.assign(...)` does not work because URL properties are not enumerable. The result is that properties such as "hostname", "host", etc. are not copied into the options. 3. The wrapped request is called with the deficient object https://github.com/googleapis/cloud-trace-nodejs/blob/ac7e886c178ca9c34502e9baa9eb190d23104347/src/plugins/plugin-http.ts#L171 Given the incorrect Object.assign, even if we repair that defect, we end up sending this rewritten object to the destination and we destroy any non-enumerable symbols or other properties assigned to the url or options object we were passed. Fix --- Looking at the source for CreateRequest, the function called by `http.request`, we see that the "input" argument is always replaced by an object, as opposed to a URL instance, before being `ObjectAssigned` https://github.com/nodejs/node/blob/dccdc51788bd5337f9fd80441ef52932383a2441/lib/_http_client.js#L88-L123 We can do the same. Our needs are a bit different, but we can ensure that before the object assign the url value is always converted to a `RequestOptions` object. * Update src/plugins/plugin-http.ts * fix: let http.request throw error on undefined/falsey URL * chore: failing tests for Node 10.x style http.request calls
1 parent e357efc commit 511b21c

2 files changed

Lines changed: 83 additions & 9 deletions

File tree

src/plugins/plugin-http.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ import {Agent, ClientRequest, ClientRequestArgs, request} from 'http';
1717
import * as httpsModule from 'https';
1818
import * as semver from 'semver';
1919
import * as shimmer from 'shimmer';
20-
// eslint-disable-next-line node/no-deprecated-api
21-
import {URL, parse as urlParse} from 'url';
20+
import {URL, UrlWithStringQuery} from 'url';
2221

2322
import {Plugin, Tracer} from '../plugin-types';
2423

@@ -95,6 +94,33 @@ function isTraceAgentRequest(options: httpModule.RequestOptions, api: Tracer) {
9594
);
9695
}
9796

97+
/**
98+
* Transform a url to a request options.
99+
*
100+
* From: https://github.com/nodejs/node/blob/v12.16.2/lib/internal/url.js#L1271-L1290
101+
*/
102+
function urlToOptions(url: URL): httpModule.RequestOptions {
103+
const options: httpModule.RequestOptions | UrlWithStringQuery = {
104+
protocol: url.protocol,
105+
hostname:
106+
typeof url.hostname === 'string' && url.hostname.startsWith('[')
107+
? url.hostname.slice(1, -1)
108+
: url.hostname,
109+
hash: url.hash,
110+
search: url.search,
111+
pathname: url.pathname,
112+
path: `${url.pathname || ''}${url.search || ''}`,
113+
href: url.href,
114+
};
115+
if (url.port !== '') {
116+
options.port = Number(url.port);
117+
}
118+
if (url.username || url.password) {
119+
options.auth = `${url.username}:${url.password}`;
120+
}
121+
return options;
122+
}
123+
98124
function makeRequestTrace(
99125
protocol: string,
100126
request: RequestFunction,
@@ -110,17 +136,17 @@ function makeRequestTrace(
110136
| ((res: httpModule.IncomingMessage) => void),
111137
callback?: (res: httpModule.IncomingMessage) => void
112138
): ClientRequest {
113-
// These are error conditions; defer to http.request and don't trace.
114-
if (!url || (typeof url === 'object' && typeof options === 'object')) {
139+
let urlString: string | undefined;
140+
if (!url) {
141+
// These are error conditions; defer to http.request and don't trace.
115142
// eslint-disable-next-line prefer-rest-params
116143
return request.apply(this, arguments);
117-
}
118-
119-
let urlString;
120-
if (typeof url === 'string') {
144+
} else if (typeof url === 'string') {
121145
// save the value of uri so we don't have to reconstruct it later
122146
urlString = url;
123-
url = urlParse(url);
147+
url = urlToOptions(new URL(url));
148+
} else if (url instanceof URL) {
149+
url = urlToOptions(url);
124150
}
125151
if (typeof options === 'function') {
126152
callback = options;

test/plugins/test-trace-http.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,54 @@ for (const nodule of Object.keys(servers) as Array<keyof typeof servers>) {
141141
// http(url, options, cb) is not a recognized signature in Node 8.
142142
versions: '>=10.x',
143143
},
144+
{
145+
description: 'calling http.get with a string url and options',
146+
fn: async () => {
147+
const waitForResponse = new WaitForResponse();
148+
http.get(
149+
`${nodule}://localhost:${port}`,
150+
{
151+
rejectUnauthorized: false,
152+
},
153+
waitForResponse.handleResponse
154+
);
155+
await waitForResponse.done;
156+
},
157+
// http(url, options, cb) is not a recognized signature in Node 8.
158+
versions: '>=10.x',
159+
},
160+
{
161+
description: 'calling http.get with a URL object and options',
162+
fn: async () => {
163+
const waitForResponse = new WaitForResponse();
164+
http.get(
165+
new URL(`${nodule}://localhost:${port}`),
166+
{
167+
rejectUnauthorized: false,
168+
},
169+
waitForResponse.handleResponse
170+
);
171+
await waitForResponse.done;
172+
},
173+
// http(url, options, cb) is not a recognized signature in Node 8.
174+
versions: '>=10.x',
175+
},
176+
{
177+
description: 'calling http.get with a parsed URL and options',
178+
fn: async () => {
179+
const waitForResponse = new WaitForResponse();
180+
http.get(
181+
{hostname: 'localhost', port: port, protocol: `${nodule}:`},
182+
{
183+
rejectUnauthorized: false,
184+
},
185+
waitForResponse.handleResponse
186+
);
187+
await waitForResponse.done;
188+
},
189+
// http(url, options, cb) is not a recognized signature in Node 8.
190+
versions: '>=10.x',
191+
},
144192
{
145193
description: 'calling http.get and using return value',
146194
fn: async () => {

0 commit comments

Comments
 (0)