diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 06aff74eac8..9e3ce210793 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -7,6 +7,10 @@ _Released 12/2/2025 (PENDING)_
- Improved performance when viewing command snapshots in the Command Log. Element highlighting is now significantly faster, especially when highlighting multiple elements or complex pages. This is achieved by reducing redundant style calculations and batching DOM operations to minimize browser reflows. Addressed in [#32951](https://github.com/cypress-io/cypress/pull/32951).
+**Bugfixes:**
+
+- Fixed an issue where the browser will freeze when Cypress intercepts a synchronous request and a routeHandler is used. Fixes [#32874](https://github.com/cypress-io/cypress/issues/32874). Addressed in [#32925](https://github.com/cypress-io/cypress/pull/32925).
+
## 15.7.0
_Released 11/19/2025_
diff --git a/packages/driver/cypress/e2e/e2e/intercept_sync_request.cy.ts b/packages/driver/cypress/e2e/e2e/intercept_sync_request.cy.ts
new file mode 100644
index 00000000000..cf1d3b67b27
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/intercept_sync_request.cy.ts
@@ -0,0 +1,40 @@
+// https://github.com/cypress-io/cypress/pull/32925
+describe('intercept sync request', () => {
+ it('completes all the way with route handler', () => {
+ cy.intercept('/app', {
+ body: `
+
+
+
+ Sync Request
+
+
+
+
+
+
+ `,
+ })
+
+ cy.intercept('/', () => {})
+ cy.visit('/app')
+ cy.get('#sync-request-button').click()
+ cy.wrap(0).should('eq', 0)
+ })
+})
diff --git a/packages/driver/cypress/e2e/e2e/origin/sync_request_with_cookie.cy.ts b/packages/driver/cypress/e2e/e2e/origin/sync_request_with_cookie.cy.ts
new file mode 100644
index 00000000000..415a93a3fcb
--- /dev/null
+++ b/packages/driver/cypress/e2e/e2e/origin/sync_request_with_cookie.cy.ts
@@ -0,0 +1,45 @@
+// https://github.com/cypress-io/cypress/pull/32925
+describe('Sync Request in cy.origin that sets cookie', () => {
+ it('passes', { browser: '!webkit' }, () => {
+ cy.intercept('https://foo.site.com', {
+ body: `
+
+
+
+ Page 1 / 2
+
+
+ `,
+ })
+
+ cy.visit('https://foo.site.com')
+
+ cy.intercept('https://test.site.com/sync', {
+ headers: {
+ 'set-cookie': 'TEST=foo',
+ },
+ body: '',
+ })
+
+ cy.intercept('https://test.site.com/bar', {
+ body: `
+
+
+
+ Page 2 / 2
+
+
+
+ `,
+ })
+
+ cy.origin('https://test.site.com', () => {
+ cy.visit('https://test.site.com/bar')
+ cy.wrap(0).should('eq', 0)
+ })
+ })
+})
diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts
index f6db7dd58ec..1e919aab643 100644
--- a/packages/driver/src/cross-origin/cypress.ts
+++ b/packages/driver/src/cross-origin/cypress.ts
@@ -22,8 +22,8 @@ import { handleTestEvents } from './events/test'
import { handleMiscEvents } from './events/misc'
import { handleUnsupportedAPIs } from './unsupported_apis'
import { patchFormElementSubmit } from './patches/submit'
-import { patchFetch } from '@packages/runner/injection/patches/fetch'
-import { patchXmlHttpRequest } from '@packages/runner/injection/patches/xmlHttpRequest'
+import { patchFetch } from '@packages/runner/injection/patches/cross-origin/fetch'
+import { patchXmlHttpRequest } from '@packages/runner/injection/patches/cross-origin/xmlHttpRequest'
import $Mocha from '../cypress/mocha'
diff --git a/packages/net-stubbing/lib/server/intercepted-request.ts b/packages/net-stubbing/lib/server/intercepted-request.ts
index 9b80816f54b..bd5ad6105b1 100644
--- a/packages/net-stubbing/lib/server/intercepted-request.ts
+++ b/packages/net-stubbing/lib/server/intercepted-request.ts
@@ -13,6 +13,7 @@ import type { BackendRoute, NetStubbingState } from './types'
import { emit, sendStaticResponse } from './util'
import type CyServer from '@packages/server'
import type { BackendStaticResponse } from '../internal-types'
+import { styleText } from 'util'
export class InterceptedRequest {
id: string
@@ -77,6 +78,14 @@ export class InterceptedRequest {
continue
}
+ // if the request is sync and the route has an interceptor (i.e. routeHandler), then skip the intercept
+ // because the we cannot wait on the before:request event when the sync request is blocking
+ if (this.req.isSyncRequest && route.hasInterceptor) {
+ process.stdout.write(styleText('yellow', `WARNING: sync XHR request was not intercepted for url: ${this.req.proxiedUrl}\n`))
+
+ continue
+ }
+
const subscriptionsByRoute = {
routeId: route.id,
immediateStaticResponse: route.staticResponse,
diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts
index 8eeaebae11b..6620864e53c 100644
--- a/packages/proxy/lib/http/request-middleware.ts
+++ b/packages/proxy/lib/http/request-middleware.ts
@@ -34,11 +34,16 @@ const ExtractCypressMetadataHeaders: RequestMiddleware = function () {
this.req.isAUTFrame = !!this.req.headers['x-cypress-is-aut-frame']
this.req.isFromExtraTarget = !!this.req.headers['x-cypress-is-from-extra-target']
+ this.req.isSyncRequest = !!this.req.headers['x-cypress-is-sync-request']
if (this.req.headers['x-cypress-is-aut-frame']) {
delete this.req.headers['x-cypress-is-aut-frame']
}
+ if (this.req.headers['x-cypress-is-sync-request']) {
+ delete this.req.headers['x-cypress-is-sync-request']
+ }
+
span?.setAttributes({
isAUTFrame: this.req.isAUTFrame,
isFromExtraTarget: this.req.isFromExtraTarget,
diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts
index 930495d1e42..07dfa0999bb 100644
--- a/packages/proxy/lib/http/response-middleware.ts
+++ b/packages/proxy/lib/http/response-middleware.ts
@@ -15,6 +15,7 @@ import { hasServiceWorkerHeader, isVerboseTelemetry as isVerbose } from '.'
import { CookiesHelper } from './util/cookies'
import * as rewriter from './util/rewriter'
import { doesTopNeedToBeSimulated } from './util/top-simulation'
+import { styleText } from 'util'
import type Debug from 'debug'
import type { CookieOptions } from 'express'
@@ -719,6 +720,17 @@ const MaybeCopyCookiesFromIncomingRes: ResponseMiddleware = async function () {
return this.next()
}
+ // if the request is sync, we cannot wait on the cross:origin:cookies:received
+ // event since the sync request is blocking. This means that the cross-origin cookies
+ // may not have been applied.
+ if (this.req.isSyncRequest) {
+ process.stdout.write(styleText('yellow', `WARNING: cross-origin cookies may not have been applied for sync request: ${this.req.proxiedUrl}\n`))
+
+ span?.end()
+
+ return this.next()
+ }
+
// we want to set the cookies via automation so they exist in the browser
// itself. however, firefox will hang if we try to use the extension
// to set cookies on a url that's in-flight, so we send the cookies down to
diff --git a/packages/proxy/lib/types.ts b/packages/proxy/lib/types.ts
index aecbac28a39..598350d4452 100644
--- a/packages/proxy/lib/types.ts
+++ b/packages/proxy/lib/types.ts
@@ -28,6 +28,7 @@ export type CypressIncomingRequest = Request & {
* Stack-ordered list of `cy.intercept()`s matching this request.
*/
matchingRoutes?: BackendRoute[]
+ isSyncRequest: boolean
}
export type RequestCredentialLevel = 'same-origin' | 'include' | 'omit' | boolean
diff --git a/packages/proxy/test/unit/http/request-middleware.spec.ts b/packages/proxy/test/unit/http/request-middleware.spec.ts
index de9e7413d9b..88a07445eba 100644
--- a/packages/proxy/test/unit/http/request-middleware.spec.ts
+++ b/packages/proxy/test/unit/http/request-middleware.spec.ts
@@ -110,6 +110,28 @@ describe('http/request-middleware', () => {
expect(ctx.req.isFromExtraTarget).toBe(false)
})
})
+
+ describe('x-cypress-is-sync-request', () => {
+ it('when it exists, removes header and sets in on the req', async () => {
+ const ctx = prepareContext({
+ 'x-cypress-is-sync-request': 'true',
+ })
+
+ await testMiddleware([ExtractCypressMetadataHeaders], ctx)
+
+ expect(ctx.req.headers!['x-cypress-is-sync-request']).toBeUndefined()
+ expect(ctx.req.isSyncRequest).toBe(true)
+ })
+
+ it('when it does not exist, sets in on the req', async () => {
+ const ctx = prepareContext()
+
+ await testMiddleware([ExtractCypressMetadataHeaders], ctx)
+
+ expect(ctx.req.headers!['x-cypress-is-sync-request']).toBeUndefined()
+ expect(ctx.req.isSyncRequest).toBe(false)
+ })
+ })
})
describe('CalculateCredentialLevelIfApplicable', () => {
diff --git a/packages/runner/injection/cross-origin.js b/packages/runner/injection/cross-origin.js
index 297402360c3..036c8fd5829 100644
--- a/packages/runner/injection/cross-origin.js
+++ b/packages/runner/injection/cross-origin.js
@@ -11,10 +11,10 @@
/* global cypressConfig */
import { createTimers } from './timers'
-import { patchDocumentCookie } from './patches/cookies'
-import { patchElementIntegrity } from './patches/setAttribute'
-import { patchFetch } from './patches/fetch'
-import { patchXmlHttpRequest } from './patches/xmlHttpRequest'
+import { patchDocumentCookie } from './patches/cross-origin/cookies'
+import { patchElementIntegrity } from './patches/cross-origin/setAttribute'
+import { patchFetch } from './patches/cross-origin/fetch'
+import { patchXmlHttpRequest } from './patches/cross-origin/xmlHttpRequest'
const findCypress = () => {
for (let index = 0; index < window.parent.frames.length; index++) {
diff --git a/packages/runner/injection/main.js b/packages/runner/injection/main.js
index ad3eb91de45..d6589401fb1 100644
--- a/packages/runner/injection/main.js
+++ b/packages/runner/injection/main.js
@@ -8,6 +8,7 @@
*/
import { createTimers } from './timers'
+import { patchXmlHttpRequest } from './patches/xmlHttpRequest'
const Cypress = window.Cypress = parent.Cypress
@@ -16,6 +17,8 @@ if (!Cypress) {
Cypress in the parent window but it is missing. This should never happen and likely is a bug. Please open an issue.')
}
+patchXmlHttpRequest(window)
+
// We wrap timers in the injection code because if we do it in the driver (like
// we used to do), any uncaught errors thrown in the timer callbacks would
// get picked up by the top frame's 'error' handler instead of the AUT's.
diff --git a/packages/runner/injection/patches/cookies.ts b/packages/runner/injection/patches/cross-origin/cookies.ts
similarity index 100%
rename from packages/runner/injection/patches/cookies.ts
rename to packages/runner/injection/patches/cross-origin/cookies.ts
diff --git a/packages/runner/injection/patches/fetch.ts b/packages/runner/injection/patches/cross-origin/fetch.ts
similarity index 100%
rename from packages/runner/injection/patches/fetch.ts
rename to packages/runner/injection/patches/cross-origin/fetch.ts
diff --git a/packages/runner/injection/patches/setAttribute.ts b/packages/runner/injection/patches/cross-origin/setAttribute.ts
similarity index 100%
rename from packages/runner/injection/patches/setAttribute.ts
rename to packages/runner/injection/patches/cross-origin/setAttribute.ts
diff --git a/packages/runner/injection/patches/utils/index.ts b/packages/runner/injection/patches/cross-origin/utils/index.ts
similarity index 100%
rename from packages/runner/injection/patches/utils/index.ts
rename to packages/runner/injection/patches/cross-origin/utils/index.ts
diff --git a/packages/runner/injection/patches/cross-origin/xmlHttpRequest.ts b/packages/runner/injection/patches/cross-origin/xmlHttpRequest.ts
new file mode 100644
index 00000000000..19cbc88d6cd
--- /dev/null
+++ b/packages/runner/injection/patches/cross-origin/xmlHttpRequest.ts
@@ -0,0 +1,51 @@
+import { captureFullRequestUrl, requestSentWithCredentials } from './utils'
+
+export const patchXmlHttpRequest = (window: Window) => {
+ // intercept method calls and add cypress headers to determine cookie applications in the proxy
+ // for simulated top
+
+ const originalXmlHttpRequestOpen = window.XMLHttpRequest.prototype.open
+ const originalXmlHttpRequestSend = window.XMLHttpRequest.prototype.send
+
+ window.XMLHttpRequest.prototype.open = function (...args) {
+ try {
+ // since the send method does NOT have access to the arguments passed into open or have the request information,
+ // we need to store a reference here to what we need in the send method
+ this._url = captureFullRequestUrl(args[1], window)
+ } finally {
+ const result = originalXmlHttpRequestOpen.apply(this, args as any)
+
+ if (args.length > 2 && !args[2]) {
+ this.setRequestHeader('x-cypress-is-sync-request', 'true')
+ this._isSyncRequest = true
+ } else {
+ this._isSyncRequest = false
+ }
+
+ return result
+ }
+ }
+
+ window.XMLHttpRequest.prototype.send = function (...args) {
+ // if the request is sync, we cannot wait on the requestSentWithCredentials
+ // function call since the sync request is blocking.
+ if (this._isSyncRequest) {
+ return originalXmlHttpRequestSend.apply(this, args)
+ }
+
+ return (async () => {
+ try {
+ // if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
+ // if the option isn't set, we can imply the default as we know the "resourceType" in the proxy
+ await requestSentWithCredentials({
+ url: this._url,
+ resourceType: 'xhr',
+ credentialStatus: this.withCredentials,
+ })
+ } finally {
+ // if our internal logic errors for whatever reason, do NOT block the end user and continue the request
+ return originalXmlHttpRequestSend.apply(this, args)
+ }
+ })()
+ }
+}
diff --git a/packages/runner/injection/patches/xmlHttpRequest.ts b/packages/runner/injection/patches/xmlHttpRequest.ts
index f1dfdb69b89..1dae08ce479 100644
--- a/packages/runner/injection/patches/xmlHttpRequest.ts
+++ b/packages/runner/injection/patches/xmlHttpRequest.ts
@@ -1,34 +1,13 @@
-import { captureFullRequestUrl, requestSentWithCredentials } from './utils'
-
export const patchXmlHttpRequest = (window: Window) => {
- // intercept method calls and add cypress headers to determine cookie applications in the proxy
- // for simulated top
-
const originalXmlHttpRequestOpen = window.XMLHttpRequest.prototype.open
- const originalXmlHttpRequestSend = window.XMLHttpRequest.prototype.send
window.XMLHttpRequest.prototype.open = function (...args) {
- try {
- // since the send method does NOT have access to the arguments passed into open or have the request information,
- // we need to store a reference here to what we need in the send method
- this._url = captureFullRequestUrl(args[1], window)
- } finally {
- return originalXmlHttpRequestOpen.apply(this, args as any)
- }
- }
+ const result = originalXmlHttpRequestOpen.apply(this, args)
- window.XMLHttpRequest.prototype.send = async function (...args) {
- try {
- // if the option is specified, communicate it to the the server to the proxy can make the request aware if it needs to potentially apply cross origin cookies
- // if the option isn't set, we can imply the default as we know the "resourceType" in the proxy
- await requestSentWithCredentials({
- url: this._url,
- resourceType: 'xhr',
- credentialStatus: this.withCredentials,
- })
- } finally {
- // if our internal logic errors for whatever reason, do NOT block the end user and continue the request
- return originalXmlHttpRequestSend.apply(this, args)
+ if (args.length > 2 && !args[2]) {
+ this.setRequestHeader('x-cypress-is-sync-request', 'true')
}
+
+ return result
}
}