Sometimes you want mostly-real API behaviour with one specific change: add a 5-second delay to test timeout handling, inject a 503 on the third request to test retry logic, modify a response field to test how the UI handles unexpected values, or strip an authentication header to test unauthorised access. The req.continue() method lets you intercept a request, optionally modify it, send it to the real server, and then modify the real response before the browser receives it.
Modifying Requests and Responses — Surgical Precision
Modification sits between spying (no changes) and stubbing (complete replacement). You keep the real server interaction but alter specific aspects.
// ── MODIFYING RESPONSES: req.continue() with callback ──
// Change one field in the real response
cy.intercept('GET', '/api/products', (req) => {
req.continue((res) => {
// Real response arrives — modify before browser receives it
res.body[0].price = 0.01; // Override first product's price
res.body[0].name = 'SALE ITEM'; // Override first product's name
});
}).as('modifiedProducts');
cy.visit('/shop');
cy.wait('@modifiedProducts');
cy.get('.product-card').first().should('contain.text', '$0.01');
// ── INJECTING NETWORK DELAYS ──
// Add 5 seconds to real API response time
cy.intercept('GET', '/api/products', (req) => {
req.continue((res) => {
res.delay = 5000; // 5 second delay on real response
});
}).as('delayedProducts');
// Test timeout handling
cy.visit('/shop');
cy.get('[data-cy="loading"]').should('be.visible');
// If the app has a 3-second timeout, it should show an error before data arrives
// ── SIMULATING INTERMITTENT FAILURES ──
let callCount = 0;
cy.intercept('GET', '/api/products', (req) => {
callCount++;
if (callCount <= 2) {
// First 2 calls fail — test retry logic
req.reply({
statusCode: 503,
body: { error: 'Service temporarily unavailable' },
});
} else {
// Third call succeeds — app should recover
req.continue();
}
}).as('flakyApi');
cy.visit('/shop');
// Verify retry mechanism works — products eventually appear
cy.get('.product-card', { timeout: 15000 }).should('have.length.greaterThan', 0);
// ── MODIFYING REQUEST HEADERS ──
cy.intercept('GET', '/api/admin/*', (req) => {
// Strip the auth header to test unauthorised access handling
delete req.headers['authorization'];
}).as('noAuth');
// ── MODIFYING REQUEST BODY ──
cy.intercept('POST', '/api/orders', (req) => {
// Inject an extra field the UI does not send
req.body.testMode = true;
req.body.testId = `cypress_${Date.now()}`;
req.continue(); // Send modified request to real server
}).as('taggedOrder');
// ── SIMULATING NETWORK ERRORS ──
// Complete network failure (server unreachable)
cy.intercept('GET', '/api/products', { forceNetworkError: true }).as('networkError');
cy.visit('/shop');
cy.wait('@networkError');
cy.get('[data-cy="network-error"]')
.should('be.visible')
.and('contain.text', 'Unable to connect');
// ── CONDITIONAL MODIFICATION based on request content ──
cy.intercept('POST', '/api/search', (req) => {
if (req.body.query === '') {
// Empty search — return error instead of forwarding to server
req.reply({
statusCode: 400,
body: { error: 'Search query cannot be empty' },
});
} else {
// Non-empty search — forward to real server
req.continue();
}
}).as('search');
req.continue() method is the bridge between spying and stubbing. Calling req.continue() without a callback forwards the request to the real server unchanged (same as spying). Calling req.continue((res) => { ... }) with a callback forwards the request to the real server AND lets you modify the response before the browser receives it. Calling req.reply({ ... }) short-circuits the request entirely and returns your fake response without contacting the server (same as stubbing). These three options — continue, continue-with-callback, and reply — give you full control over the request lifecycle.cy.intercept() and a counter variable, you can simulate exact failure-then-recovery sequences and verify that the retry UI (retry button, auto-retry timer, error-then-recovery transition) works correctly.forceNetworkError: true simulates a complete network failure — the browser receives no response at all, as if the server were unreachable. This is different from a 500 status code (the server responded with an error). Applications handle these differently: network errors trigger the browser’s built-in error handling (no HTTP response to inspect), while 500 errors still provide a response body with error details. Test both scenarios — they exercise different error handling code paths.Common Mistakes
Mistake 1 — Using req.reply() when req.continue() is needed
❌ Wrong: req.reply({ statusCode: 200, body: req.body }) — returns a fake response containing the request body instead of forwarding to the server.
✅ Correct: req.continue((res) => { res.body.modified = true; }) — forwards to the real server and modifies only the response.
Mistake 2 — Forgetting to call req.continue() after modifying request headers
❌ Wrong: cy.intercept('GET', '/api/data', (req) => { req.headers['x-test'] = 'true'; }) — the request hangs because no reply or continue was called.
✅ Correct: cy.intercept('GET', '/api/data', (req) => { req.headers['x-test'] = 'true'; req.continue(); }) — modify then forward.