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

fix: handle Node 10 style http requests#1233

Merged
kjin merged 4 commits intogoogleapis:masterfrom
two-chairs:fix-got-10
Apr 16, 2020
Merged

fix: handle Node 10 style http requests#1233
kjin merged 4 commits intogoogleapis:masterfrom
two-chairs:fix-got-10

Conversation

@AaronFriel
Copy link
Copy Markdown
Contributor

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:

function makeRequestTrace(
protocol: string,
request: RequestFunction,
api: Tracer
): RequestFunction {
// On Node 8+ we use the following function to patch both request and get.
// Here `request` may also happen to be `get`.
return function requestTrace(
this: never,
url: httpModule.RequestOptions | string | URL,
options?:
| httpModule.RequestOptions
| ((res: httpModule.IncomingMessage) => void),
callback?: (res: httpModule.IncomingMessage) => void
): ClientRequest {
// These are error conditions; defer to http.request and don't trace.
if (!url || (typeof url === 'object' && typeof options === 'object')) {
return request.apply(this, arguments);
}

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.

  1. Object.assign does not work for URL properties because URL properties are not enumerable.

let urlString;
if (typeof url === 'string') {
// save the value of uri so we don't have to reconstruct it later
urlString = url;
url = urlParse(url);
}
if (typeof options === 'function') {
callback = options;
options = url;
} else {
options = Object.assign({}, url, options);
}

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.

  1. The wrapped request is called with the deficient object

const req = request(options, res => {

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.

@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Apr 10, 2020
@AaronFriel
Copy link
Copy Markdown
Contributor Author

@kjin I believe this will unblock the got 10.x upgrade.

@AaronFriel
Copy link
Copy Markdown
Contributor Author

CC @bcoe, if this could be reviewed and released it'd be great not to rely on a fork!

@AaronFriel AaronFriel mentioned this pull request Apr 13, 2020
@kjin kjin self-requested a review April 13, 2020 17:47
@bcoe
Copy link
Copy Markdown
Contributor

bcoe commented Apr 13, 2020

@AaronFriel thanks for the patch 🙏 I'm not the most well versed in this library, and it would be helpful if we added a few tests.

As it stands today, you can't use the currently published version of @gogole-cloud/trace-agent with got?

Comment thread src/plugins/plugin-http.ts
Comment thread src/plugins/plugin-http.ts Outdated
AaronFriel and others added 2 commits April 14, 2020 10:33
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.
@AaronFriel
Copy link
Copy Markdown
Contributor Author

Rebased and resolved conflicts.

@bcoe
Copy link
Copy Markdown
Contributor

bcoe commented Apr 14, 2020

@kjin it sounds like Kelvin has good technical feedback 😄

Might I still press you for a test, otherwise it looks like this is coming along great.

@AaronFriel
Copy link
Copy Markdown
Contributor Author

@bcoe that is correct, got 10 throws an exception in the http plugin source code. And I'm not sure who Kelvin is.

I wrote three tests to be very explicit about how the request function is called, two of which failed (string and URL class instance) as expected, rebasing my branch on that failing test commit resulted in the tests passing. The "parsed URL" test never failed because Object.assign behaves as expected when the URL-ish object is passed in, but should help prevent bitrot.

Copy link
Copy Markdown
Contributor

@kjin kjin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! (I'm Kelvin, by the way)

@JustinBeckwith JustinBeckwith added kokoro:force-run Add this label to force Kokoro to re-run the tests. automerge Merge the pull request once unit tests and other checks pass. labels Apr 15, 2020
@yoshi-kokoro yoshi-kokoro removed the kokoro:force-run Add this label to force Kokoro to re-run the tests. label Apr 15, 2020
@gcf-merge-on-green
Copy link
Copy Markdown
Contributor

Your PR has attempted to merge for 3 hours. Please check that all required checks have passed, you have an automerge label, and that all your reviewers have approved the PR

1 similar comment
@gcf-merge-on-green
Copy link
Copy Markdown
Contributor

Your PR has attempted to merge for 3 hours. Please check that all required checks have passed, you have an automerge label, and that all your reviewers have approved the PR

@gcf-merge-on-green
Copy link
Copy Markdown
Contributor

Merge-on-green attempted to merge your PR for 6 hours, but it was not mergeable because either one of your required status checks failed, or one of your required reviews was not approved. Learn more about your required status checks here: https://help.github.com/en/github/administering-a-repository/enabling-required-status-checks. You can remove and reapply the label to re-run the bot.

@kjin kjin merged commit 511b21c into googleapis:master Apr 16, 2020
@bcoe
Copy link
Copy Markdown
Contributor

bcoe commented Apr 16, 2020

@AaronFriel awesome, thank you for adding the regression tests, and for the contribution.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

automerge Merge the pull request once unit tests and other checks pass. cla: yes This human has signed the Contributor License Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants