Cypress Detected a Cross Origin Error: How to Fix It

10 min readupdated September 10, 2026

Your test clicks "Sign in with Google", or follows a link to a payment provider, and Cypress stops with a wall of red text:

CypressError: Cypress detected a cross origin error happened on page load:
  > Blocked a frame with origin "http://localhost:3000" from accessing
    a cross-origin frame.

Nothing is broken in your application. Cypress hit a rule the browser enforces on it, and there is a supported way through.

Why this happens

Cypress runs your test inside the browser, in the same window as your app. That is what gives it the time-travel debugger and the automatic waiting. It also means your test code is page code, and page code obeys the browser's rules.

The rule here is the same-origin policy. A page from one origin cannot read or control a page from another origin. An origin is the combination of protocol, domain and port, so all of these are different origins:

http://localhost:3000     vs   https://localhost:3000   (different protocol)
https://app.example.com   vs   https://pay.example.com  (different subdomain)
https://example.com       vs   https://example.com:8080 (different port)

When your test navigates somewhere new, Cypress is now on the wrong side of that boundary and cannot see the page. That is the error.

Selenium and Playwright never show it, because they drive the browser from outside and the policy does not apply to them. This is the clearest example of the tradeoff Cypress makes, and it is covered in Cypress vs Selenium.

The fix: cy.origin()

cy.origin() tells Cypress to run a block of commands against a different origin.

cy.visit('/');
cy.get('[data-testid="pay"]').click(); // sends us to the payment provider

cy.origin('https://pay.example.com', () => {
  cy.get('#card-number').type('4242424242424242');
  cy.contains('button', 'Pay').click();
});

// back on our own origin
cy.get('[data-testid="order-confirmed"]').should('be.visible');

Commands inside the callback run on that origin. Commands after it run on yours again.

The trap that catches everyone: the callback is not a closure

This is the part that produces the confused second search, and it is worth reading carefully.

The callback is stringified, sent to a separate Cypress instance, and evaluated there. The Cypress documentation states it directly: the callback "is not a closure and does not retain access to the JavaScript context in which it was declared".

So this does not work:

const email = 'demo@nimbus.app';

cy.origin('https://auth.example.com', () => {
  cy.get('#email').type(email); // ReferenceError: email is not defined
});

Pass values through the args option instead. The documentation calls it the only mechanism for getting data into the callback:

const sentArgs = { username: 'demo@nimbus.app', password: 'secure123' };

cy.origin(
  'https://auth.example.com',
  { args: sentArgs },
  ({ username, password }) => {
    cy.visit('/login');
    cy.get('input#username').type(username);
    cy.get('input#password').type(password);
    cy.contains('button', 'Login').click();
  },
);

Note the shape: origin first, then the options object, then the callback. Forgetting the middle argument is a common slip.

Three commands you cannot use inside the callback

These throw if you call them in a cy.origin() block:

  • cy.origin() itself, so no nesting.
  • cy.intercept()
  • cy.session()

Set up your intercepts before the cy.origin() block. They keep working across the boundary.

cy.intercept('POST', '**/api/charge').as('charge'); // outside, before

cy.origin('https://pay.example.com', () => {
  cy.contains('button', 'Pay').click();
});

cy.wait('@charge'); // outside, after

Imports do not work either

Inside the callback you cannot use require() or import(). If you need a helper, use Cypress.require(), which needs the experimentalOriginDependencies option turned on.

In practice, most teams avoid this entirely by keeping the callback small and passing plain values through args.

What about chromeWebSecurity: false?

You will find this suggested in a lot of old answers:

// cypress.config.js
module.exports = defineConfig({
  chromeWebSecurity: false,
});

It does work. It lets you navigate to any origin without the error, and it allows access to cross-origin iframes.

Be careful with it, for two reasons.

It only works in Chrome-based browsers, by Cypress's own documentation. Turn it on and your suite quietly stops being runnable in Firefox.

And it turns off a browser security model to make a test pass. Your test is now running in a browser that behaves differently from your users' browsers, which is the opposite of what a test is for.

Use cy.origin() as the default. Keep chromeWebSecurity: false for the cases it genuinely solves, mostly cross-origin iframes, and know what you traded.

The subdomain case

Two subdomains of the same site are still different origins. app.example.com and docs.example.com need cy.origin() just like unrelated domains do.

Older Cypress had an injectDocumentDomain option that relaxed this between subdomains. It is deprecated and will be removed, so do not build on it.

The fix people forget: split the test

Sometimes you do not need cy.origin() at all.

Cypress only complains when a single test crosses origins. Different tests can visit different origins freely. So if your test is really two behaviours stitched together, split it:

it('signs in through the identity provider', () => {
  // everything on auth.example.com
});

it('shows the dashboard for a signed-in user', () => {
  // set the session directly, then test our own app
});

This is usually the better test anyway. A test that spans two products tends to fail for reasons that have nothing to do with your code, and third-party downtime becomes your red build. Fixing flaky tests covers why depending on services you do not control is a losing position.

Should you test the third-party flow at all?

Worth asking before you spend a day on cy.origin().

You do not need to test that Google's login page works. Google tests that. What you need to test is that your application does the right thing with the result.

For most teams the honest answer is:

  • Test your own login form properly, with real assertions.
  • For third-party sign-in, test it once as a smoke check, or set the session directly and skip the provider.
  • Never make the provider a dependency of every test in the suite.

I have watched a team's whole pipeline go red because an identity provider had a slow morning. Nothing they owned was broken, and they could not ship.

Quick reference

Situation What to do
One test visits a second domain cy.origin()
You need variables inside the callback Pass them via { args }
You need cy.intercept() Set it up before the block
Different subdomains Still needs cy.origin()
Cross-origin iframe chromeWebSecurity: false, Chrome only
Two unrelated behaviours Split into two tests
Third-party login in every test Set the session directly instead

Remember this

The error is not a bug. Cypress runs inside the browser, so it obeys the same-origin policy, and cy.origin() is the supported way across.

The one thing to keep in your head: the callback is not a closure. Anything it needs goes through args. That single fact explains most of the confusion after the first fix.

The official guide is cross-origin testing and the cy.origin() API reference.

If cross-origin flows are a regular part of your app, it is worth knowing that Playwright treats them as ordinary navigation. Playwright vs Cypress covers where that difference matters.

Put it into practice

Solve a graded problem: write a test, and we check it would catch a real bug.

Browse the problems →

Learn this in full

Reading about a problem is not the same as fixing one. Pick a lecture or a practice problem and write the test yourself.

Keep reading