Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix .matchHeader() with allowUnmocked #1480

Merged
merged 13 commits into from Apr 15, 2019
Merged

fix: Fix .matchHeader() with allowUnmocked #1480

merged 13 commits into from Apr 15, 2019

Conversation

ww-daniel-mora
Copy link
Contributor

@ww-daniel-mora ww-daniel-mora commented Mar 23, 2019

Currently, the header filter fails if there is not a getHeader method present on the options object. This sometimes appears to be the case. This is a simple node program written that should work as a repo

import httpProxy from 'http-proxy';
import nock from 'nock';

nock('http://example.com')
  .matchHeader('any-string', val => true)
  .get('/hello'),
  reply(200, 'goodbye');

let proxyServer = httpProxy.createProxyServer({
	changeOrigin: true,
	selfHandleResponse: false,
	target: 'http://example.com',
  });
proxyServer.listen(8080);
curl -X GET http://localhost:8080/hello

This change also makes the two checkHeader methods consistent. Where one previously respected functions and the other did not.

@paulmelnikow
Copy link
Member

Hi! Thanks for opening this.

What's the expected behavior for the repro program?

options.getHeader sure is a bit odd. It doesn't seem to be part of the defined options for an http request, which makes me think it's a nock internal thing…

I just played with matchHeader, and couldn't confirm this bug, though I found another bug, and some unexpected behavior. I will open a PR with the new tests.

@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow Thank you for your speedy reply! I think this is the getHeader method that nock is calling https://github.com/nodejs/node/blob/169b7f1f3b3751289f24678930e6a5731464ebc9/lib/_http_outgoing.js#L495. The above example basically does the following:

  1. Creates a proxy server that will forward requests from localhost:8080 to example.com
  2. Creates a nock interceptor that will catch requests sent to example.com/hello and reply with 200 'goodbye'
  3. Lastly, there is curl call to the local proxy server which should then attempt to forward the request to example.com/hello which should then be intercepted by nock. This actually works unless you try to apply a header filter.

The outgoing request that the http-proxy code creates does not have a getHeader method defined on it. In the version of nock that I am using, if you have a header filter and there is no getHeader method then nock will not intercept any calls. Basically, everything comes back as not matching the header filter. This change basically attempts to coerce the object to conform to nocks expectations.

@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow let me do a little more digging and see if I can pin down what is happening a bit better.

@paulmelnikow
Copy link
Member

When I run the test I see "goodbye" in the terminal. Is that the expected behavior?

@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow yes that is the expected behavior. I did a bit more playing around to make a minimal example and this is a working minimal example.

import httpProxy from 'http-proxy';
import nock from 'nock';

nock('http://example.com', {allowUnmocked: true})
  .matchHeader('any-string', val => true)
  .get('/hello')
  .reply(200, 'goodbye');

let proxyServer = httpProxy.createProxyServer({
	changeOrigin: true,
	selfHandleResponse: false,
	target: 'http://example.com',
  });
proxyServer.listen(8080);

What I have found is that if run without allowUnmocked to be false then nock will hit both of the checkHeader methods that I edit in this PR. If you run with allowUnmocked to be true then nock will actually only hit one of the checkHeader methods. At the time it hits this method getHeader does not exist on the options object.

@ww-daniel-mora
Copy link
Contributor Author

ww-daniel-mora commented Mar 24, 2019

The first hit to checkHeader comes from the newRequest call here

    module.request = function(options, callback) {
      // debug('request options:', options);
      return newRequest(proto, overriddenRequest.bind(module), options, callback);
    };

... and then goes to matchIndependentOfBody. During this invocation getHeader never appears to be defined to the check header call fails.

The second checkHeader gets triggered from this call to req.end

  req.end = function(buffer, encoding, callback) {
    debug('req.end');
    if (!req.aborted && !ended) {
      req.write(buffer, encoding, function () {
        if (typeof callback === 'function') {
          callback();
        }

... and then goes to match. At this point when we reach the checkHeader logic getHeader is defined on options.

When allowUnmocked is true the second call is never triggered and the first call appears broken. I think this shows two bugs but you can better diagnose. Two things are surprising though.

  1. The call to matchIndependantOfBody does not work (at least with some combination of tools/versions). This code change would fix this bug.
  2. The call to match (which given the names I assume would include body matches) does not appear to be called when allowUnmocked is set to true. This may be intentional, but, it is at least surprising.

@paulmelnikow
Copy link
Member

I'm getting "goodbye" without any code changes. I guess I'm confused about what the bug is.

The matchHeader function always returns true, so it should match every request. The "goodbye" means the mock is matching. It sounds like the code in nock, confusing it may be, is working as intended.

Could you clarify the expected + observed behavior of the test?

@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow Does that include my recent comments with allowUnmocked?

@paulmelnikow
Copy link
Member

Sorry, I'm not sure what you're asking! 😀 I'd like to understand what the bug is first. Otherwise it's hard to make sense of the fix / implementation change.

@ww-daniel-mora
Copy link
Contributor Author

ww-daniel-mora commented Mar 25, 2019

Let me try to restate and see if that makes it more clear.

  1. Create a node server with the following code (I used typescript but you can translate to JS if you like)
    import httpProxy from 'http-proxy';
    import nock from 'nock';
    
    nock('http://example.com', {allowUnmocked: true})
      .matchHeader('any-string', val => true)
      .get('/hello')
      .reply(200, 'goodbye');
    
    let proxyServer = httpProxy.createProxyServer({
      	    changeOrigin: true,
        selfHandleResponse: false,
        target: 'http://example.com',
      });
    proxyServer.listen(8080);
  2. Make a curl call
     curl -X GET http://localhost:8080/hello
  3. EXPECTED
    Nock intercpets the call and you see 'goodbye'
    ACTUAL
    Nock does not intercept the call and you see the example.com page

This is different from the original example because I added the allowUnmocked: true setting.

@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow I've added a test as well. If you just add the test I added you will also see the bug.

@paulmelnikow
Copy link
Member

Gooooot it! The test in the top post actually passes and is working as expected; it's the one with { allowUnmocked: true } that is failing. Thanks for adding the test. The bug is much easier to reason about in that form 😀

I am not convinced that options is ever used interchangeably with OutgoingMessage so I don't think it's quite parallel with https://github.com/nodejs/node/blob/169b7f1f3b3751289f24678930e6a5731464ebc9/lib/_http_outgoing.js#L495.

I hate to spread more layers on top of existing cruft. What would you think about removing these lines:

options.getHeader = function(name) {
return getHeader(req, name)
}

And proceeding from there, to get the matching working again?

return (
options.getHeader &&
common.matchStringOrRegexp(options.getHeader(header.name), header.value)
if (_.isFunction(header.value)) {
Copy link
Member

Choose a reason for hiding this comment

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

This looks like a bug. When this condition is true, the function returns without executing common.matchStringOrRegexp().

lib/intercept.js Outdated
@@ -388,7 +388,17 @@ function activate() {
options = urlParse(options.toString())
}
options.proto = proto

Copy link
Member

Choose a reason for hiding this comment

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

As I mentioned I'm not sure we need to insert getHeader into options at all. I've found no evidence that this is for parity with an existing API or compatibility with some existing library.

Could you remove this code, and update the code in the places where options.getHeader is called, instead? It's fine to delegate the repeated code to a helper function, though let's not unnecessarily stick that helper function into options.

@@ -95,6 +95,21 @@ test('match headers with function', async t => {
scope.done()
})

test('match headers with function and allowUnmocked', async t => {
Copy link
Member

Choose a reason for hiding this comment

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

If you merge master into this file, there are some new tests which are a bit clearer and more specific. If you'd like, you could update this to use one of those.

ww-daniel-mora and others added 5 commits March 26, 2019 20:12
Currently, the header filter fails if there is not a `getHeader` method present on the options object. This sometimes appears to be the case. This is a simple node program written that should work as a repo
```typescript
import httpProxy from 'http-proxy';
import nock from 'nock';

let proxyServer = httpProxy.createProxyServer({
	changeOrigin: true,
	selfHandleResponse: false,
	target: 'http://example.com',
  });
proxyServer.listen(8080);
nock('http://example.com')
  .matchHeader('any-string', val => true)
  .get('hello'),
  reply(200, 'goodbye')
```
```bash
curl -X GET http://localhost:8080/hello
```

This change also make the two checkHeader methods consistent. Where one previously respected functions and the other did not.
Fix lint errors.
…he incomming object

* Reverted changes to intercept.js
* Moved shared header logic to a helper function in interceptor.js
* Added test to cover header checks with allowUnmocked set to true
@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow I believe these changes meet your criteria. I suspect there is still a bug on body filters with allowUnmocked set to true, and I am unclear on why nock checks things differently depending on this flag, but, that is an investigation for another day.

Copy link
Member

@paulmelnikow paulmelnikow left a comment

Choose a reason for hiding this comment

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

This is looking good.

t.equal(statusCode, 200)
t.equal(body, 'Hello World!')
scope.done()
})
Copy link
Member

Choose a reason for hiding this comment

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

👍

t.equal(statusCode, 200)
t.equal(body, 'Hello World!')
scope.done()
})
Copy link
Member

Choose a reason for hiding this comment

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

👍

common.matchStringOrRegexp(options.getHeader(header.name), header.value)
)
const checkHeaders = function(filter) {
return filterMatches(filter, options)
Copy link
Member

Choose a reason for hiding this comment

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

This looks great!

@@ -82,7 +82,7 @@ test('match headers on number with regexp', async t => {
})

test('match header on scope with function: gets the expected argument', async t => {
t.plan(3)
t.plan(4) // The check in matchHeader should run twice
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, is there a reason it should? It seems confusing for the check to run more than once per request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I ran the debug and what is happening is it is called once from matchIndependentOfBody and once from match. Previously the two functions used different header matching logic. Now they use the same logic so line 92 will be called twice.

Copy link
Member

Choose a reason for hiding this comment

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

Okay. I can dig into it a bit more and see if I can get all the tests passing with the function only being called once. Ideally I'd like to fix that before merging because I don't think there's a good reason to evaluate it twice and it seems confusing for the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So matchIndependantOfBody is used here https://github.com/nock/nock/blob/beta/lib/intercept.js#L398 to detect if we should let the request through. But if all the check pass on matchIndependantOfBody we don't really do anything with it because we wait for the body when we execute match. I think what we would basically need to do to get down to 3 executions rather than 4 is to drop the whole matchIndependantOfBody line of execution and only executing matching once. Inside the match method. This would simplify nock but it looked like a lot more than I was really willing to bite off.

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha. Thank you so much for looking into it this far! I can pick it up from here.

Since the headers aren't part of the body, it seems like the check belongs in matchIndependentOfBody, though maybe doing it just once inside of match will be a more reasonable incremental change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can't only check in the first because there might be I rejecting body filter. So you could only check in the second method when you have all the data. As an alternative, you could store the results of matchIndependant... on the object and then use the result in the match call rather than rerunning all the checks.

As a contributor I do find the separate matches confusing so executing the checks once would read better, but, it was so odd I assumed it was done for good reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If possible you could also consider merging this change in as is and addressing the extra call separately. We would trade the duplicate call for working header matching.

@paulmelnikow paulmelnikow changed the title Make header filters more robust Fix .matchHeader() with allowUnmocked Mar 27, 2019
@paulmelnikow paulmelnikow changed the title Fix .matchHeader() with allowUnmocked fix: Fix .matchHeader() with allowUnmocked Apr 12, 2019
@paulmelnikow
Copy link
Member

@ww-daniel-mora Hi! Thanks for your patience! I've moved the header check out of matchIndependentOfBody() which I've renamed to matchAddress(), and now there's a single set of header checks happening in match().

Before I ship it, would you like to have another look and/or take it for a spin?

If you don't have time, no worries! I know you've worked hard on this.

Copy link
Member

@paulmelnikow paulmelnikow left a comment

Choose a reason for hiding this comment

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

Going to give OP a chance to look at this if he wants!

@ww-daniel-mora
Copy link
Contributor Author

@paulmelnikow looks good. Thanks for your help!

@paulmelnikow
Copy link
Member

Coverage % is down in interceptor.js, though it's because code was removed. There are no new uncovered lines.

@paulmelnikow paulmelnikow merged commit d6667f0 into nock:beta Apr 15, 2019
@nockbot
Copy link
Collaborator

nockbot commented Apr 15, 2019

🎉 This PR is included in version 11.0.0-beta.9 🎉

The release is available on:

Your semantic-release bot 📦🚀

@nockbot
Copy link
Collaborator

nockbot commented Aug 12, 2019

🎉 This PR is included in version 11.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

gr2m pushed a commit that referenced this pull request Sep 4, 2019
- Move header checks into `match()`.
- Rename internal method `matchIndependentOfBody()` to `matchAddress()`
gr2m pushed a commit that referenced this pull request Sep 4, 2019
- Move header checks into `match()`.
- Rename internal method `matchIndependentOfBody()` to `matchAddress()`
gr2m pushed a commit that referenced this pull request Sep 5, 2019
- Move header checks into `match()`.
- Rename internal method `matchIndependentOfBody()` to `matchAddress()`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants