skip to content

In Cypress, why does cy.intercept() never stub a request made by cy.request()?

level: middleimportance: should knowfreq 64%

answer

  1. Which half puts the bytes on the wire?
  2. Interception listens in the browser only
  3. An empty Network tab is a clue
  4. Cookies still cross between the halves
  5. Real endpoint, no stub, every time

basics

~10 s

cy.request() is issued by the Cypress Node process, not by the browser, so it never passes through the browser network layer where cy.intercept() listens. It reaches the real endpoint every time.

solid answer

~40 s

`cy.intercept()` can only see traffic the **browser** produces — the fetch and XHR calls the application under test makes from the page. `cy.request()` is not one of those. Cypress issues it from its **Node process**, so there is no browser request to match, nothing appears in the DevTools Network tab, and no CORS preflight is sent. It goes straight to the real endpoint. Cypress does bridge the two halves in one respect: before sending, it attaches the cookies the browser would have sent, and any `Set-Cookie` on the response is written back into the browser's cookie jar, so a session established this way is usable by the page. As of Cypress 16 that is still true in Chrome, Chromium and Edge, where interception now happens on the native browser network.

go deeper

for a junior

Know that Cypress has two ways to reach an API: the page's own calls, and cy.request(), which Cypress sends itself. Being able to say they are not the same channel is enough here.

for a middle

Explain the mechanism: the command is queued in the browser but the HTTP call happens in Node, which is why interception, DevTools and CORS all behave differently for it.

for a senior

An interviewer expects the diagnosis story. Show how you recognise a suite that is asserting against a stub it actually bypassed, and how you decide which checks belong on the wire and which belong in the page.

for a principal

Own where the suite is allowed to talk to a backend directly rather than through the UI, and what that costs in confidence when the browser's own origin and cookie rules are never exercised.

## Two processes, two routes to the network A Cypress run has a browser half and a Node half, and each of them can put bytes on the wire. Which half issued a request decides everything about how visible and how interceptable it is. - The **application under test** runs in the browser. Its `fetch` and `XMLHttpRequest` calls leave the page through the browser's own network stack. Those are the requests `cy.intercept()` was built to observe, stub and delay. - **`cy.request()`** does not run in the page at all. The command is enqueued in the browser, but the actual HTTP call is performed by the Cypress Node process and only the response is handed back to the test. The browser is never asked to make it. That single difference produces every consequence people trip over. There is nothing in the browser to intercept, nothing for DevTools to record, and no page origin behind the call for the browser to police. ## What follows from it | | An app request from the page | `cy.request()` | |---|---|---| | Issued by | The application under test | The Cypress Node process | | Matched by `cy.intercept()` | Yes | No | | Listed in the DevTools Network tab | Yes | No | | CORS preflight and origin rules | Enforced by the browser | Not applied at all | | Cookies sent | By the browser | Attached by Cypress from the cookie jar | | `Set-Cookie` on the response | Stored by the browser | Written back into the browser's jar | The cookie row is the one that surprises people in the other direction: `cy.request()` is deliberately *not* a naked HTTP client. Cypress attaches the cookies that would have gone had the request come from the browser, and it feeds responses' cookies back, so signing in over HTTP leaves the page in a signed-in state. ## Reading it in a bank statement suite Say the statements page calls `GET /api/statements` from the page, and the embedded third-party PDF viewer then fetches the document itself. - To stub the statement list — to force an empty account, a slow response or a 500 — you need `cy.intercept()`, because that call comes from the page. - To check the API contract directly, or to read the latest period before visiting, `cy.request()` is the right tool: it is faster, it skips the UI, and it is unaffected by whatever the page is doing. - What you must not do is stub `/api/statements` and then assert the stub by calling `cy.request('/api/statements')`. The request bypasses the interceptor entirely and hits the real server, so the assertion tells you about production data, not about your stub. A passing `cy.request()` is also weak evidence about the browser: because no origin rules apply, it can succeed against a host the application itself would be blocked from calling. ## Inspecting a request DevTools cannot show you Since the Network tab is empty, use the surfaces the Node half feeds instead: 1. Click the `cy.request()` entry in the Command Log. Cypress prints the request URL, headers and body together with the response status, headers and body to the browser console — open DevTools first, because that is where the output lands. 2. On a finished CI run, the same command detail is available afterwards through Test Replay. 3. If the call itself is failing, remember that `responseTimeout` — not `defaultCommandTimeout` — is the clock it runs against. ## As of Cypress 16 Cypress 16 changed *how* interception works in Chrome, Chromium and Edge: it now intercepts on the native browser network rather than through the legacy proxy path used elsewhere. That is a change of mechanism inside the browser half, and it does not move the boundary described here. `cy.request()` is still issued from Node in every browser, is still invisible to `cy.intercept()`, and still never appears in the Network tab. ## How the response crosses back Because the call happens in Node, what your test receives is not a live browser `Response` object. The Node half performs the request, then hands a **plain, serialized result** back to the browser half, and that object is what the command yields: - `response.status`, `response.headers`, `response.body` and `response.duration` are ordinary data you can assert on directly. - `response.body` is parsed into a JavaScript object when the server's `Content-Type` header ends in `json`, and left as a string otherwise. The parsing decision is made from the *response* header, not from what you sent. - There is no stream to consume and nothing to `await` — the value has already fully crossed the boundary by the time the next command runs. The clock is `responseTimeout`, not `defaultCommandTimeout`, which is another consequence of the work happening on the Node side rather than as a queued browser interaction. ## When a stub looks ignored If an interception you are sure about never fires, walk the boundary before you touch the matcher: 1. Ask which half made the call. If your test made it with `cy.request()`, no matcher will ever match it. 2. Ask whether the page made the call at all — a cached response or a request the app skipped is a different problem from a mismatched route. 3. Only then look at the route pattern itself.

  • How do you inspect the full request and response of a Cypress cy.request() call?
    Click its entry in the Command Log. Cypress prints the request URL, headers and body plus the response status, headers and body to the browser console, so open DevTools before clicking. The same detail is available after the fact on a CI run through Test Replay.
  • Does cy.request() go through the browser's CORS checks?
    No. There is no page origin behind it, so no preflight is sent and no cross-origin rule applies; it can call any host directly. That makes it convenient for setup, but it also means a passing cy.request() proves nothing about whether the application itself would be allowed to make that call.

saying these in an interview costs you the question

  • Claims the right matcher will make an intercept catch it
  • Thinks cy.request() wraps the browser's fetch
  • Expects the call in the DevTools Network tab
  • Assumes it cannot send the session cookie
  • Treats a passing cy.request() as proof the app's call works