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: only update content-length header if one was present in spy #25920

Merged
merged 13 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ _Released 03/28/2023 (PENDING)_

- Fixed a compatibility issue so that component test projects can use [Vite](https://vitejs.dev/) version 4.2.0 and greater. Fixes [#26138](https://github.com/cypress-io/cypress/issues/26138).
- Changed the way that Git hashes are loaded so that non-relevant runs are excluded from the Debug page. Fixes [#26058](https://github.com/cypress-io/cypress/issues/26058).
- Fixed an issue where [`cy.intercept()`](https://docs.cypress.io/api/commands/intercept) added an additional `content-length` header to intercepted requests that did not set a `content-length` header on the original request. Fixes [#24407](https://github.com/cypress-io/cypress/issues/24407).
emilyrohrbough marked this conversation as resolved.
Show resolved Hide resolved

**Misc:**

Expand Down
2 changes: 1 addition & 1 deletion packages/net-stubbing/lib/server/middleware/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const InterceptRequest: RequestMiddleware = async function () {
request.req.body = req.body

const mergeChanges = (before: CyHttpMessages.IncomingRequest, after: CyHttpMessages.IncomingRequest) => {
if (before.headers['content-length'] === after.headers['content-length']) {
if ('content-length' in before.headers && before.headers['content-length'] === after.headers['content-length']) {
// user did not purposely override content-length, let's set it
after.headers['content-length'] = String(Buffer.from(after.body).byteLength)
}
Expand Down
80 changes: 80 additions & 0 deletions packages/net-stubbing/test/unit/middleware-request-spec.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

Should a test be added here instead of the unit test (or maybe in addition to)?

Copy link
Contributor Author

@Bourg Bourg Mar 24, 2023

Choose a reason for hiding this comment

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

@mschile as far as I have been able to tell, reproducing this from inside a Cypress test requires that there actually be an API upstream that behaves differently depending on the content-length header - there's no way to test it in isolation that I've found. That was part of why this was so hard for me to identify this as a root cause of an issue I was having in the first place - everything looks untouched from inside the test runner.

The way we originally found this issue was that healthy API endpoints would start replying 400 when spied on by Cypress, and the reason turned out to be that AWS ELB - at least as configured in our case - did not expect the content-length header on a GET request and would reject the requests. For the minimal reproduction linked in the issue, I made a simple HTML document that makes a single fetch call to an Express server that echoes back the request headers. You would basically need to move all that machinery into those tests - the only point at which you can see the issue is from the perspective of the Express server, not Cypress.

I do not believe there is any event you can listen to in Cypress that gets you at the request headers of a spied request at the relevant point of the lifecycle. I agree it would be better if we could integration test this, but I think it may be more trouble than it's worth unless there's a more pure way to see the modified headers without an external server.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks @Bourg, we do start an external server in the driver tests so we should be able to output the content-length header to verify it doesn't get inadvertently added.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm going to take a look into this in a few 😃

Copy link
Contributor

Choose a reason for hiding this comment

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

@mschile I added a simple test in b05eda4. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks good! You could even use the existing dump-headers request if you wanted. Something like:

it('does not calculate content-length on spied request if one does not exist on the initial request (if merging)', { retries: 0 }, function () {
  cy.intercept('/dump-headers', function (req) {
    // modify the intercepted request to trigger a request merge in net_stubbing
    req.headers['foo'] = 'bar'
    // send the modified request and skip any other
    // matching request handlers
    req.continue()
  })

  cy.visit('/dump-headers')

  // ensure the content-length header does not exist
  cy.contains('content-length').should('not.exist')
})

Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { InterceptRequest } from '../../lib/server/middleware/request'
import { state as NetStubbingState } from '../../lib/server/state'

describe('request', () => {
context('InterceptedRequest', () => {
// @see https://github.com/cypress-io/cypress/issues/24407
it('does not set the content-length header if the header was not there to begin with on the original request', async () => {
const socket = {
toDriver: sinon.stub(),
}
const state = NetStubbingState()

const beforeRequestData = {
body: 'stubbed_body',
proxiedUrl: 'https://foobar.com',
url: 'https://foobar.com',
}

const afterRequestData = {
...beforeRequestData,
body: '',
headers: {},
}

// using a ES6 proxy to intercept the promise assignment on pendingEventHandlers.
// this way, we can resolve the event immediately
const pendingEventProxy = new Proxy(state.pendingEventHandlers, {
get (target, prop, receiver) {
// @ts-expect-error
return Reflect.get(...arguments)
},
set (obj, prop, value) {
// @ts-expect-error
const setProp = Reflect.set(...arguments)

// invoke the promise function immediately
if (typeof value === 'function') {
value({
changedData: afterRequestData,
stopPropagation: false,
})
}

return setProp
},
})

state.pendingEventHandlers = pendingEventProxy

const request = {
req: {
...beforeRequestData,
headers: {},
matchingRoutes: [
{
id: '1',
hasInterceptor: true,
routeMatcher: {},
},
],
pipe: sinon.stub(),
},
res: {
once: sinon.stub(),
},
socket,
debug: sinon.stub(),
netStubbingState: state,
next: sinon.stub(),
onError: sinon.stub(),
onResponse: sinon.stub(),
}

await InterceptRequest.call(request)
expect(request.req.headers['content-length']).to.be.undefined
})
})
})