skip to content

Why does a Cypress cy.origin() callback error when it calls cy.intercept() or cy.session()?

level: seniorimportance: should knowfreq 40%

answer

  1. Each origin runs its own instance
  2. A few commands refuse to cross
  3. Interceptions belong on the primary side
  4. Session wraps origin, never the reverse
  5. cy.origin blocks do not nest

basics

~20 s

Those commands are not supported inside the callback, because the block runs in a second Cypress instance in the other origin. Declare them outside the block instead: the interception before the navigation, and cy.session() wrapping the whole flow.

solid answer

~40 s

A `cy.origin()` callback runs in a separate Cypress instance injected into the secondary origin, and a short list of commands refuse to run there: `cy.intercept()`, `cy.session()`, the `Cypress.session.*` methods, and a nested `cy.origin()`. Each throws a dedicated error naming the command and pointing at the open issue for it. The fix is placement, not a workaround. Register interceptions **before** the navigation that leaves your origin — they are set up by the runner rather than by the page, so they still match requests the secondary origin makes. Put `cy.origin()` **inside** the setup callback of `cy.session()`, never the reverse, so the session wrapper owns the whole sign-in. And drive several origins with consecutive top-level blocks rather than by nesting them.

code

javascript · 12 lines
javascript
it('shows the viewer error banner when a statement PDF is unavailable', () => {
  // Registered on the primary origin, before the navigation. The same call
  // inside the cy.origin() callback below would throw.
  cy.intercept('GET', '**/documents/*.pdf', { statusCode: 503 })

  cy.visit('https://statements.example.com/statements/2026-08')
  cy.get('[data-cy=open-in-viewer]').click()

  cy.origin('https://viewer.pdfhost.example', () => {
    cy.get('[data-cy=viewer-error]').should('be.visible')
  })
})

go deeper

for a junior

Know that a cy.origin() callback is a restricted place and that not every Cypress command is allowed in it; recognising the error message is enough here.

for a middle

Explain that the callback runs in a second Cypress instance, and name the commands that throw: cy.intercept(), cy.session(), Cypress.session methods and a nested cy.origin().

for a senior

Be ready to restructure a real sign-in-plus-stub flow so the interception sits before the navigation and cy.session() wraps the block, and to say why that ordering is the correct one.

for a principal

Own the call on whether a suite works around these restrictions or avoids driving third-party origins at all, and what that decision costs in coverage and in flake.

## Why the callback is a restricted environment Entering `cy.origin()` does not move your test into the other origin; it **starts a second Cypress instance there** and evaluates the stringified callback inside it. That instance drives the DOM of the secondary origin, but it is not the instance that owns the test as a whole. Commands whose effect is global to the test — the network route table, the cached session, the origin switch itself — therefore have no coherent meaning inside the callback, and Cypress refuses them outright rather than half-doing them. ## The commands that throw | Written inside the callback | What happens | Where it belongs | | --- | --- | --- | | `cy.intercept()` | throws, suggesting you use it outside the callback | before the navigation, on the primary origin | | `cy.session()` | throws, suggesting you use it outside the callback | around the block, in its setup callback | | `Cypress.session.*` methods | throw with a pointer to the session API docs | outside the block, at the top level | | a nested `cy.origin()` | throws as not currently supported | as the next top-level block | Everything else you would expect is available: `cy.visit()`, queries and actions, assertions, `cy.request()`, cookie and storage commands, custom commands you have loaded into that origin, and `Cypress.require()` for pulling in modules. ## Restructuring a flow that needs all three A bank statement app that hands a document off to a third-party viewer usually wants all of it in one test: a stubbed PDF response, a signed-in session, and commands running on the vendor's origin. The order that works: 1. **Interceptions first, on your own origin.** `cy.intercept()` is registered by the runner, not injected into the page, so a route declared before the navigation keeps matching once the browser is on the vendor's origin. Declaring it early also avoids the race where the request fires during the navigation itself. 2. **`cy.session()` on the outside.** Its setup callback may contain a `cy.origin()` block, which is how a redirect-based sign-in gets cached; the reverse nesting is rejected. Validation logic belongs in the session's own `validate` option, on the primary origin. 3. **One block per origin, in sequence.** If the flow passes through two vendors, write two `cy.origin()` calls one after the other. Between blocks you are back on the primary origin, which is the natural place to assert that the round trip landed where it should. ## Why aliases and state do not simply flow across The two instances share a test, not a memory. Practical consequences worth expecting: - the callback cannot see spec-level variables at all — data crosses only through the `args` option, as serializable values - anything the block yields must be serializable too, so assertions are usually cleanest kept inside it - the JavaScript execution context **is** reused between blocks for the same origin in one spec, which is why requiring custom commands once in a `before` hook makes them available to later blocks for that origin - that reuse does not extend to a different origin: a second vendor's block starts empty and needs its own `Cypress.require()` ## Reading the failure when it happens in CI These errors are explicit and name the command, so the trap is rarely the message itself — it is what a team does next. Two failure modes to watch for: - **Copying the flow instead of moving the command.** Faced with the interception error, people re-implement the stub as a fixture served by the vendor, or drop the stub entirely and let the test depend on a live third party. Moving one line out of the block is almost always the smaller change. - **Concluding the scenario is untestable.** The error says the command is unsupported *in the callback*, not that cross-origin stubbing is impossible. Interception, session caching and origin switching all work in the same test; they just each have a place. As of Cypress 16 each restriction is still an open issue rather than a permanent design decision, so the error messages link to the issue and invite a use case — worth knowing when you are deciding whether to build a workaround or wait.

  • Where does cy.session() go in a Cypress test whose sign-in happens on another origin?
    Outside the block. `cy.session()` is called at the top level and its setup callback contains the `cy.origin()` block that drives the identity provider, so the whole redirect flow is cached under one session id. Writing it the other way round throws, and the `validate` option runs on the primary origin where your application's own session lives.
  • How does a Cypress test drive two different secondary origins in one test?
    With consecutive top-level `cy.origin()` calls, one per origin — callbacks may not contain another `cy.origin()`. Between the blocks the test is back on its primary origin. Note that the execution context persists per origin within a spec, so modules required in an earlier block for the same origin are still loaded, but a different origin starts fresh.

saying these in an interview costs you the question

  • Stubs the network from inside the cy.origin() callback
  • Nests one cy.origin() block inside another
  • Wraps cy.origin() around cy.session() instead of the other way round
  • Concludes cross-origin requests cannot be stubbed at all
  • Reimplements the whole login flow to avoid moving one line