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

Using cy.intercept to spy adds a "Content-Length: 0" request header #24407

Closed
Bourg opened this issue Oct 26, 2022 · 2 comments · Fixed by #25920
Closed

Using cy.intercept to spy adds a "Content-Length: 0" request header #24407

Bourg opened this issue Oct 26, 2022 · 2 comments · Fixed by #25920

Comments

@Bourg
Copy link
Contributor

Bourg commented Oct 26, 2022

Current behavior

If an API route is spied on using cy.intercept (e.g. cy.intercept('/api') with no stub), an additional Content-Length: 0 header will be sent to the server, despite the original request from the application code not including a Content-Length header at all.

Without cy.intercept, the headers are left unchanged and there is no bug.

When using cy.intercept to stub (e.g. cy.intercept('/api', {})), there is no bug.

Desired behavior

The headers sent to the server should be identical to the headers in the original request regardless of whether cy.intercept is used to spy on the request.

Test code to reproduce

https://github.com/Bourg/cypress-bug-content-length

Testing this requires a server process since the problematic request headers only show up after the request has left the browser. The repo has a minimal express server and instructions on how to boot it.

Cypress Version

10.11.0

Node version

v18.12.0

Operating System

macOS 12.5.1

Debug Logs

% DEBUG=cypress:net-stubbing* npm run cypress:run

> cypress-header-bug-minimal@1.0.0 cypress:run
> cypress run


====================================================================================================

  (Run Starting)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Cypress:        10.11.0                                                                        │
  │ Browser:        Electron 106 (headless)                                                        │
  │ Node Version:   v18.12.0 (/Users/abourgerie/.nvm/versions/node/v18.12.0/bin/node)              │
  │ Specs:          1 found (interceptBug.cy.js)                                                   │
  │ Searched:       cypress/e2e/**/*.cy.{js,jsx,ts,tsx}                                            │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


────────────────────────────────────────────────────────────────────────────────────────────────────

  Running:  interceptBug.cy.js                                                              (1 of 1)


  cy.intercept should not change headers
  cypress:net-stubbing:server:intercept-response InterceptResponse { req: { url: '/' }, request: undefined } +0ms
  cypress:net-stubbing:server:intercept-response InterceptResponse { req: { url: '/api' }, request: undefined } +36ms
    ✓ receives no content-length header without cy.intercept (152ms)
  cypress:net-stubbing:server:driver-events received driver event { eventName: 'route:added', args: [ 'route:added', { routeId: '1666797462330-14', hasInterceptor: false, routeMatcher: [Object] } ] } +0ms
  cypress:net-stubbing:server:intercept-response InterceptResponse { req: { url: '/' }, request: undefined } +94ms
  cypress:net-stubbing:server:intercept-request intercepting request { requestId: 'interceptedRequest6', req: { url: '/api' } } +0ms
  cypress:net-stubbing:server:util sending event to driver { eventName: 'before:request', data: { eventId: 'event7', subscription: { eventName: 'before:request', await: false, routeId: '1666797462330-14' }, browserRequestId: '76357.67', requestId: 'interceptedRequest6', data: { headers: [Object], url: 'http://localhost:3000/api', method: 'GET', httpVersion: '1.1', body: '' } } } +0ms
  cypress:net-stubbing:server:intercept-response InterceptResponse { req: { url: 'http://localhost:3000/api' }, request: InterceptedRequest { subscriptionsByRoute: [ [Object] ], includeBodyInAfterResponse: false, responseSent: false, onResponse: [Function (anonymous)], id: 'interceptedRequest6', req: IncomingMessage { _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, socket: [Socket], httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, rawHeaders: [Array], rawTrailers: [], aborted: false, upgrade: false, url: 'http://localhost:3000/api', method: 'GET', statusCode: null, statusMessage: null, client: [Socket], _consuming: false, _dumped: false, proxiedUrl: 'http://localhost:3000/api', next: [Function: next], baseUrl: '', originalUrl: '/api', _parsedUrl: [Url], params: [Object], query: {}, res: [ServerResponse], secret: undefined, cookies: [Object: null prototype] {}, signedCookies: [Object: null prototype] {}, route: [Route], isAUTFrame: false, browserPreRequest: [Object], requestId: 'interceptedRequest6', body: '', [Symbol(kCapture)]: false, [Symbol(kHeaders)]: [Object], [Symbol(kHeadersCount)]: 28, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0, [Symbol(RequestTimeout)]: undefined }, res: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Socket], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [IncomingMessage], _sent100: false, _expect_continue: false, locals: [Object: null prototype] {}, flush: [Function: flush], write: [Function: write], end: [Function: end], on: [Function: on], writeHead: [Function: writeHead], [Symbol(kCapture)]: false, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: null }, continueRequest: [Function: next], onError: [Function: onError], _onResponse: [Function: onResponse], matchingRoutes: [ [Object] ], state: { requests: [Object], routes: [Array], pendingEventHandlers: {}, reset: [Function: reset] }, socket: SocketE2E { ensureProp: [Function: ensureProp], supportsRunEvents: true, experimentalSessionAndOrigin: false, ended: false, localBus: [EventEmitter], onTestFileChange: [Function: bound ], testFilePath: 'cypress/e2e/interceptBug.cy.js', onStudioTestFileChange: [Function: bound onStudioTestFileChange], removeOnStudioTestFileChange: [Function: bound removeOnStudioTestFileChange], _io: [SocketIOServer], _sendResetBrowserTabsForNextTestMessage: [AsyncFunction (anonymous)], _sendResetBrowserStateMessage: [AsyncFunction (anonymous)], _sendFocusBrowserMessage: [AsyncFunction (anonymous)], _isRunnerSocketConnected: [Function (anonymous)] }, lastEvent: 'before:request' } } +24ms
  cypress:net-stubbing:server:util sending event to driver { eventName: 'response:callback', data: { eventId: 'event9', subscription: { eventName: 'response:callback', await: false, routeId: '1666797462330-14' }, browserRequestId: '76357.67', requestId: 'interceptedRequest6', data: { headers: [Object], url: 'http://localhost:3000/api', method: null, httpVersion: '1.1', statusCode: 200, statusMessage: 'OK', body: '{"connection":"keep-alive","host":"localhost:3000","proxy-connection":"keep-alive","sec-ch-ua":"\\"Not;A=Brand\\";v=\\"99\\", \\"Chromium\\";v=\\"106\\"","sec-ch-ua-mobile":"?0","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Cypress/10.11.0 Chrome/106.0.5249.51 Electron/21.0.0 Safari/537.36","sec-ch-ua-platform":"\\"macOS\\"","accept":"*/*","sec-fetch-site":"same-origin","sec-fetch-mode":"cors","sec-fetch-dest":"empty","referer":"http://localhost:3000/","accept-encoding":"gzip","accept-language":"en-US","if-none-match":"W/\\"229-GpjgY70/3CX5a8rUCAoc+HhB6s0\\"","content-length":"0"}' } } } +7ms
  cypress:net-stubbing:server:intercept-request request/response finished, cleaning up { requestId: 'interceptedRequest6' } +14ms
  cypress:net-stubbing:server:util sending event to driver { eventName: 'after:response', data: { eventId: 'event10', subscription: { eventName: 'after:response', await: false, routeId: '1666797462330-14' }, browserRequestId: '76357.67', requestId: 'interceptedRequest6', data: {} } } +6ms
    1) receives no content-length header when cy.intercept is used to spy
  cypress:net-stubbing:server:driver-events received driver event { eventName: 'route:added', args: [ 'route:added', { routeId: '1666797466567-22', hasInterceptor: false, routeMatcher: [Object], staticResponse: [Object] } ] } +4s
  cypress:net-stubbing:server:intercept-response InterceptResponse { req: { url: '/' }, request: undefined } +4s
  cypress:net-stubbing:server:intercept-request intercepting request { requestId: 'interceptedRequest13', req: { url: '/api' } } +4s
  cypress:net-stubbing:server:util sending event to driver { eventName: 'before:request', data: { eventId: 'event14', subscription: { eventName: 'before:request', await: false, routeId: '1666797466567-22' }, browserRequestId: '76357.69', requestId: 'interceptedRequest13', data: { headers: [Object], url: 'http://localhost:3000/api', method: 'GET', httpVersion: '1.1', body: '' } } } +4s
  cypress:net-stubbing:server:intercept-response InterceptResponse { req: { url: 'http://localhost:3000/api' }, request: InterceptedRequest { subscriptionsByRoute: [ [Object] ], includeBodyInAfterResponse: false, responseSent: true, onResponse: [Function (anonymous)], id: 'interceptedRequest13', req: IncomingMessage { _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, socket: [Socket], httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, rawHeaders: [Array], rawTrailers: [], aborted: false, upgrade: false, url: 'http://localhost:3000/api', method: 'GET', statusCode: null, statusMessage: null, client: [Socket], _consuming: false, _dumped: false, proxiedUrl: 'http://localhost:3000/api', next: [Function: next], baseUrl: '', originalUrl: '/api', _parsedUrl: [Url], params: [Object], query: {}, res: [ServerResponse], secret: undefined, cookies: [Object: null prototype] {}, signedCookies: [Object: null prototype] {}, route: [Route], isAUTFrame: false, browserPreRequest: [Object], requestId: 'interceptedRequest13', body: '', [Symbol(kCapture)]: false, [Symbol(kHeaders)]: [Object], [Symbol(kHeadersCount)]: 28, [Symbol(kTrailers)]: null, [Symbol(kTrailersCount)]: 0, [Symbol(RequestTimeout)]: undefined }, res: ServerResponse { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: false, chunkedEncoding: false, shouldKeepAlive: true, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: true, sendDate: true, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: null, _hasBody: true, _trailer: '', finished: false, _headerSent: false, _closed: false, socket: [Socket], _header: null, _keepAliveTimeout: 5000, _onPendingData: [Function: bound updateOutgoingData], req: [IncomingMessage], _sent100: false, _expect_continue: false, locals: [Object: null prototype] {}, flush: [Function: flush], write: [Function: write], end: [Function: end], on: [Function: on], writeHead: [Function: writeHead], body: '{"a":1,"b":2,"c":3}', [Symbol(kCapture)]: false, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: null }, continueRequest: [Function: next], onError: [Function: onError], _onResponse: [Function: onResponse], matchingRoutes: [ [Object] ], state: { requests: [Object], routes: [Array], pendingEventHandlers: {}, reset: [Function: reset] }, socket: SocketE2E { ensureProp: [Function: ensureProp], supportsRunEvents: true, experimentalSessionAndOrigin: false, ended: false, localBus: [EventEmitter], onTestFileChange: [Function: bound ], testFilePath: 'cypress/e2e/interceptBug.cy.js', onStudioTestFileChange: [Function: bound onStudioTestFileChange], removeOnStudioTestFileChange: [Function: bound removeOnStudioTestFileChange], _io: [SocketIOServer], _sendResetBrowserTabsForNextTestMessage: [AsyncFunction (anonymous)], _sendResetBrowserStateMessage: [AsyncFunction (anonymous)], _sendFocusBrowserMessage: [AsyncFunction (anonymous)], _isRunnerSocketConnected: [Function (anonymous)] }, lastEvent: 'before:request' } } +15ms
  cypress:net-stubbing:server:util sending event to driver { eventName: 'response:callback', data: { eventId: 'event15', subscription: { eventName: 'response:callback', await: false, routeId: '1666797466567-22' }, browserRequestId: '76357.69', requestId: 'interceptedRequest13', data: { headers: [Object], body: '{"a":1,"b":2,"c":3}', url: 'http://localhost:3000/api', method: null, httpVersion: null, statusCode: 200, statusMessage: null } } } +3ms
  cypress:net-stubbing:server:intercept-request request/response finished, cleaning up { requestId: 'interceptedRequest13' } +5ms
  cypress:net-stubbing:server:util sending event to driver { eventName: 'after:response', data: { eventId: 'event16', subscription: { eventName: 'after:response', await: false, routeId: '1666797466567-22' }, browserRequestId: '76357.69', requestId: 'interceptedRequest13', data: {} } } +3ms
    ✓ receives no content-length header when cy.intercept is used to stub (83ms)


  2 passing (4s)
  1 failing

  1) cy.intercept should not change headers
       receives no content-length header when cy.intercept is used to spy:
     AssertionError: Timed out retrying after 4000ms: Expected to find content: 'No Content-Length header' but never did.
      at Context.eval (webpack:///./cypress/e2e/interceptBug.cy.js:16:44)




  (Results)

  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ Tests:        3                                                                                │
  │ Passing:      2                                                                                │
  │ Failing:      1                                                                                │
  │ Pending:      0                                                                                │
  │ Skipped:      0                                                                                │
  │ Screenshots:  1                                                                                │
  │ Video:        true                                                                             │
  │ Duration:     4 seconds                                                                        │
  │ Spec Ran:     interceptBug.cy.js                                                               │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘


  (Screenshots)

  -  /Users/abourgerie/Scratch/cypress-header-bug-minimal/cypress/screenshots/interce     (1280x720)
     ptBug.cy.js/cy.intercept should not change headers -- receives no content-length
      header when cy.intercept is used to spy (failed).png


  (Video)

  -  Started processing:  Compressing to 32 CRF
  -  Finished processing: /Users/abourgerie/Scratch/cypress-header-bug-minimal/cypres    (0 seconds)
                          s/videos/interceptBug.cy.js.mp4


====================================================================================================

  (Run Finished)


       Spec                                              Tests  Passing  Failing  Pending  Skipped
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✖  interceptBug.cy.js                       00:04        3        2        1        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✖  1 of 1 failed (100%)                     00:04        3        2        1        -        -

abourgerie@C02G46BXMD6R cypress-header-bug-minimal %

Other

I also observed this on Cypress 10.5.0 and 10.10.0, as I upgraded while trying to debug.
I also observed this on Node v16.15.0, which is the version my company uses in production.

@mschile mschile self-assigned this Oct 26, 2022
@mschile
Copy link
Contributor

mschile commented Oct 28, 2022

@Bourg, thanks for logging this issue and including the reproduction. It appears we are incorrectly comparing the content-length of the headers without actually verifying the header is populated.

if (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)
}

I am going to route this issue to the appropriate team.

In the meantime, you could use the deprecated cy.route to spy on the call.

cy.server().route('/api').as('api')
cy.visit("");
cy.wait('@api')

If you are using fetch, you'll also need to set experimentalFetchPolyfill to true (this is not needed when using cy.intercept).

@cypress-bot
Copy link
Contributor

cypress-bot bot commented Mar 28, 2023

Released in 12.9.0.

This comment thread has been locked. If you are still experiencing this issue after upgrading to
Cypress v12.9.0, please open a new issue.

@cypress-bot cypress-bot bot locked as resolved and limited conversation to collaborators Mar 28, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants