Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dc16aef
fix: Browser will freeze when sync request is intercepted
alexsch01 Nov 10, 2025
9182b4e
Update CHANGELOG.md
alexsch01 Nov 10, 2025
4801766
Update CHANGELOG.md
alexsch01 Nov 10, 2025
7b4f2e7
prevent cross origin cookies from breaking sync requests
alexsch01 Nov 11, 2025
02f6105
Update xmlHttpRequest.ts
alexsch01 Nov 12, 2025
7f0d6c6
Update xmlHttpRequest.ts
alexsch01 Nov 12, 2025
12a25e4
fix lint
alexsch01 Nov 12, 2025
6a21ef2
Merge branch 'develop' into fix-browser-freeze
alexsch01 Nov 14, 2025
a837527
Update intercepted-request.ts
alexsch01 Nov 18, 2025
3cdaa5f
Update response-middleware.ts
alexsch01 Nov 18, 2025
c27f137
Merge branch 'develop' into fix-browser-freeze
alexsch01 Nov 18, 2025
247da30
grammar fix
alexsch01 Nov 18, 2025
6664756
better warning
alexsch01 Nov 18, 2025
a9e67e1
Blank
alexsch01 Nov 18, 2025
5a65b0c
Update intercepted-request.ts
alexsch01 Nov 18, 2025
7d4a48b
Update response-middleware.ts
alexsch01 Nov 18, 2025
88c83e1
Update xmlHttpRequest.ts
alexsch01 Nov 18, 2025
29b4dc7
Merge branch 'develop' into fix-browser-freeze
jennifer-shehane Nov 19, 2025
775edee
Alexsch01 patch 1
alexsch01 Nov 19, 2025
844904d
Update xmlHttpRequest.ts
alexsch01 Nov 19, 2025
be6a22f
Update main.js
alexsch01 Nov 19, 2025
cda831d
this is correct now
alexsch01 Nov 20, 2025
f846e8e
move sync intercept and move patches
mschile Nov 20, 2025
da1d38f
Merge branch 'develop' into fix-browser-freeze
jennifer-shehane Nov 20, 2025
931b4fe
add unit test for proxy
alexsch01 Nov 21, 2025
82707fc
Create intercept_sync_request.cy.ts
alexsch01 Nov 21, 2025
cfabc45
Create sync_request_with_cookie.cy.ts
alexsch01 Nov 21, 2025
ad809b6
lint
alexsch01 Nov 21, 2025
48807a6
Update intercept_sync_request.cy.ts
alexsch01 Nov 22, 2025
8adc9d1
Update intercept_sync_request.cy.ts
alexsch01 Nov 22, 2025
39a2fa3
Update sync_request_with_cookie.cy.ts
alexsch01 Nov 22, 2025
5645c1a
test updates
mschile Dec 2, 2025
9317f69
test updates
mschile Dec 2, 2025
ef57a66
Merge branch 'develop' into fix-browser-freeze
mschile Dec 2, 2025
e1f4ca0
adding system test
mschile Dec 2, 2025
4bb4ec4
update warnings
mschile Dec 2, 2025
137b3c3
Merge branch 'develop' into fix-browser-freeze
mschile Dec 2, 2025
a7fbcc6
fix lint
mschile Dec 2, 2025
d4f7972
update to use errors package
mschile Dec 3, 2025
af024cb
Merge branch 'develop' into fix-browser-freeze
mschile Dec 3, 2025
eded2cf
Merge branch 'develop' into fix-browser-freeze
mschile Dec 4, 2025
601615f
test update
mschile Dec 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ _Released 12/16/2025 (PENDING)_
**Bugfixes:**

- Fixed an issue where a EPIPE error shows up after CTRL+C is done in terminal. Fixes [#30659](https://github.com/cypress-io/cypress/issues/30659). Addressed in [#32873](https://github.com/cypress-io/cypress/pull/32873).
- Fixed an issue where the browser would freeze when Cypress intercepts a synchronous XHR 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.1

Expand Down
3 changes: 3 additions & 0 deletions packages/data-context/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,9 @@ enum ErrorTypeEnum {
SETUP_NODE_EVENTS_INVALID_EVENT_NAME_ERROR
SETUP_NODE_EVENTS_IS_NOT_FUNCTION
SUPPORT_FILE_NOT_FOUND
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET
SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED
TESTING_TYPE_NOT_CONFIGURED
TESTS_DID_NOT_START_FAILED
TESTS_DID_NOT_START_RETRYING
Expand Down
141 changes: 141 additions & 0 deletions packages/driver/cypress/e2e/commands/net_stubbing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,147 @@ describe('network stubbing', { retries: 15 }, function () {
})
})

it('does not intercept an XHR sync request with a route handler', () => {
cy.intercept('/', {
body: `
<!DOCTYPE html>
<html>
<body>
<div id="sync_response"></div>
<div id="async_response"></div>
<script>
const xhr = new window.XMLHttpRequest()
xhr.open('GET', '/fixtures/valid.json', false)
xhr.send()
document.querySelector('#sync_response').innerHTML = JSON.stringify(JSON.parse(xhr.responseText))

const xhr2 = new window.XMLHttpRequest()
xhr2.open('GET', '/async', true)
xhr2.onload = () => {
document.querySelector('#async_response').innerHTML = xhr2.responseText
}
xhr2.send()
</script>
</body>
</html>
`,
})

// this intercept won't get hit because the request is sync
cy.intercept('/fixtures/valid.json', () => {}).as('sync')

// this intercept will get hit because the request is async
cy.intercept('/async', (req) => {
req.reply({ body: 'async' })
}).as('async')

cy.visit('/')
cy.get('#sync_response').should('contain', '{"foo":1,"bar":{"baz":"cypress"}}')
cy.get('#async_response').should('contain', 'async')
cy.wait('@async')
})

it('does not intercept a cross-origin XHR sync request with a route handler', { browser: '!webkit' }, () => {
cy.intercept('http://www.foobar.com:3500/', {
body: `
<!DOCTYPE html>
<html>
<body>
<div id="sync_response"></div>
<div id="async_response"></div>
<script>
const xhr = new window.XMLHttpRequest()
xhr.open('GET', '/fixtures/valid.json', false)
xhr.send()
document.querySelector('#sync_response').innerHTML = JSON.stringify(JSON.parse(xhr.responseText))

const xhr2 = new window.XMLHttpRequest()
xhr2.open('GET', '/async', true)
xhr2.onload = () => {
document.querySelector('#async_response').innerHTML = xhr2.responseText
}
xhr2.send()
</script>
</body>
</html>
`,
})

// this intercept won't get hit because the request is sync
cy.intercept('http://www.foobar.com:3500/fixtures/valid.json', (req) => {
req.reply({ body: '' })
})

// this intercept will get hit because the request is async
cy.intercept('http://www.foobar.com:3500/async', (req) => {
req.reply({ body: 'async' })
}).as('async')

cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/')
cy.get('#sync_response').should('contain', '{"foo":1,"bar":{"baz":"cypress"}}')
cy.get('#async_response').should('contain', 'async')
})

cy.wait('@async')
})

it('intercepts a sync XHR request from a Worker with a route handler', () => {
cy.intercept('/', {
body: `
<!DOCTYPE html>
<html>
<body>
<div id="sync_response"></div>
<div id="async_response"></div>
<script>
const workerScript = \`
const xhr = new XMLHttpRequest()
xhr.open('GET', 'http://localhost:3500/fixtures/valid.json', false)
xhr.send()
postMessage({ type: 'sync', data: JSON.stringify(JSON.parse(xhr.responseText)) })

const xhr2 = new XMLHttpRequest()
xhr2.open('GET', 'http://localhost:3500/async', true)
xhr2.onload = () => {
postMessage({ type: 'async', data: xhr2.responseText })
}
xhr2.send()
\`
const blob = new Blob([workerScript], { type: 'application/javascript' })
const workerURL = URL.createObjectURL(blob)
const worker = new Worker(workerURL)

worker.onmessage = (m) => {
if (m.data.type === 'sync') {
document.querySelector('#sync_response').innerHTML = m.data.data
} else if (m.data.type === 'async') {
document.querySelector('#async_response').innerHTML = m.data.data
}
}
</script>
</body>
</html>
`,
})

// this intercept will get hit because the sync XHR request is sent from the worker
cy.intercept('http://localhost:3500/fixtures/valid.json', (req) => {
req.reply({ body: '{"foo":"bar"}' })
}).as('sync')

// this intercept will get hit because the request is async
cy.intercept('http://localhost:3500/async', (req) => {
req.reply({ body: 'async' })
}).as('async')

cy.visit('/')
cy.get('#sync_response').should('contain', '{"foo":"bar"}')
cy.get('#async_response').should('contain', 'async')
cy.wait('@sync')
cy.wait('@async')
})

context('overrides', function () {
it('chains middleware with string matcher as expected', function () {
const e: string[] = []
Expand Down
57 changes: 57 additions & 0 deletions packages/driver/cypress/e2e/e2e/origin/cookie_misc.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,63 @@ describe('misc cookie tests', { browser: '!webkit' }, () => {
})
})

it('cookies are not set for XHR sync requests', { browser: '!webkit' }, () => {
// this intercept won't get hit because the request is sync
cy.intercept('http://www.foobar.com:3500/set-cookie*', (req) => {
req.reply({
headers: {
'set-cookie': 'SYNC_COOKIE=sync',
},
body: '',
})
})

cy.intercept('http://www.foobar.com:3500/async', {
headers: {
'set-cookie': 'ASYNC_COOKIE=async',
},
body: '',
}).as('async')

cy.visit('http://localhost:3500/fixtures/empty.html')
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('http://www.foobar.com:3500/')
cy.window().then((win) => {
const xhr = new win.XMLHttpRequest()

xhr.open('GET', '/set-cookie?cookie=foo=bar', false)
xhr.send()

const xhr2 = new win.XMLHttpRequest()

xhr2.open('GET', '/async', true)
xhr2.send()
})
})

// cy.getAllCookies does not wait for the cookies to be set, so we need to wait manually
cy.wait(500)

// only the ASYNC_COOKIE=async cookie will be set
// SYNC_COOKIE=sync is not set because the intercept is not hit
// foo=bar is not set because the request is sync and we are not able to sync the cookie with the automation
cy.getAllCookies().then((cookies) => {
expect(cookies).to.have.length(1)
expect(cookies[0]).to.deep.equal({
name: 'ASYNC_COOKIE',
value: 'async',
domain: 'www.foobar.com',
path: '/',
httpOnly: false,
hostOnly: true,
secure: false,
sameSite: 'lax',
})
})

cy.wait('@async')
})

/**
* FIXES:
* https://github.com/cypress-io/cypress/issues/25205 (cookies set with expired time with value deleted show up as set with value deleted)
Expand Down
4 changes: 2 additions & 2 deletions packages/driver/src/cross-origin/cypress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
15 changes: 15 additions & 0 deletions packages/errors/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,21 @@ export const AllCypressErrors = {
${fmt.highlightSecondary(error)}
`
},
SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED: (url: string) => {
return errTemplate`\
Warning: Synchronous XHR request was not intercepted: ${fmt.url(url)}. Learn more: ${fmt.url('https://on.cypress.io/synchronous-xhr-requests')}
`
},
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED: (url: string) => {
return errTemplate`\
Warning: Cookies may not have been applied to synchronous XHR request: ${fmt.url(url)}. Learn more: ${fmt.url('https://on.cypress.io/synchronous-xhr-requests')}
`
},
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET: (url: string) => {
return errTemplate`\
Warning: Cookies may not have been set for synchronous XHR response: ${fmt.url(url)}. Learn more: ${fmt.url('https://on.cypress.io/synchronous-xhr-requests')}
`
},
} as const

const _typeCheck: Record<keyof AllCypressErrorObj, (...args: any[]) => ErrTemplateResult> = AllCypressErrors
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Warning: Cookies may not have been applied to synchronous XHR request: http://localhost:8080. Learn more: https://on.cypress.io/synchronous-xhr-requests

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Warning: Cookies may not have been set for synchronous XHR response: http://localhost:8080. Learn more: https://on.cypress.io/synchronous-xhr-requests

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Warning: Synchronous XHR request was not intercepted: http://localhost:8080. Learn more: https://on.cypress.io/synchronous-xhr-requests

15 changes: 15 additions & 0 deletions packages/errors/test/visualSnapshotErrors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1121,5 +1121,20 @@ describe('visual error templates', () => {
default: [],
}
},
SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED: () => {
return {
default: ['http://localhost:8080'],
}
},
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED: () => {
return {
default: ['http://localhost:8080'],
}
},
SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET: () => {
return {
default: ['http://localhost:8080'],
}
},
})
})
9 changes: 9 additions & 0 deletions packages/net-stubbing/lib/server/intercepted-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 * as errors from '@packages/errors'

export class InterceptedRequest {
id: string
Expand Down Expand Up @@ -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) {
errors.warning('SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED', this.req.proxiedUrl)

continue
}

const subscriptionsByRoute = {
routeId: route.id,
immediateStaticResponse: route.staticResponse,
Expand Down
1 change: 1 addition & 0 deletions packages/net-stubbing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"throttle": "^1.0.3"
},
"devDependencies": {
"@packages/errors": "0.0.0-development",
"@packages/network": "0.0.0-development",
"@packages/proxy": "0.0.0-development",
"@packages/server": "0.0.0-development",
Expand Down
10 changes: 10 additions & 0 deletions packages/proxy/lib/http/request-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { doesTopNeedToBeSimulated } from './util/top-simulation'
import type { HttpMiddleware } from './'
import type { CypressIncomingRequest } from '../types'
import { urlMatchesOriginProtectionSpace } from '@packages/network-tools'
import * as errors from '@packages/errors'

// do not use a debug namespace in this file - use the per-request `this.debug` instead
// available as cypress-verbose:proxy:http
Expand All @@ -34,11 +35,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,
Expand Down Expand Up @@ -220,6 +226,10 @@ const MaybeAttachCrossOriginCookies: RequestMiddleware = function () {
return this.next()
}

if (this.req.isSyncRequest) {
errors.warning('SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED', this.req.proxiedUrl)
}

// Top needs to be simulated since the AUT is in a cross origin state. Get the "requested with" and credentials and see what cookies need to be attached
const currentAUTUrl = this.getAUTUrl()
const shouldCookiesBeAttachedToRequest = shouldAttachAndSetCookies(this.req.proxiedUrl, currentAUTUrl, this.req.resourceType, this.req.credentialsLevel, this.req.isAUTFrame)
Expand Down
12 changes: 12 additions & 0 deletions packages/proxy/lib/http/response-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 * as errors from '@packages/errors'

import type Debug from 'debug'
import type { CookieOptions } from 'express'
Expand Down Expand Up @@ -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) {
errors.warning('SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET', this.req.proxiedUrl)

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
Expand Down
1 change: 1 addition & 0 deletions packages/proxy/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading