skip to content

In Cypress 16, how does Chrome's own HTTP cache change what a `cy.intercept()` observes?

level: seniorimportance: should knowfreq 38%

answer

  1. Vantage point moved, server did not
  2. The cache sits upstream now
  3. A merge happens before Cypress sees it
  4. Stubs were never network responses
  5. cy.request() bypasses the browser cache

basics

~20 s

The cache now sits between your server and Cypress. A revalidation your server answers with 304 is merged with the cached copy and reported to the intercept as a complete 200, and a response Cypress produced itself is never cached at all.

solid answer

~50 s

On the legacy network path Cypress made the request itself, so it saw the raw status line — including a `304 Not Modified` on a revalidation. As of Cypress 16, Chrome, Chromium and Edge intercept on the native browser network and **the browser's cache sits between your server and Cypress**. The browser sends the conditional request, receives the `304`, merges it with the copy it already holds, and hands Cypress the complete response, so the intercept reports `200`. Your server still answers `304`; Cypress just no longer observes it. The mirror case is that responses answered **inside** Cypress — a fixture, a static body, a body a handler rewrote — were never network responses, so the browser stores nothing and a second navigation issues the request again even when the stub declared `cache-control`. To assert on caching itself, use `cy.request()`, which goes through Cypress rather than the browser cache.

code

javascript · 13 lines
javascript
// Cypress 16 on Chrome: the intercept reports the complete response both times
cy.intercept('GET', '**/assets/radar-legend.png').as('legend')
cy.visit('/dashboard')
cy.wait('@legend').its('response.statusCode').should('equal', 200)

cy.reload()
// the server still answers 304, but the browser merges it first
cy.wait('@legend').its('response.statusCode').should('equal', 200)

// assert on caching itself through Cypress, not through the browser cache
cy.request('/assets/radar-legend.png')
  .its('status')
  .should('equal', 200)

go deeper

for a junior

Recall that in Cypress 16 an intercept in Chrome reports 200 where it used to report 304, and that the server's behaviour has not changed.

for a middle

Be ready to explain that the browser's cache now sits between the server and Cypress, and why a stubbed response is never stored by the browser.

for a senior

An interviewer expects the diagnosis: a failure in one browser and a pass in another points at the network path, and the repair is to move transport assertions to cy.request() or up to what the user sees.

for a principal

Own the policy on what a browser suite is allowed to assert about caching at all, and where that contract is better verified — a request-level test or an integration test against the origin.

## Which side of the cache Cypress is on The whole behaviour follows from one structural fact. On the **legacy network path** Cypress was the connection: it made the request, so it saw whatever the server put on the wire, and the browser cached whatever Cypress handed back. On the **native browser network** in Cypress 16, the browser owns the connection and its HTTP cache sits **between your server and Cypress**. Everything below is a consequence of that reordering. ## Revalidations report 200, not 304 When the browser already holds a cached copy of the radar legend or the station list, it revalidates with a conditional request and your server answers `304 Not Modified`. On the native browser network the browser receives that `304`, **merges it with the copy it already had**, and hands Cypress a complete response. The intercept sees `200`. - Your server's behaviour has not changed; it still answers `304`. - What changed is the vantage point — Cypress is downstream of the merge. - A Cypress 15 suite that asserted `200` on the first load and `304` on the second now sees `200` twice, and fails on the second assertion after an upgrade. - The failure is **browser-specific**: the same spec still sees `304` in Firefox, WebKit and Electron, which stay on the legacy path. A suite that runs both will fail in one browser and pass in the other, which is the confusing part. To assert on caching behaviour itself, issue the request with `cy.request()`. It goes through Cypress rather than the browser cache and reports the status your server actually sent. ## Responses Cypress produced are never cached The mirror-image case catches teams the other way round. Some responses never involve a network exchange at all: - Responses stubbed by `cy.intercept()` before the request is sent — a `fixture` or a static `body`. - Documents loaded by `cy.visit()` on `http` origins. - Responses whose body a request or response handler modified. The browser does not store any of these, **because there was no network response to store**. A second navigation issues the request again and your route matches again, even if the stub declared `cache-control: max-age=3600`. A test that stubbed the forecast once and expected the reload to be served from cache will now find the route firing twice. | Situation | Legacy network path | Native browser network | |---|---|---| | Server answers a revalidation | Intercept sees `304` | Intercept sees `200` | | Stub declares `cache-control` | Browser may cache it | Never cached | | Browser rejects a response | Status and headers reported | `interception.response` is `undefined` | ## Diagnosing this on a real suite 1. **Read which browser failed.** A failure in Chrome that passes in Firefox is the signature of a network-path difference, not of a flaky app. 2. **Look at what the assertion describes.** If it names a status code that only a *cache* would produce, or a match count that depends on caching, it is asserting on transport rather than on the application. 3. **Rewrite it at the level you actually care about.** Assert on what the dashboard rendered; if the caching contract itself is the requirement, move that assertion to `cy.request()` where the status is observable. 4. **Only then consider the escape hatch.** `forceHttp1: true` restores the old vantage point for every browser, but it is deprecated at introduction and buys time rather than a solution. ## Traps worth naming - **Do not read `200` as proof the server re-sent the body.** It may be the merged result of a `304` and a cached copy. - **Do not use a route's match count as a cache assertion.** With stubs never cached, the count now measures your stubbing, not the browser's storage. - **Do not assume the response was rejected because it is missing.** A response the browser's network stack refuses never reaches Cypress at all: the request is recorded and `interception.response` is `undefined`, so assert on the error state your app renders instead. - **Do not fix one browser and declare victory.** The two network paths coexist in a cross-browser run, and an assertion that passes on one may fail on the other. ## What to remember Cypress 16 did not change what your server does; it changed **where Cypress stands relative to the browser's cache**. Assertions about the application survive the move untouched. Assertions about caching, status lines and storage need either `cy.request()` or a rewrite at the level of what the user sees.

  • A stub declares cache-control: max-age=3600 and the route still fires on reload. Is that a bug?
    No. As of Cypress 16 a response answered inside Cypress was never a network response, so the browser has nothing to store regardless of the headers the stub declared. Expect the route to match again on the second navigation and write the assertion accordingly.
  • Why can an intercepted request in Cypress 16 have no response object at all?
    Because the browser's network stack can refuse a response before delivering it. The interception still records the request, but the status and headers never leave the browser, so interception.response is undefined. Assert on the error state your application renders rather than on a status you cannot see.

saying these in an interview costs you the question

  • Blames the server for no longer sending 304
  • Treats a route's match count as a cache assertion
  • Fixes Chrome and never checks the Firefox run
  • Sets forceHttp1 permanently to keep a 304 assertion
  • Assumes 200 means the body was re-sent in full