LEARN · DEBUGGING GUIDE

Cypress intercept stub not intercepting requests — debugging guide

You write a cy.intercept() stub, the test passes but the real network call goes through — or you get a timeout instead of a stub response. Here’s how to find out why.

IntermediateTesting8 min read

What this usually means

cy.intercept() is a powerful API but has a few gotchas. Most commonly, the URL pattern you provided doesn't actually match the request's URL — either because of protocol (http vs https), trailing slashes, query parameters, or a different HTTP method. Another frequent cause is the order of intercept definitions: Cypress matches requests to the last registered matching handler, so if you define a generic stub after a more specific one (or vice versa), the wrong handler takes precedence. Timing issues also bite engineers: if the request fires before cy.intercept() is set up (especially in before() vs beforeEach()), the stub never sees it. Finally, missing cy.as('alias') or using the wrong alias string leads to silent failures.

( 01 )Fast diagnosis

The first ten minutes — establish facts before touching code.

  • 11. Open Cypress DevTools and go to the 'Routes' tab — it shows all registered intercepts and whether they matched any requests.
  • 22. Add a cy.intercept('*').as('all') and cy.wait('@all', { timeout: 10000 }) before your test assertion — if this catches the request, your pattern is wrong.
  • 33. Check the exact URL your app is hitting by adding console.log to the fetch/XHR wrapper or using a network proxy.
  • 44. Verify the HTTP method: cy.intercept('POST', '/api/...') will not intercept a GET request.
  • 55. Ensure cy.intercept() is called before the request is triggered — move it to beforeEach() if it's in a test body after a click.
  • 66. Test your URL pattern in a regex tester: /api\/users/.test('http://localhost:3000/api/users') — Cypress does substring match by default, but exact match with path-to-regexp can surprise.
( 02 )Where to look

The specific files, logs, configs, and dashboards that usually own this bug.

  • searchcypress/e2e/*.cy.js — the spec file where the intercept is defined
  • searchNetwork tab in Cypress runner or Chrome DevTools — compare actual request URLs vs your pattern
  • searchCypress 'Routes' tab (Debug sidebar) — shows registered intercepts and match counts
  • searchcypress/support/e2e.js — global intercepts or overwritten commands that might interfere
  • searchApplication source code — the actual fetch/axios call URL and method
  • searchCI logs or screenshot/video artifacts — headless mode may hide network details
  • searchCypress config (cypress.config.js) — baseUrl, proxy settings, or experimental features
( 03 )Common root causes

Practical causes, not theory. These are the things you will actually find.

  • warningURL pattern mismatch: missing query params, trailing slash, or protocol (http vs https)
  • warningHTTP method mismatch: default is GET*, but your request is POST/PUT/DELETE
  • warningIntercept defined after request fires (race condition: test body vs before/beforeEach)
  • warningMultiple intercepts matching the same URL — the last one wins, but order is confusing
  • warningAlias not assigned (missing .as('alias') or typo in cy.wait())
  • warningCypress.config() baseUrl changes URL resolution: relative vs absolute patterns
  • warningStub response object is malformed (missing required fields) causing app to ignore it
( 04 )Fix patterns

Concrete fix directions. Pick the one that matches your root cause.

  • buildUse exact URL string instead of path pattern for critical stubs: cy.intercept('GET', 'http://localhost:3000/api/users', ...)
  • buildAdd a catch-all intercept at the top of the test to log unmatched requests: cy.intercept('*', req => { console.log(req.url) })
  • buildMove cy.intercept() calls to beforeEach() or even before() with proper cleanup to ensure they're registered before any requests
  • buildUse cy.intercept().as('alias') and then cy.wait('@alias') to synchronize — this also verifies the request happened
  • buildFor multiple intercepts, use routeHandler to differentiate by request body or headers: cy.intercept({ method: 'POST', url: '/api/items', headers: { 'x-custom': 'value' } }, ...)
  • buildClear intercepts between tests with Cypress.intercept.clear() or use a fresh cy.session() per test
( 05 )How to verify

A fix you cannot prove is a guess. Close the loop.

  • verifiedRun the test with Cypress open and watch the Routes tab — see the intercept count increment
  • verifiedAdd a cy.wait('@yourAlias') just after the action that triggers the call — if it passes, the intercept matched
  • verifiedCheck the response in the Cypress Network tab: if it shows 'stubbed' with a green badge, it worked
  • verifiedTemporarily change the stub to return a unique header (e.g., 'x-stubbed': 'true') and verify the app receives it
  • verifiedWrite a negative test: use cy.intercept().as('unexpected') and assert that cy.get('@unexpected.all').should('have.length', 0) to prove no unmatched calls
  • verifiedIn CI, enable video recording and review the test run to see if the network call was intercepted
( 06 )Mistakes to avoid

Things that make this bug worse or harder to find.

  • warningDon't define intercepts inside describe() block without before/beforeEach — they may not be registered in time
  • warningDon't use regex patterns without escaping dots and slashes: /api\/users/ is correct, not /api/users/
  • warningDon't forget that cy.intercept() with no method argument defaults to GET* (all methods) — but if you specify a method, it's exact
  • warningDon't rely on cy.intercept() alone for timing — always use cy.wait() or cy.spy() to ensure the stub is active
  • warningDon't assume the baseUrl is applied to intercept patterns — Cypress uses the pattern as-is; if it's relative, it's resolved against the current page URL
  • warningDon't reuse intercept aliases across tests without clearing — state leaks can cause stale matches
( 07 )War story

The phantom 404: When cy.intercept() silently ignored my POST

Senior Test EngineerReact 18, Axios, Cypress 12, MSW for mocking (but not used here)

Timeline

  1. 09:15Write new test for user profile update; stub PUT /api/user/me with a 200 response
  2. 09:20Run test — fails with timeout on cy.wait('@updateProfile')
  3. 09:25Check Cypress Network tab: actual PUT request hits server and returns 404
  4. 09:30Check Routes tab: intercept is registered but match count = 0
  5. 09:40Realize the test calls axios.put('/api/user/me') but my pattern is '/api/user/me/' with trailing slash
  6. 09:45Fix pattern to exact URL, add cy.wait('@updateProfile') — test passes
  7. 09:50Root cause: trailing slash in pattern; also missing method specification (default GET) but that wasn't the issue here
  8. 10:00Add a catch-all intercept in support file to log all unmatched requests for future tests

I was adding a test for the user profile update flow. The component calls axios.put('/api/user/me', data). I wrote cy.intercept('PUT', '/api/user/me/', { statusCode: 200 }).as('updateProfile'). Looked fine. Ran the test — it hung on cy.wait('@updateProfile', { timeout: 10000 }) and eventually timed out. My first thought was 'Cypress is broken' or 'the app didn't send the request'. I opened the DevTools Network tab and saw the request went out — to /api/user/me (no trailing slash) and got a real 404 from the server. So the stub didn't match.

I checked the Routes tab in Cypress debugger. The intercept was registered with the pattern '/api/user/me/' and the method PUT. Match count was 0. That's when I saw the trailing slash in the pattern. Axios was sending to '/api/user/me' — no slash. Cypress does exact path matching for strings (if you use path-to-regexp it's different, but with a string it's an exact match including slash). Removing the slash fixed it instantly.

But why didn't I catch this earlier? Because I'd written the pattern in a hurry, copying from a previous test that used a trailing slash. The real lesson: always double-check the exact URL your app sends. Now I have a utility function that logs all unmatched requests in the support file. It's saved me hours since.

Root cause

Trailing slash in the intercept URL pattern (/api/user/me/) did not match the actual request URL (/api/user/me).

The fix

Changed cy.intercept('PUT', '/api/user/me/', ...) to cy.intercept('PUT', '/api/user/me', ...). Also added a global catch-all intercept to log unmatched requests.

The lesson

Never assume the URL pattern you write matches the actual request. Always verify with the Routes tab or a catch-all logger. Small differences like trailing slashes, query params, or encoding can silently break stubs.

( 08 )How cy.intercept() matching actually works

Cypress uses the routeMatcher object to decide if a request matches. If you pass a string URL, it's converted to a path-to-regexp pattern. That means '/api/users' matches '/api/users', '/api/users/123', and '/api/users/123/'. But '/api/users/' (with trailing slash) only matches paths that end with a slash. So trailing slashes matter.

If you pass a regex, it's tested against the full URL (including origin). So /\/api\/users$/ will not match '/api/users/123'. The method field is optional and defaults to 'GET*' which matches any method. If you specify a method, it's exact (uppercase). Headers matching is also case-insensitive by default, but body matching is exact JSON (if using cy.intercept with a body field).

( 09 )Timing and ordering gotchas

cy.intercept() registers a route handler that persists until the test ends or you clear intercepts. However, if you define an intercept inside a test after a cy.visit() or a click that triggers a request, the request may fire before the intercept is registered. Always define intercepts in beforeEach() or before() — never after an action that might fire the request synchronously.

Multiple intercepts matching the same URL: Cypress uses the last registered matching handler. So if you register a general catch-all first and a specific stub later, the specific stub will win (because it's later). But this can be confusing when you have test-level intercepts overriding global ones. Use cy.intercept().as('alias') and cy.wait() to ensure the correct one is used.

( 10 )Stubbing responses — common pitfalls

When you pass a response object to cy.intercept(), it must be a valid HTTP response: { statusCode, body, headers, delayMs }. If you omit statusCode, it defaults to 200. If you omit body, the response body is empty. If your app expects a specific JSON structure, missing fields can cause the app to error silently.

If you use a function handler (req.reply()), ensure you call req.reply() exactly once. Calling it multiple times will cause errors. Also, if you modify req.body, it's a read-only property — you need to clone it. Finally, remember that cy.intercept() stubs do not cancel the actual request unless you use req.reply() — the request still goes to the server unless you explicitly prevent it (by not calling req.continue or req.reply).

( 11 )Aliasing and waiting

The .as('alias') chain is critical. If you forget it, you can't cy.wait() for the request. But also note that cy.wait('@alias') resolves when the request is made, not when the response is received. If you need to wait for the response, use cy.wait('@alias').its('response.statusCode').

A common mistake is using the wrong alias string: cy.wait('@updateProfile') but the intercept is aliased as 'update-profile'. Cypress alias names are case-insensitive but hyphen vs underscore matters. Also, cy.wait() will fail if no request matched the alias by the timeout. Use { timeout: 10000 } to give more time.

( 12 )Debugging with Routes tab and cy.spy()

The Cypress Routes tab (in the Debug sidebar when running in open mode) shows every registered intercept, its pattern, method, and how many requests matched. If the count stays 0, your pattern is wrong. If it's >0 but the test still fails, check the response.

You can also use cy.spy() to intercept requests without stubbing: cy.intercept('GET', '/api/users').as('getUsers'). Then in your test, cy.get('@getUsers').should('have.been.calledOnce'). This is useful for verifying calls without modifying behavior.

Frequently asked questions

Why does my intercept work locally but fail in CI?

In headless mode, the baseUrl might differ (e.g., localhost vs 127.0.0.1) or the app may use environment-specific API paths. Also, CI often runs tests faster, exposing race conditions where the intercept is registered after the request fires. Use cy.intercept() in beforeEach() and add a small cy.wait(100) before the action to stabilize.

Can I stub GraphQL requests with cy.intercept()?

Yes. Since GraphQL typically uses a single endpoint (e.g., /graphql) with POST method, you can match by URL and method. To differentiate queries/mutations, use a function handler matching req.body.operationName or req.body.query. Example: cy.intercept('POST', '/graphql', (req) => { if (req.body.operationName === 'GetUsers') { req.reply({ data: { users: [] } }) } }).

What's the difference between cy.route() and cy.intercept()?

cy.route() is deprecated since Cypress 6.0. It used XMLHttpRequest only, while cy.intercept() handles both XHR and fetch. cy.intercept() also supports more flexible matching (regex, glob, path-to-regexp) and can modify requests/responses in flight. Always use cy.intercept() for new tests.

My intercept matches but the response is ignored by the app — why?

The app may be checking the response structure. If your stub returns a different shape than expected (e.g., missing fields, wrong data types), the app might throw an error silently. Use the Network tab to inspect the real response and copy its shape exactly. Also, ensure the Content-Type header is correct (e.g., application/json).

How do I intercept requests that have dynamic query parameters?

Use a regex pattern that matches the base path and ignore query params: cy.intercept(/\/api\/items\?.*/, ...). Or use a function handler that checks req.url.includes('/api/items'). For exact param matching, use the query object in routeMatcher: cy.intercept({ pathname: '/api/items', query: { page: '1' } }, ...).