A Cypress assertion on `req.httpVersion` passes in Firefox and fails in Chrome. Why?
answer
- Two browsers, two network paths
- Who negotiated the protocol, and when
- The old value described the wrong hop
- Assert on the application, not the transport
- A browser gate needs Electron excluded by name
basics
~20 sThe two browsers are on different network paths. As of Cypress 16 Chrome intercepts on the native browser network, where the protocol is negotiated after interception, so httpVersion is undefined. Firefox still proxies through Cypress, which always reported 1.1.
solid answer
~50 sBecause the browsers use different network paths. As of Cypress 16, Chrome, Chromium and Edge intercept on the *native browser network*: the browser opens the connection, so the protocol is negotiated after Cypress has already handed you the request. `req.httpVersion` is therefore `undefined` and `res.httpVersion` is `null`. Firefox, WebKit and Electron stay on the legacy proxy path, where Cypress made the upstream request itself and always reported `'1.1'` — a value describing the hop to Cypress, not your server, so the assertion was passing for the wrong reason. The fix is to assert on something that describes your application: the request method, a header your code sets, the decoded body, or the rendered page. If a transport assertion must survive, gate it with `Cypress.isBrowser({ family: 'chromium', name: '!electron' })` — excluding Electron by name is what makes the gate match the path.
code
javascript · 20 linesconst usesNativeBrowserNetwork = Cypress.isBrowser({
family: 'chromium',
name: '!electron',
})
it('loads the August statement list', () => {
cy.intercept('GET', '/api/statements*', (req) => {
// passes on both network paths
expect(req.method).to.equal('GET')
expect(req.headers).to.have.property('accept')
if (!usesNativeBrowserNetwork) {
expect(req.httpVersion).to.equal('1.1')
}
}).as('statements')
cy.visit('/statements/2026-08')
cy.wait('@statements')
cy.get('[data-cy=statement-row]').should('have.length', 31)
})go deeper
Know that transport details like the HTTP version are not always visible to a test, and that what a Cypress assertion can see depends on the browser it runs in.
Explain the mechanism: on the native browser network the browser negotiates the protocol after interception, so no value exists to report. Contrast that with the proxy path's constant 1.1.
Show the diagnosis, not just the fact. Expect to be asked how you would tell a genuine cross-browser bug from a path difference, and what you would rewrite the assertion to check instead.
Take a position on transport assertions in general: they are cheap to write, they encode the harness rather than the product, and a suite full of them becomes a migration bill every time the runner changes shape.
## Why one assertion has two answers `req.httpVersion` describes the transport a request travelled over. In Cypress that value has never described your server on its own — it described **whichever hop Cypress could see** — and as of Cypress 16 the two supported browsers see different hops. - On the **legacy network path** (Firefox, WebKit, Electron, or anything with `forceHttp1: true`), Cypress is the connection. It makes the upstream request itself over HTTP/1.1, so it reports `'1.1'` — a fact about the Cypress proxy, not about your statements API. The assertion passes, and it has always been passing for the wrong reason. - On the **native browser network** (Chrome, Chromium and Edge, by default), the browser opens the connection. The protocol is negotiated *after* the request handler has already run, so at the moment Cypress hands you the request the answer genuinely does not exist yet. Rather than report a value that could be wrong, Cypress reports none: `req.httpVersion` is `undefined` and `res.httpVersion` is `null`. So the Chrome failure is not a regression. It is the transport metadata becoming honest. ## The wider family of path-dependent assertions `httpVersion` is the most commonly hit member of a small set. On the native browser network in Chrome, Chromium and Edge: | Assertion | Legacy network path | Native browser network | | --- | --- | --- | | `req.httpVersion` | `'1.1'` | `undefined` | | `res.httpVersion` | `'1.1'` | `null` | | `response.headers['content-encoding']` | `'br'` / `'gzip'` | absent | | `request.headers['content-length']` | present | absent | | Revalidated resource status | `304` | `200` | | Response the browser rejects | observable | `interception.response` is `undefined` | Every row has the same cause: **Cypress is no longer the party making the connection**, so anything that only existed on the Cypress-to-server hop is gone. ## What to assert instead The durable fix is to stop asserting on transport and start asserting on your application. In a bank-statement suite that means: 1. Assert on the **request your app sent** — its method, its URL, a header your code attaches, the body it built. 2. Assert on the **response body**, which is handed to you already decoded on both paths. 3. Assert on the **rendered page** — the statement rows appeared, the PDF frame is visible, the error state rendered. Those assertions pass in every browser and on both network paths, and they describe behaviour a user could notice. An assertion on `httpVersion` describes plumbing. ## Keeping the assertion, if you truly must If a suite genuinely needs a transport assertion for the browsers still on the legacy path, gate it with `Cypress.isBrowser()`: - The right filter is `{ family: 'chromium', name: '!electron' }`. - Excluding **Electron by name** is what makes the gate match the network path. Electron is Chromium-based, so `{ family: 'chromium' }` on its own also matches Electron — which is on the legacy path — and puts it on the wrong side of the gate. - `Cypress.config('forceHttp1')` belongs in the same expression if your project ever sets it, since it moves Chrome back to the legacy path too. ## How the failure usually reaches you It rarely arrives as a clean assertion diff. Two shapes are common: - **The suite is green locally and red in CI**, because a developer runs Chrome and the pipeline runs Firefox as well, or the other way round. The first instinct — "CI is flaky" — sends people looking in entirely the wrong place. - **One browser's job fails on a single expectation** while every behavioural assertion in the same spec passes. That asymmetry is the tell: application behaviour is identical, only the transport metadata differs, which points at the path rather than at the app. Confirm it cheaply by printing `Cypress.browser` and the value in question from inside the handler for one run. If the browser is Chromium-family and not Electron, and the value is missing rather than wrong, you are looking at a path difference, not a defect in the statements API. ## What not to do - **Do not set `forceHttp1: true` to make the assertion pass.** It is deprecated in the same release that introduced it, it restarts the Cypress server when changed, it cannot be scoped per test, and it puts Chrome back on a transport your users never use. Skipping a browser gate is explicitly not a reason to use it. - **Do not assert `'1.1'` and call the suite cross-browser.** It only ever held because Cypress itself spoke HTTP/1.1. - **Do not delete the test.** The test was probably checking something real about your API client; rewrite what it asserts rather than dropping the coverage. If your suite only passes on the legacy path and the difference is **not** one of the documented behaviours above, that is worth reporting as a bug rather than pinning the configuration to hide it.
- Why is `Cypress.isBrowser({ family: 'chromium' })` the wrong gate for this?Electron is a Chromium-family browser, so that filter matches it — but Electron stays on the legacy network path and still reports `'1.1'`. The gate would skip the assertion in the one Chromium browser where it still holds. Adding `name: '!electron'` makes the filter line up with the network path rather than the engine.
- Why not just set `forceHttp1: true` and keep the assertion as it is?It works, and it is the wrong trade. `forceHttp1` was introduced and deprecated in Cypress 16 as a temporary migration aid, it restarts the Cypress server when changed, and it cannot be scoped per test. Setting it puts Chrome back on a transport your users never see, which is exactly the production gap the change closed.
saying these in an interview costs you the question
- Calls the Chrome behaviour a Cypress bug
- Reaches for forceHttp1 as the fix
- Thinks '1.1' described the real server protocol
- Gates on family chromium and forgets Electron
- Deletes the test rather than rewriting the assertion