diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md
index 6009b776873..4f69396c252 100644
--- a/cli/CHANGELOG.md
+++ b/cli/CHANGELOG.md
@@ -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
diff --git a/packages/data-context/schemas/schema.graphql b/packages/data-context/schemas/schema.graphql
index adf7cfa627d..72dac0a8698 100644
--- a/packages/data-context/schemas/schema.graphql
+++ b/packages/data-context/schemas/schema.graphql
@@ -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
diff --git a/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts b/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts
index ac9394d132a..17323dfcf64 100644
--- a/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts
+++ b/packages/driver/cypress/e2e/commands/net_stubbing.cy.ts
@@ -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: `
+
+
+
+
+
+
+
+
+ `,
+ })
+
+ // 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: `
+
+
+
+
+
+
+
+
+ `,
+ })
+
+ // 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: `
+
+
+
+
+
+
+
+
+ `,
+ })
+
+ // 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[] = []
diff --git a/packages/driver/cypress/e2e/e2e/origin/cookie_misc.cy.ts b/packages/driver/cypress/e2e/e2e/origin/cookie_misc.cy.ts
index e6d9259f279..6676a4ba4c1 100644
--- a/packages/driver/cypress/e2e/e2e/origin/cookie_misc.cy.ts
+++ b/packages/driver/cypress/e2e/e2e/origin/cookie_misc.cy.ts
@@ -38,6 +38,86 @@ 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',
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({ foo: 1, bar: 2 }),
+ }).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)
+
+ cy.getAllCookies().then((cookies) => {
+ const isFirefox = Cypress.isBrowser({ family: 'firefox' })
+ const asyncCookie = {
+ name: 'ASYNC_COOKIE',
+ value: 'async',
+ path: '/',
+ secure: false,
+ hostOnly: true,
+ httpOnly: false,
+ domain: 'www.foobar.com',
+ sameSite: isFirefox ? 'unspecified' : 'lax',
+ }
+
+ const fooBarCookie = {
+ name: 'foo',
+ value: 'bar',
+ path: '/',
+ secure: false,
+ hostOnly: true,
+ httpOnly: false,
+ domain: 'www.foobar.com',
+ sameSite: isFirefox ? 'unspecified' : 'lax',
+ }
+
+ if (isFirefox) {
+ // in Firefox both the foo=bar and ASYNC_COOKIE=async cookies will be set
+ // SYNC_COOKIE=sync is not set because the intercept is not hit
+ expect(cookies).to.have.length(2)
+ expect(cookies[0]).to.deep.equal(fooBarCookie)
+ expect(cookies[1]).to.deep.equal(asyncCookie)
+ } else {
+ // in other browsers 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
+ expect(cookies).to.have.length(1)
+ expect(cookies[0]).to.deep.equal(asyncCookie)
+ }
+ })
+
+ 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)
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/errors/src/errors.ts b/packages/errors/src/errors.ts
index 20ad76079fe..36cc234e40c 100644
--- a/packages/errors/src/errors.ts
+++ b/packages/errors/src/errors.ts
@@ -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 ErrTemplateResult> = AllCypressErrors
diff --git a/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED.ansi b/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED.ansi
new file mode 100644
index 00000000000..44cbfaf2368
--- /dev/null
+++ b/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_APPLIED.ansi
@@ -0,0 +1,2 @@
+[31mWarning: Cookies may not have been applied to synchronous XHR request: [94mhttp://localhost:8080[39m[31m. Learn more: [94mhttps://on.cypress.io/synchronous-xhr-requests[39m[31m[39m
+[31m[39m
\ No newline at end of file
diff --git a/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET.ansi b/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET.ansi
new file mode 100644
index 00000000000..d62125d2ee3
--- /dev/null
+++ b/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_COOKIES_NOT_SET.ansi
@@ -0,0 +1,2 @@
+[31mWarning: Cookies may not have been set for synchronous XHR response: [94mhttp://localhost:8080[39m[31m. Learn more: [94mhttps://on.cypress.io/synchronous-xhr-requests[39m[31m[39m
+[31m[39m
\ No newline at end of file
diff --git a/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED.ansi b/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED.ansi
new file mode 100644
index 00000000000..1558376730f
--- /dev/null
+++ b/packages/errors/test/__snapshots__/SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED.ansi
@@ -0,0 +1,2 @@
+[31mWarning: Synchronous XHR request was not intercepted: [94mhttp://localhost:8080[39m[31m. Learn more: [94mhttps://on.cypress.io/synchronous-xhr-requests[39m[31m[39m
+[31m[39m
\ No newline at end of file
diff --git a/packages/errors/test/visualSnapshotErrors.spec.ts b/packages/errors/test/visualSnapshotErrors.spec.ts
index 3814e874a3c..e477af50707 100644
--- a/packages/errors/test/visualSnapshotErrors.spec.ts
+++ b/packages/errors/test/visualSnapshotErrors.spec.ts
@@ -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'],
+ }
+ },
})
})
diff --git a/packages/net-stubbing/lib/server/intercepted-request.ts b/packages/net-stubbing/lib/server/intercepted-request.ts
index 9b80816f54b..bf9a6933889 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 * as errors from '@packages/errors'
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) {
+ errors.warning('SYNCHRONOUS_XHR_REQUEST_NOT_INTERCEPTED', this.req.proxiedUrl)
+
+ continue
+ }
+
const subscriptionsByRoute = {
routeId: route.id,
immediateStaticResponse: route.staticResponse,
diff --git a/packages/net-stubbing/package.json b/packages/net-stubbing/package.json
index 8c475e5cf7b..5dec8ed3396 100644
--- a/packages/net-stubbing/package.json
+++ b/packages/net-stubbing/package.json
@@ -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",
diff --git a/packages/proxy/lib/http/request-middleware.ts b/packages/proxy/lib/http/request-middleware.ts
index 8eeaebae11b..6fff486e343 100644
--- a/packages/proxy/lib/http/request-middleware.ts
+++ b/packages/proxy/lib/http/request-middleware.ts
@@ -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
@@ -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,
@@ -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)
diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts
index 930495d1e42..83ab30d975c 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 * as errors from '@packages/errors'
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) {
+ 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
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
}
}
diff --git a/system-tests/__snapshots__/xhr_spec.js b/system-tests/__snapshots__/xhr_spec.js
index 8d6f78d2c3f..a426ab38ed0 100644
--- a/system-tests/__snapshots__/xhr_spec.js
+++ b/system-tests/__snapshots__/xhr_spec.js
@@ -24,21 +24,28 @@ exports['e2e xhr / passes in global mode'] = `
✓ does not inject into json's contents from file server even requesting text/html
✓ works prior to visit
✓ can stub a 100kb response
+Warning: Cookies may not have been applied to synchronous XHR request: http://www.foobar.com:1919/json. Learn more: https://on.cypress.io/synchronous-xhr-requests
+
+Warning: Synchronous XHR request was not intercepted: http://www.foobar.com:1919/json. Learn more: https://on.cypress.io/synchronous-xhr-requests
+
+Warning: Cookies may not have been set for synchronous XHR response: http://www.foobar.com:1919/json. Learn more: https://on.cypress.io/synchronous-xhr-requests
+
+ ✓ displays warnings in the terminal when using sync XHR requests
server with 1 visit
✓ response body
✓ request body
- aborts
- 8 passing
+ 9 passing
1 pending
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ Tests: 9 │
- │ Passing: 8 │
+ │ Tests: 10 │
+ │ Passing: 9 │
│ Failing: 0 │
│ Pending: 1 │
│ Skipped: 0 │
@@ -56,9 +63,9 @@ exports['e2e xhr / passes in global mode'] = `
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ ✔ xhr.cy.js XX:XX 9 8 - 1 - │
+ │ ✔ xhr.cy.js XX:XX 10 9 - 1 - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
- ✔ All specs passed! XX:XX 9 8 - 1 -
+ ✔ All specs passed! XX:XX 10 9 - 1 -
`
@@ -89,21 +96,28 @@ exports['e2e xhr / passes through CLI'] = `
✓ does not inject into json's contents from file server even requesting text/html
✓ works prior to visit
✓ can stub a 100kb response
+Warning: Cookies may not have been applied to synchronous XHR request: http://www.foobar.com:1919/json. Learn more: https://on.cypress.io/synchronous-xhr-requests
+
+Warning: Synchronous XHR request was not intercepted: http://www.foobar.com:1919/json. Learn more: https://on.cypress.io/synchronous-xhr-requests
+
+Warning: Cookies may not have been set for synchronous XHR response: http://www.foobar.com:1919/json. Learn more: https://on.cypress.io/synchronous-xhr-requests
+
+ ✓ displays warnings in the terminal when using sync XHR requests
server with 1 visit
✓ response body
✓ request body
- aborts
- 8 passing
+ 9 passing
1 pending
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ Tests: 9 │
- │ Passing: 8 │
+ │ Tests: 10 │
+ │ Passing: 9 │
│ Failing: 0 │
│ Pending: 1 │
│ Skipped: 0 │
@@ -121,9 +135,9 @@ exports['e2e xhr / passes through CLI'] = `
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ ✔ xhr.cy.js XX:XX 9 8 - 1 - │
+ │ ✔ xhr.cy.js XX:XX 10 9 - 1 - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
- ✔ All specs passed! XX:XX 9 8 - 1 -
+ ✔ All specs passed! XX:XX 10 9 - 1 -
`
diff --git a/system-tests/projects/e2e/cypress/e2e/xhr.cy.js b/system-tests/projects/e2e/cypress/e2e/xhr.cy.js
index 17d5d773115..fcab5642f05 100644
--- a/system-tests/projects/e2e/cypress/e2e/xhr.cy.js
+++ b/system-tests/projects/e2e/cypress/e2e/xhr.cy.js
@@ -122,6 +122,28 @@ describe('xhrs', () => {
})
})
+ it('displays warnings in the terminal when using sync XHR requests', () => {
+ // this intercept won't get hit because the request is sync
+ cy.intercept('http://www.foobar.com:1919/json', (req) => {
+ req.reply({
+ body: '',
+ })
+ })
+
+ cy.origin('http://www.foobar.com:1919', () => {
+ cy.visit('/')
+ cy.window().then((win) => {
+ const xhr = new win.XMLHttpRequest
+
+ // create a sync request
+ xhr.open('GET', '/json', false)
+ xhr.send()
+
+ expect(JSON.parse(xhr.response)).to.deep.eq({ content: 'json' })
+ })
+ })
+ })
+
describe('server with 1 visit', () => {
beforeEach(() => {
cy.visit('/xhr.html')
diff --git a/system-tests/test/xhr_spec.js b/system-tests/test/xhr_spec.js
index f5b753899ca..80f49711ba3 100644
--- a/system-tests/test/xhr_spec.js
+++ b/system-tests/test/xhr_spec.js
@@ -15,9 +15,15 @@ const onServer = function (app) {
})
})
- return app.post('/html', (req, res) => {
+ app.post('/html', (req, res) => {
return res.json({ content: 'content' })
})
+
+ app.get('/json', (req, res) => {
+ res.setHeader('Set-Cookie', 'foo=bar')
+
+ return res.json({ content: 'json' })
+ })
}
describe('e2e xhr', () => {
@@ -26,6 +32,12 @@ describe('e2e xhr', () => {
port: 1919,
onServer,
},
+ settings: {
+ hosts: {
+ '*.foobar.com': '127.0.0.1',
+ },
+ e2e: {},
+ },
})
systemTests.it('passes in global mode', {