QA How-To
How to Use Cypress cy.intercept (2026)
Learn Cypress cy.intercept for spying, stubbing, request matching, aliases, GraphQL, response changes, caching issues, and reliable TypeScript tests in CI.
21 min read | 2,770 words
TL;DR
Cypress cy.intercept observes or controls HTTP requests sent by the application under test. Register a precise route before triggering the request, alias it, wait with `cy.wait('@alias')`, inspect the interception, and finish with a user-visible assertion.
Key Takeaways
- Register intercepts before the application action that sends the request.
- Match method and URL narrowly enough that the alias represents one meaningful exchange.
- Use aliases and cy.wait instead of fixed delays for network-driven UI state.
- Choose spying for integrated confidence and stubbing for deterministic edge cases.
- Assert both request details and the user-visible result after the response completes.
- Account for route ordering, browser caching, and requests that never pass through the browser.
Cypress cy.intercept lets a test spy on browser network traffic, provide stubbed responses, modify outgoing requests, or alter real responses. The reliable pattern is to register the route before the application sends the request, give it an alias, trigger the UI action, wait for the alias, and assert the resulting interface state.
This guide covers URL and RouteMatcher syntax, static and dynamic stubs, request and response handling, GraphQL, TypeScript typing, caching, route order, and test strategy. The examples use current Cypress APIs and avoid the removed cy.route workflow.
TL;DR
describe('user search', () => {
it('waits for the search response', () => {
cy.intercept('GET', '**/api/users?query=*').as('searchUsers');
cy.visit('/users');
cy.get('[aria-label="Search users"]').type('Asha');
cy.wait('@searchUsers').then(({ request, response }) => {
expect(request.query.query).to.eq('Asha');
expect(response?.statusCode).to.eq(200);
});
cy.contains('[role=row]', 'Asha Verma').should('be.visible');
});
});
| Goal | Intercept pattern | Result |
|---|---|---|
| Observe a real call | cy.intercept('GET', url) |
Request continues to server |
| Return a fixed body | cy.intercept('GET', url, { body }) |
Server is not called |
| Inspect or change request | cy.intercept(url, req => { ... }) |
Handler controls request phase |
| Inspect or change response | req.continue(res => { ... }) |
Real response reaches callback |
| Wait deterministically | .as('name') plus cy.wait('@name') |
Test resumes after matching cycle |
1. What Cypress cy.intercept Does
cy.intercept registers a route in Cypress's network proxy layer for requests made by the application under test. With no response handler, it spies and lets the request continue. With a body or StaticResponse, it stubs the response. With a function, it can inspect or modify the request, reply immediately, continue to the real server, and inspect or modify the real response.
// Spy only.
cy.intercept('GET', '**/api/projects').as('getProjects');
// Static stub.
cy.intercept('GET', '**/api/projects', {
statusCode: 200,
body: [{ id: 'p-1', name: 'Migration' }],
}).as('getProjectsStub');
The command yields null, not the response. Alias the route and use cy.wait to receive an interception object containing request and, when available, response details. Intercepts are automatically cleared before each test, so register them in the test or in a beforeEach that runs for that test.
The request must pass through the browser network layer Cypress controls. cy.request() runs from Cypress's Node-side process and does not trigger a browser cy.intercept route. A response served entirely from browser cache also never reaches the network layer. These boundaries explain many apparent non-matches.
An intercept is neither a service virtualization platform nor proof that the backend behaved correctly. A stubbed test proves how the UI handles the defined response. A spy on a real environment provides integration evidence but carries environment variability. A balanced suite uses each mode for an explicit purpose.
2. Register, Alias, Trigger, Wait, Assert
Order is the foundation of a reliable network test. Register the intercept first. Then visit the page or perform the action that sends the request. Wait for the alias and inspect the exchange. Finally, assert the user-visible result.
it('loads the current account', () => {
cy.intercept('GET', '**/api/account').as('getAccount');
cy.visit('/account');
cy.wait('@getAccount')
.its('response.statusCode')
.should('eq', 200);
cy.contains('h1', 'Account').should('be.visible');
cy.contains('Mina Shah').should('be.visible');
});
If cy.visit triggers the request, registering afterward is too late. Cypress cannot retroactively attach a route to traffic that already completed. This race may look intermittent when application boot speed changes across local and CI machines.
Use an alias that names the operation, such as getAccount, createInvoice, or searchUsers. Avoid apiCall and request1. One alias can match multiple calls, and consecutive cy.wait('@getAccount') calls wait for successive matching exchanges. You can inspect all completed matches through cy.get('@getAccount.all'); numeric suffixes used with cy.get, such as @getAccount.1, are one-based and are not cy.wait syntax.
Do not stop at the network assertion. A 200 response does not prove the view rendered it correctly. Conversely, a visible result alone may come from cached application state instead of the request under test. Combining request, response, and UI assertions provides a clearer chain of evidence.
For the wider command queue and retry model, read the Cypress end-to-end testing guide.
3. Match Requests Precisely with URLs and RouteMatcher
The shortest overload accepts a URL string, glob, or regular expression. Add an HTTP method whenever the endpoint supports several verbs. Without a method, the route matches all methods.
cy.intercept('GET', '/api/users');
cy.intercept('GET', '**/api/users?*');
cy.intercept('GET', /\/api\/users\/[^/?]+$/);
String patterns are glob-matched with Cypress minimatch behavior. Matching considers the full request URL, although a pattern with matchBase behavior can make concise path patterns useful. Test uncertain patterns directly with Cypress.minimatch while debugging. A literal question mark in a JavaScript glob string must be escaped for minimatch, which means writing a doubled backslash in source. RouteMatcher's query field is usually clearer.
cy.intercept({
method: 'GET',
pathname: '/api/search',
query: {
q: 'playwright',
page: '2',
},
}).as('searchPageTwo');
RouteMatcher supports properties such as method, url, hostname, path, pathname, query, headers, https, port, auth, and times. Every supplied property must match. String properties use glob matching, so do not add constraints that vary unpredictably across environments. Use pathname when query parameters should be matched separately.
Limit a one-time failure stub with times:
cy.intercept(
{ method: 'GET', url: '**/api/recommendations', times: 1 },
{ forceNetworkError: true },
).as('recommendationsFailure');
The next matching request is no longer handled by that one-time route. Remember that overlapping routes also participate according to route ordering. A precise matcher is safer than assuming the broad route disappears in the way you imagine.
4. Spy on Real Requests and Assert Their Contract
Spying keeps the real server in the flow. It is appropriate when the environment is controlled enough for integration confidence and the scenario needs to prove the UI sent the correct request.
type CreateInvoiceRequest = {
customerId: string;
lines: Array<{ sku: string; quantity: number }>;
};
type CreateInvoiceResponse = {
id: string;
status: 'draft' | 'submitted';
};
cy.intercept<CreateInvoiceRequest, CreateInvoiceResponse>(
'POST',
'**/api/invoices',
).as('createInvoice');
cy.get('[name=customer]').select('Acme Ltd');
cy.get('[data-testid=submit-invoice]').click();
cy.wait<CreateInvoiceRequest, CreateInvoiceResponse>('@createInvoice')
.then(({ request, response }) => {
expect(request.body.customerId).to.eq('acme');
expect(request.body.lines).to.deep.include({ sku: 'QA-101', quantity: 1 });
expect(response?.statusCode).to.eq(201);
expect(response?.body.id).to.match(/^inv-/);
});
cy.contains('[role=status]', 'Invoice created').should('be.visible');
Type parameters make request and response bodies easier to inspect in TypeScript, but they do not validate runtime data. The server can still return a different shape. Use focused assertions or runtime schema validation when contract drift is a material risk.
Avoid testing every header added by the browser or gateway. Assert headers the application controls and the scenario needs, such as an idempotency key or content type. Volatile tracing and user-agent values make poor exact assertions.
A route handler may assert on the outgoing request, but placing all assertions inside it can make failure timing harder to read. Waiting on the alias keeps exchange assertions in the visible command flow. Use a handler when you must change the request or assign a conditional alias.
5. Stub Static Responses and Error Conditions
A StaticResponse can define statusCode, headers, body, fixture, delay, throttleKbps, forceNetworkError, and logging behavior. Use it to create deterministic states that are difficult, destructive, or slow to reproduce through a shared backend.
cy.intercept('GET', '**/api/subscription', {
statusCode: 503,
headers: { 'content-type': 'application/problem+json' },
body: {
code: 'SERVICE_UNAVAILABLE',
message: 'Subscription service is temporarily unavailable',
},
delay: 150,
}).as('subscriptionUnavailable');
cy.visit('/billing');
cy.wait('@subscriptionUnavailable');
cy.contains('[role=alert]', 'temporarily unavailable').should('be.visible');
cy.contains('button', 'Try again').should('be.enabled');
Fixtures are useful for large stable bodies:
cy.intercept('GET', '**/api/projects', { fixture: 'projects/active.json' })
.as('activeProjects');
Keep fixtures small, named by scenario, and reviewed like code. A huge production response copied months ago becomes stale and makes the test's intent invisible. Inline a short body when it explains the case better.
Test representative boundaries: empty collection, partial optional data, authorization error, validation problem, server error, slow response, and network failure. Do not stub impossible combinations only because the UI can render them. Derive cases from the API contract or a known resilience requirement.
A stub responds before the server sees the request, so it cannot verify persistence, authorization enforcement, database constraints, or downstream integration. Cover those risks in API and service-level tests. The API mocking and stubbing guide explains how to divide confidence between simulated and integrated checks.
6. Modify Requests and Real Responses
A route handler receives an IncomingHttpRequest. You can read or change its URL, method, body, and headers, then call req.reply() to stub or req.continue() to send it upstream. If the handler completes without replying, Cypress can continue through matching routes and eventually to the server. Be explicit when modification is important.
cy.intercept('POST', '**/api/orders', (req) => {
req.headers['x-test-run-id'] = 'checkout-creates-order';
req.body = {
...req.body,
source: 'e2e',
};
req.continue();
}).as('createOrder');
To inspect or adjust a real response, pass a callback to req.continue.
cy.intercept('GET', '**/api/features', (req) => {
req.continue((res) => {
expect(res.statusCode).to.eq(200);
res.body = {
...res.body,
experimentalCheckout: true,
};
res.setDelay(100);
});
}).as('getFeatures');
Use current response methods such as res.setDelay() and res.setThrottle(), not obsolete names from older network-stubbing examples. A request can call req.reply(staticResponse) for a dynamic decision.
cy.intercept('GET', '**/api/accounts/*', (req) => {
const accountId = req.url.split('/').pop();
if (accountId === 'suspended') {
req.reply({
statusCode: 423,
body: { code: 'ACCOUNT_LOCKED' },
});
} else {
req.continue();
}
});
Do not call Cypress commands such as cy.fixture() inside the route handler. The handler runs when the request arrives and is not a normal place to enqueue commands. Load required data before registration or use the fixture property in a static response. The handler may return a Promise, which Cypress awaits, but keep it quick and deterministic.
7. Cypress cy.intercept vs cy.request and UI-Only Waiting
These tools operate at different layers. Picking the wrong one creates misleading coverage.
| Tool or approach | Runs from | Primary use | Triggered by cy.intercept? |
|---|---|---|---|
cy.intercept |
Cypress network proxy for app traffic | Spy, stub, or modify browser requests | It is the interceptor |
cy.request |
Cypress Node process | Direct API setup and API assertions | No |
| UI assertion | Browser DOM through Cypress commands | Verify user-visible state | Not by itself |
Fixed cy.wait(2000) |
Test timer | Delay regardless of system state | No, avoid for synchronization |
| Server mock platform | Separate process or environment | Broader service virtualization | Depends on routing, not Cypress aliasing |
Use cy.request to create test data or validate an endpoint directly when the browser is irrelevant. Use cy.intercept when the application's browser request or response behavior matters. Use UI assertions to prove rendering and interaction. A strong end-to-end scenario can use cy.request for fast setup, spy on the browser's critical call, and assert the resulting UI.
Never expect an intercept to catch a cy.request. If a direct API request must be asserted, assert its yielded response. If the application server makes a server-to-server call after receiving the browser request, Cypress usually sees the browser-to-application call, not the internal downstream exchange. Observe downstream behavior through service tests, logs, or an approved mock at that boundary.
Replacing cy.wait(2000) with cy.wait('@saveProfile') ties synchronization to an actual event. Still account for client rendering after the response by chaining a retryable UI assertion. Cypress automatically retries .should(...) queries, so no second arbitrary delay is needed.
8. Handle Multiple Calls, Polling, and Route Order
One alias can collect several interceptions. Consecutive waits consume successive matching calls. Use this for pagination or a known retry sequence, but keep assertions explicit.
cy.intercept('GET', '**/api/jobs?*').as('getJobs');
cy.visit('/jobs');
cy.wait('@getJobs').its('request.query.page').should('eq', '1');
cy.contains('button', 'Next').click();
cy.wait('@getJobs').its('request.query.page').should('eq', '2');
To inspect completed calls together, retrieve the alias collection.
cy.get('@getJobs.all').then((calls) => {
expect(calls).to.have.length(2);
expect(calls.map((call) => call.response?.statusCode)).to.deep.eq([200, 200]);
});
Ordinary intercept routes are matched in reverse definition order. Routes declared with { middleware: true } run first in definition order and are suited to cross-cutting handling, such as removing cache headers. Overlapping broad and narrow routes can therefore produce surprising results. Register broad middleware intentionally and keep scenario stubs narrow.
beforeEach(() => {
cy.intercept(
{ url: 'https://api.example.test/**/*', middleware: true },
(req) => {
req.on('before:response', (res) => {
res.headers['cache-control'] = 'no-store';
});
},
);
});
Use times for a controlled first-call failure followed by a real or separately stubbed retry. Verify both calls and the UI's retry state. Avoid an alias so broad that background polling consumes the wait intended for a user action. Match method, pathname, query, or a request header that distinguishes the operation.
9. Intercept GraphQL Operations
GraphQL commonly sends queries and mutations to one POST endpoint, so URL and method cannot distinguish operations. Inspect req.body.operationName when the client sends it, or parse the query text only as a fallback. Assign a per-request alias.
cy.intercept('POST', '**/graphql', (req) => {
if (req.body.operationName === 'SearchJobs') {
req.alias = 'gqlSearchJobs';
}
if (req.body.operationName === 'SaveJob') {
req.alias = 'gqlSaveJob';
}
});
cy.visit('/jobs');
cy.get('[aria-label="Search jobs"]').type('SDET{enter}');
cy.wait('@gqlSearchJobs').then(({ request, response }) => {
expect(request.body.variables.term).to.eq('SDET');
expect(response?.body.errors).to.be.undefined;
expect(response?.body.data.searchJobs).to.be.an('array');
});
For stubbing, reply based on the operation.
cy.intercept('POST', '**/graphql', (req) => {
if (req.body.operationName === 'SearchJobs') {
req.reply({
statusCode: 200,
body: {
data: {
searchJobs: [{ id: 'job-1', title: 'Senior SDET' }],
},
},
});
}
});
A GraphQL transport response can be HTTP 200 while the body contains errors. Assert both the HTTP status and the GraphQL envelope relevant to the scenario. Also assert variables, because the same operation with incorrect input may still return structurally valid data.
Persisted queries may send a hash instead of query text. Operation name or a documented extensions field is then the better discriminator. If the client omits operation names, consider adding them for observability and testability rather than matching fragile query formatting.
10. Debug Intercepts That Never Match
Start with timing. Confirm the intercept appears in the Routes instrument before the request occurs. If cy.visit triggers the call, registration must precede it. Click the request in the Command Log and compare its full URL, method, query, and headers with the matcher.
Next check the execution boundary. A cy.request call, server-side render fetch, plugin task, or backend-to-backend request does not originate as application browser traffic. A service worker or browser cache can satisfy a resource without a new network exchange. Inspect browser developer tools and disable or control caching in the test environment where appropriate.
Verify glob assumptions with minimatch:
expect(
Cypress.minimatch(
'https://api.example.test/api/users?page=2',
'**/api/users?*',
),
).to.be.true;
Prefer RouteMatcher pathname and query when escaping becomes confusing. Remember that a missing method matches all verbs, while an unexpected preflight OPTIONS request may appear separately from the application request. Match the intended verb explicitly.
Check overlapping intercepts and definition order. A later broad stub can take precedence over an earlier scenario spy. Name routes clearly and keep global support intercepts limited to cross-cutting needs. Use { log: false } only for noisy requests after debugging; hiding logs too early removes useful evidence.
Finally, inspect whether the application actually sent the request. A validation error, disabled button, stale local cache, or application exception may stop it. Assert the precondition before waiting, and capture console errors or application logs. A timeout on cy.wait('@alias') sometimes indicates a product bug rather than a matcher bug.
11. Build a Maintainable Network Test Strategy
Classify each network-aware test as spy, stub, or hybrid and record why. Spy on core happy paths that need deployed integration confidence. Stub rare failures and deterministic data variations. Use direct API calls for setup and service-level checks. This avoids both extremes: a slow suite dependent on every backend and a fully mocked suite that cannot detect integration drift.
Keep intercept declarations close to the scenario unless they represent a genuine suite-wide rule. A large beforeEach full of invisible stubs makes it difficult to know which backend behavior the test exercises. Shared helpers should return or document the alias names they create and accept scenario data explicitly.
function stubSubscription(
body: { plan: string; state: 'active' | 'past_due' },
) {
cy.intercept('GET', '**/api/subscription', {
statusCode: 200,
body,
}).as('getSubscription');
}
it('shows a payment recovery action', () => {
stubSubscription({ plan: 'pro', state: 'past_due' });
cy.visit('/billing');
cy.wait('@getSubscription');
cy.contains('button', 'Update payment method').should('be.visible');
});
Validate stubs against the source API contract. Typed fixture factories reduce accidental drift, and schema checks can protect large shared bodies. Still run integrated tests that reveal authentication, gateway, serialization, CORS, and deployment issues. The Cypress best practices guide offers complementary advice on isolation and selector design.
Review network tests for sensitive logging and data. Interception objects can contain tokens and personal information. Assert only what matters, avoid printing full bodies by default, and use synthetic accounts. Test code is production engineering infrastructure and should follow the same data-handling expectations as other observability tools.
Interview Questions and Answers
Q: What is the difference between spying and stubbing with cy.intercept?
A spy registers a matcher without providing a response, so the request continues to the real server. A stub supplies a static or dynamic response and can prevent the server call. Spies provide integration evidence; stubs provide controlled, deterministic scenarios.
Q: Why must cy.intercept usually come before cy.visit?
Many applications send initialization requests during page load. If the route is registered after cy.visit, the request may already be complete and cannot be captured retroactively. Registering first removes that race.
Q: Does cy.intercept catch cy.request calls?
No. cy.request executes from the Cypress process rather than as application browser traffic. Assert the response yielded by cy.request directly, and use cy.intercept for requests made by the application under test.
Q: How do you distinguish GraphQL operations on one endpoint?
Inspect req.body.operationName and assign req.alias for the matching query or mutation. Then wait on that operation-specific alias and assert variables plus the GraphQL response envelope. Query-text matching is a fallback because formatting and persisted-query behavior can change.
Q: What causes an intercept to miss a request?
Common causes are late registration, an incorrect method or URL matcher, browser cache, a non-browser request, and overlapping route order. The application may also never send the expected request because an earlier UI condition failed.
Q: How do you test one failed call followed by a successful retry?
Register a failure route with times: 1, then allow or define the subsequent route. Alias and assert both exchanges and verify the UI's retry behavior. Keep matchers precise so background calls do not consume the one-time route.
Common Mistakes
- Registering the intercept after the page load or click that already sent the request.
- Omitting the HTTP method and unintentionally matching GET, POST, and preflight traffic.
- Using a broad glob whose alias is consumed by unrelated polling.
- Expecting
cy.interceptto capturecy.requestor backend-to-backend calls. - Replacing every backend with stubs and claiming full integration coverage.
- Asserting the response but never checking whether the UI rendered the result correctly.
- Calling Cypress commands inside a route handler instead of preparing data beforehand.
- Ignoring reverse route definition order when broad and narrow routes overlap.
- Adding
cy.wait(2000)instead of waiting on a meaningful alias and retryable UI state. - Copying huge production fixtures that contain stale or sensitive information.
- Forgetting that a browser-cached response never reaches the network interception layer.
Conclusion
Cypress cy.intercept is most reliable when its matcher, registration timing, alias, and assertions describe one intentional browser exchange. Use spying to observe real integration, stubbing to create controlled states, and route handlers only when request or response behavior must change.
Refactor one fixed-delay network test by registering a precise intercept before the trigger, waiting on its alias, checking the important request and response fields, and asserting the visible outcome. That pattern produces faster diagnosis, clearer intent, and less timing-dependent automation.
Interview Questions and Answers
Explain the standard cy.intercept workflow.
I register a precise method and route before the triggering action, assign an operation-based alias, trigger the UI request, and wait on the alias. I inspect important request and response fields, then assert the user-visible outcome. This removes fixed timing guesses.
When would you spy instead of stub?
I spy when the test needs confidence in the deployed integration and the environment is controlled. I stub when I need deterministic data, rare failures, or a frontend-focused case. A balanced suite keeps core integrated paths while simulating targeted edge conditions.
How does route ordering work in Cypress intercepts?
Ordinary matching routes are evaluated in reverse definition order. Routes with `middleware: true` run first in definition order. I avoid accidental overlap by keeping matchers narrow and reserving middleware for deliberate cross-cutting behavior.
How would you diagnose cy.wait timing out on an alias?
I confirm the route was registered before the trigger, compare the actual request method and URL to the matcher, and check cache and route overlap. I verify the request originates from the application browser. I also inspect whether the UI failed before it could send the request.
How do you intercept GraphQL reliably?
I match the POST GraphQL endpoint, inspect `operationName`, and assign a request-specific alias. I assert variables and both transport and GraphQL-level results. For persisted queries, I use operation metadata rather than fragile query-string formatting.
What are the limitations of a stubbed cy.intercept test?
It does not prove that the backend accepted, authorized, persisted, or correctly generated the response. It can drift from the real API contract. I protect stubs with types or schemas and retain service-level and integrated browser coverage.
Frequently Asked Questions
What does Cypress cy.intercept do?
It registers a route that can observe, stub, or modify matching HTTP requests made by the application under test. Alias the route and use `cy.wait` to inspect a completed request and response cycle.
Does cy.intercept need to be before cy.visit?
Yes when page load may trigger the target request. Cypress cannot capture a request that finished before the intercept was registered.
Why is my Cypress intercept not matching?
Check registration timing, method, full URL or RouteMatcher fields, glob escaping, route ordering, and browser caching. Also verify the traffic comes from the browser and that the application actually sends it.
Can cy.intercept mock an API response?
Yes. Pass a body, fixture, or `StaticResponse`, or call `req.reply()` in a route handler. The resulting test validates frontend behavior against the defined stub, not the real backend implementation.
Does cy.intercept catch cy.request?
No. `cy.request` is sent outside the application browser context. Use the response yielded by `cy.request` for direct assertions.
How do I wait for multiple intercepted requests?
Call `cy.wait('@alias')` repeatedly for successive matches, wait on an array of different aliases, or inspect completed matches through `cy.get('@alias.all')`. Keep aliases precise so unrelated calls do not satisfy the wait.
Can cy.intercept modify a real response?
Yes. Call `req.continue(res => { ... })` in a route handler, then inspect or modify response properties. Current APIs include response controls such as `res.setDelay()` and `res.setThrottle()`.