perf: fix proxy correlation timeout issues (#28751)

This commit is contained in:
Matt Schile
2024-01-22 15:35:49 -07:00
committed by GitHub
parent 152b3555b8
commit c672581a71
17 changed files with 406 additions and 111 deletions

View File

@@ -29,9 +29,7 @@ mainBuildFilters: &mainBuildFilters
- develop
- /^release\/\d+\.\d+\.\d+$/
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- 'fix/set_module_resolution_with_commonjs'
- 'publish-binary'
- 'em/circle2'
- 'update-v8-snapshot-cache-on-develop'
# usually we don't build Mac app - it takes a long time
# but sometimes we want to really confirm we are doing the right thing
@@ -42,8 +40,7 @@ macWorkflowFilters: &darwin-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'fix/set_module_resolution_with_commonjs', << pipeline.git.branch >> ]
- equal: [ 'ryanm/fix/service-worker-capture', << pipeline.git.branch >> ]
- equal: [ 'mschile/protocol/proxy_correlation', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -54,8 +51,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'fix/set_module_resolution_with_commonjs', << pipeline.git.branch >> ]
- equal: [ 'em/circle2', << pipeline.git.branch >> ]
- equal: [ 'mschile/protocol/proxy_correlation', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -78,9 +74,7 @@ windowsWorkflowFilters: &windows-workflow-filters
- equal: [ develop, << pipeline.git.branch >> ]
# use the following branch as well to ensure that v8 snapshot cache updates are fully tested
- equal: [ 'update-v8-snapshot-cache-on-develop', << pipeline.git.branch >> ]
- equal: [ 'fix/set_module_resolution_with_commonjs', << pipeline.git.branch >> ]
- equal: [ 'lerna-optimize-tasks', << pipeline.git.branch >> ]
- equal: [ 'mschile/mochaEvents_win_sep', << pipeline.git.branch >> ]
- equal: [ 'mschile/protocol/proxy_correlation', << pipeline.git.branch >> ]
- matches:
pattern: /^release\/\d+\.\d+\.\d+$/
value: << pipeline.git.branch >>
@@ -150,7 +144,7 @@ commands:
name: Set environment variable to determine whether or not to persist artifacts
command: |
echo "Setting SHOULD_PERSIST_ARTIFACTS variable"
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "publish-binary" && "$CIRCLE_BRANCH" != "fix/set_module_resolution_with_commonjs" ]]; then
echo 'if ! [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "mschile/protocol/proxy_correlation" ]]; then
export SHOULD_PERSIST_ARTIFACTS=true
fi' >> "$BASH_ENV"
# You must run `setup_should_persist_artifacts` command and be using bash before running this command

View File

@@ -1,4 +1,10 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.6.4
**Performance:**
- Fixed a performance regression from [`13.3.2`](https://docs.cypress.io/guides/references/changelog#13.3.2) where aborted requests may not correlate correctly. Fixes [#28734](https://github.com/cypress-io/cypress/issues/28734).
## 13.6.3
_Released 1/16/2024_

View File

@@ -2779,37 +2779,15 @@ describe('src/cy/commands/navigation', () => {
})
context('resets state', () => {
context('test isolation on', { testIsolation: true }, () => {
it('resets the server state', () => {
cy.stub(Cypress, 'backend').log(false).callThrough()
it('resets the server state', () => {
cy.stub(Cypress, 'backend').log(false).callThrough()
Cypress.emitThen('test:before:run:async', {
id: 'r1',
currentRetry: 1,
})
.then(() => {
expect(Cypress.backend).to.be.calledWith(
'reset:server:state',
{ testIsolation: true },
)
})
Cypress.emitThen('test:before:run:async', {
id: 'r1',
currentRetry: 1,
})
})
context('test isolation off', { testIsolation: false }, () => {
it('resets the server state', () => {
cy.stub(Cypress, 'backend').log(false).callThrough()
Cypress.emitThen('test:before:run:async', {
id: 'r1',
currentRetry: 1,
})
.then(() => {
expect(Cypress.backend).to.be.calledWith(
'reset:server:state',
{ testIsolation: false },
)
})
.then(() => {
expect(Cypress.backend).to.be.calledWith('reset:server:state')
})
})
})

View File

@@ -1,38 +0,0 @@
// https://github.com/cypress-io/cypress/issues/28545
describe('lots of requests', () => {
beforeEach(() => {
cy.intercept('/lots-of-requests', (req) => {
req.reply(
`<html>
<head>
<title>Lots of Requests</title>
<script>
for (let i = 0; i < 50; i++) {
fetch('/1mb?i=' + i + '&ts=' + Date.now())
}
</script>
<script>
fetch('/1mb?i=last&ts=' + Date.now())
.then((response) => {
const html = '<div id="done">Done</div>'
document.body.insertAdjacentHTML('beforeend', html)
})
</script>
</head>
<body></body>
</html>`,
)
})
})
describe('test isolation off', { testIsolation: false }, () => {
it('test 1', () => {
cy.visit('http://localhost:3500/lots-of-requests')
})
it('test 2', () => {
cy.get('#done').should('contain', 'Done')
})
})
})

View File

@@ -452,7 +452,7 @@ export default (Commands, Cypress, cy, state, config) => {
// reset any state on the backend
// TODO: this is a bug in e2e it needs to be returned
return Cypress.backend('reset:server:state', { testIsolation: Cypress.config('testIsolation') })
return Cypress.backend('reset:server:state')
})
Cypress.on('test:before:run', reset)

View File

@@ -371,10 +371,14 @@ export class Http {
}
ctx.error = error
if (ctx.req.browserPreRequest && !ctx.req.browserPreRequest.errorHandled) {
// if there is a pre-request and the error has not been handled and the response has not been destroyed
// (which implies the request was canceled by the browser), try to re-use the pre-request for the next retry
//
// browsers will retry requests in the event of network errors, but they will not send pre-requests,
// so try to re-use the current browserPreRequest for the next retry after incrementing the ID.
if (ctx.req.browserPreRequest && !ctx.req.browserPreRequest.errorHandled && !ctx.res.destroyed) {
ctx.req.browserPreRequest.errorHandled = true
// browsers will retry requests in the event of network errors, but they will not send pre-requests,
// so try to re-use the current browserPreRequest for the next retry after incrementing the ID.
const preRequest = {
...ctx.req.browserPreRequest,
requestId: getUniqueRequestId(ctx.req.browserPreRequest.requestId),
@@ -448,15 +452,12 @@ export class Http {
}
}
reset (options: { resetPreRequests: boolean, resetBetweenSpecs: boolean }) {
reset (options: { resetBetweenSpecs: boolean }) {
this.buffers.reset()
this.setAUTUrl(undefined)
if (options.resetPreRequests) {
this.preRequests.reset()
}
if (options.resetBetweenSpecs) {
this.preRequests.reset()
this.serviceWorkerManager = new ServiceWorkerManager()
}
}
@@ -477,6 +478,10 @@ export class Http {
this.preRequests.removePendingPreRequest(requestId)
}
getPendingBrowserPreRequests () {
return this.preRequests.pendingPreRequests
}
addPendingUrlWithoutPreRequest (url: string) {
this.preRequests.addPendingUrlWithoutPreRequest(url)
}

View File

@@ -100,13 +100,25 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
shouldCorrelatePreRequest: shouldCorrelatePreRequests,
})
if (!this.shouldCorrelatePreRequests()) {
if (!shouldCorrelatePreRequests) {
span?.end()
return this.next()
}
const onClose = () => {
// if we haven't matched a browser pre-request and the request has been destroyed, raise an error
if (this.req.destroyed) {
span?.end()
this.reqMiddlewareSpan?.end()
this.onError(new Error('request destroyed before browser pre-request was received'))
}
}
const copyResourceTypeAndNext = () => {
this.res.off('close', onClose)
this.req.resourceType = this.req.browserPreRequest?.resourceType
span?.setAttributes({
@@ -144,6 +156,8 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
return copyResourceTypeAndNext()
}
this.res.once('close', onClose)
this.debug('waiting for prerequest')
this.pendingRequest = this.getPreRequest((({ browserPreRequest, noPreRequestExpected }) => {
this.req.browserPreRequest = browserPreRequest

View File

@@ -275,6 +275,7 @@ export class PreRequests {
proxyRequestReceivedTimestamp: performance.now() + performance.timeOrigin,
timeout: setTimeout(() => {
ctxDebug('Never received pre-request or url without pre-request for request %s after waiting %sms. Continuing without one.', key, this.requestTimeout)
debug('Never received pre-request or url without pre-request for request %s after waiting %sms. Continuing without one.', key, this.requestTimeout)
metrics.unmatchedRequests++
pendingRequest.timedOut = true
callback({
@@ -308,7 +309,10 @@ export class PreRequests {
this.pendingPreRequests = new QueueMap<PendingPreRequest>()
// Clear out the pending requests timeout callbacks first then clear the queue
this.pendingRequests.forEach(({ callback, timeout }) => {
this.pendingRequests.forEach(({ callback, timeout, timedOut }) => {
// If the request has already timed out, just return
if (timedOut) return
clearTimeout(timeout)
metrics.unmatchedRequests++
callback?.({

View File

@@ -18,6 +18,10 @@ export class NetworkProxy {
this.http.removePendingBrowserPreRequest(requestId)
}
getPendingBrowserPreRequests () {
return this.http.getPendingBrowserPreRequests()
}
addPendingUrlWithoutPreRequest (url: string) {
this.http.addPendingUrlWithoutPreRequest(url)
}
@@ -59,7 +63,7 @@ export class NetworkProxy {
this.http.setBuffer(buffer)
}
reset (options: { resetPreRequests: boolean, resetBetweenSpecs: boolean } = { resetPreRequests: true, resetBetweenSpecs: false }) {
reset (options: { resetBetweenSpecs: boolean } = { resetBetweenSpecs: false }) {
this.http.reset(options)
}

View File

@@ -80,6 +80,44 @@ describe('http', function () {
})
})
it('creates fake pending browser pre request', function () {
incomingRequest.callsFake(function () {
this.req.browserPreRequest = {
requestId: '1234',
errorHandled: false,
}
this.res.destroyed = false
throw new Error('oops')
})
error.callsFake(function () {
expect(this.error.message).to.eq('Internal error while proxying "GET url" in 0:\noops')
this.end()
})
const http = new Http(httpOpts)
http.addPendingBrowserPreRequest = sinon.stub()
return http
// @ts-expect-error
.handleHttpRequest({ method: 'GET', proxiedUrl: 'url' }, { on, off })
.then(function () {
expect(incomingRequest).to.be.calledOnce
expect(incomingResponse).to.not.be.called
expect(error).to.be.calledOnce
expect(http.addPendingBrowserPreRequest).to.be.calledOnceWith({
requestId: '1234-retry-1',
errorHandled: false,
})
expect(on).to.not.be.called
expect(off).to.be.calledThrice
})
})
it('ensures not to create fake pending browser pre requests on multiple errors', function () {
incomingRequest.callsFake(function () {
this.req.browserPreRequest = {
@@ -111,6 +149,39 @@ describe('http', function () {
})
})
it('does not create fake pending browser pre request when the response is destroyed', function () {
incomingRequest.callsFake(function () {
this.req.browserPreRequest = {
errorHandled: false,
}
this.res.destroyed = true
throw new Error('oops')
})
error.callsFake(function () {
expect(this.error.message).to.eq('Internal error while proxying "GET url" in 0:\noops')
this.end()
})
const http = new Http(httpOpts)
http.addPendingBrowserPreRequest = sinon.stub()
return http
// @ts-expect-error
.handleHttpRequest({ method: 'GET', proxiedUrl: 'url' }, { on, off })
.then(function () {
expect(incomingRequest).to.be.calledOnce
expect(incomingResponse).to.not.be.called
expect(error).to.be.calledOnce
expect(http.addPendingBrowserPreRequest).to.not.be.called
expect(on).to.not.be.called
expect(off).to.be.calledThrice
})
})
it('moves to Error stack if err in IncomingResponse', function () {
incomingRequest.callsFake(function () {
this.incomingRes = {}
@@ -214,22 +285,22 @@ describe('http', function () {
httpOpts = { config: {}, middleware: {} }
})
it('resets preRequests when resetPreRequests is true', function () {
it('resets preRequests when resetBetweenSpecs is true', function () {
const http = new Http(httpOpts)
http.preRequests.reset = sinon.stub()
http.reset({ resetPreRequests: true, resetBetweenSpecs: false })
http.reset({ resetBetweenSpecs: true })
expect(http.preRequests.reset).to.be.calledOnce
})
it('does not reset preRequests when resetPreRequests is false', function () {
it('does not reset preRequests when resetBetweenSpecs is false', function () {
const http = new Http(httpOpts)
http.preRequests.reset = sinon.stub()
http.reset({ resetPreRequests: false, resetBetweenSpecs: false })
http.reset({ resetBetweenSpecs: false })
expect(http.preRequests.reset).to.not.be.called
})

View File

@@ -738,6 +738,108 @@ describe('http/request-middleware', () => {
})
})
describe('CorrelateBrowserPreRequest', () => {
const { CorrelateBrowserPreRequest } = RequestMiddleware
it('skips if shouldCorrelatePreRequests returns false', async () => {
const ctx = {
res: {
off: sinon.stub(),
},
shouldCorrelatePreRequests: () => false,
getPreRequest: sinon.stub(),
}
await testMiddleware([CorrelateBrowserPreRequest], ctx)
.then(() => {
expect(ctx.getPreRequest).not.to.be.called
})
})
it('sets browserPreRequest on the request', async () => {
const browserPreRequest = sinon.stub()
const ctx = {
req: {
proxiedUrl: 'https://www.cypress.io/',
browserPreRequest: undefined,
headers: [],
},
res: {
off: sinon.stub(),
once: sinon.stub(),
},
shouldCorrelatePreRequests: () => true,
getPreRequest: sinon.stub().yields({
browserPreRequest,
}),
}
await testMiddleware([CorrelateBrowserPreRequest], ctx)
.then(() => {
expect(ctx.getPreRequest).to.be.calledOnce
expect(ctx.req.browserPreRequest).to.equal(browserPreRequest)
expect(ctx.res.once).to.be.calledWith('close')
expect(ctx.res.off).to.be.calledWith('close')
})
})
it('sets noPreRequestExpected on the request', async () => {
const ctx = {
req: {
proxiedUrl: 'https://www.cypress.io/',
browserPreRequest: undefined,
noPreRequestExpected: undefined,
headers: [],
},
res: {
off: sinon.stub(),
once: sinon.stub(),
},
shouldCorrelatePreRequests: () => true,
getPreRequest: sinon.stub().yields({
noPreRequestExpected: true,
}),
}
await testMiddleware([CorrelateBrowserPreRequest], ctx)
.then(() => {
expect(ctx.getPreRequest).to.be.calledOnce
expect(ctx.req.noPreRequestExpected).to.be.true
expect(ctx.res.once).to.be.calledWith('close')
expect(ctx.res.off).to.be.calledWith('close')
})
})
it('errors when the request is destroyed prior to receiving a pre-request', () => {
const ctx = {
req: {
proxiedUrl: 'https://www.cypress.io/',
destroyed: true,
browserPreRequest: undefined,
noPreRequestExpected: undefined,
headers: [],
},
res: {
off: sinon.stub(),
once: sinon.stub(),
},
shouldCorrelatePreRequests: () => true,
getPreRequest: sinon.stub(),
onError: sinon.stub(),
}
testMiddleware([CorrelateBrowserPreRequest], ctx)
ctx.res.once.callArg(1)
expect(ctx.getPreRequest).to.be.calledOnce
expect(ctx.req.noPreRequestExpected).to.be.undefined
expect(ctx.req.browserPreRequest).to.be.undefined
expect(ctx.res.once).to.be.calledWith('close')
expect(ctx.onError).to.be.calledOnce
})
})
describe('SendRequestOutgoing', () => {
const { SendRequestOutgoing } = RequestMiddleware

View File

@@ -50,6 +50,7 @@ export interface Cfg extends ReceivedCypressOptions {
const localCwd = process.cwd()
const debug = Debug('cypress:server:project')
const debugVerbose = Debug('cypress-verbose:server:project')
type StartWebsocketOptions = Pick<Cfg, 'socketIoCookie' | 'namespace' | 'screenshotsFolder' | 'report' | 'reporter' | 'reporterOptions' | 'projectRoot'>
@@ -415,6 +416,8 @@ export class ProjectBase extends EE {
reporterInstance.emit(event, runnable)
if (event === 'test:before:run') {
debugVerbose('browserPreRequests prior to running %s: %O', runnable.title, this.server.getBrowserPreRequests())
this.emit('test:before:run', {
runnable,
previousResults: reporterInstance?.results() || {},

View File

@@ -472,8 +472,8 @@ export class ServerBase<TSocket extends SocketE2E | SocketCt> {
options.getRenderedHTMLOrigins = this._networkProxy?.http.getRenderedHTMLOrigins
options.getCurrentBrowser = () => this.getCurrentBrowser?.()
options.onResetServerState = (options: { testIsolation: boolean }) => {
this.networkProxy.reset({ resetPreRequests: !!options.testIsolation, resetBetweenSpecs: false })
options.onResetServerState = () => {
this.networkProxy.reset({ resetBetweenSpecs: false })
this.netStubbingState.reset()
this._remoteStates.reset()
this.resourceTypeAndCredentialManager.clear()
@@ -500,6 +500,10 @@ export class ServerBase<TSocket extends SocketE2E | SocketCt> {
this.networkProxy.removePendingBrowserPreRequest(requestId)
}
getBrowserPreRequests () {
return this._networkProxy?.getPendingBrowserPreRequests()
}
emitRequestEvent (eventName, data) {
this.socket.toDriver('request:event', eventName, data)
}
@@ -630,7 +634,7 @@ export class ServerBase<TSocket extends SocketE2E | SocketCt> {
}
reset () {
this._networkProxy?.reset({ resetPreRequests: true, resetBetweenSpecs: true })
this._networkProxy?.reset({ resetBetweenSpecs: true })
this.resourceTypeAndCredentialManager.clear()
const baseUrl = this._baseUrl ?? '<root>'

View File

@@ -412,7 +412,7 @@ export class SocketBase {
case 'http:request':
return options.onRequest(userAgent, automationRequest, args[0])
case 'reset:server:state':
return options.onResetServerState(args[0])
return options.onResetServerState()
case 'log:memory:pressure':
return firefoxUtil.log()
case 'firefox:force:gc':

View File

@@ -143,13 +143,17 @@ type ExecOptions = {
*/
snapshot?: boolean
/**
* By default strip ansi codes from stdout. Pass false to turn off.
* By default strip ansi codes from stdout/stderr. Pass false to turn off.
*/
stripAnsi?: boolean
/**
* Pass a function to assert on and/or modify the stdout before snapshotting.
*/
onStdout?: (stdout: string) => string | void
/**
* Pass a function to assert on and/or modify the stderr.
*/
onStderr?: (stderr: string) => string | void
/**
* Pass a function to receive the spawned process as an argument.
*/
@@ -857,24 +861,30 @@ const systemTests = {
})
if (options.stripAnsi) {
// always strip ansi from stdout before yielding
// always strip ansi from stdout/stderr before yielding
// it to any callback functions
stdout = stripAnsi(stdout)
stderr = stripAnsi(stderr)
}
if (options.onStdout) {
const newStdout = options.onStdout(stdout)
if (newStdout && _.isString(newStdout)) {
stdout = newStdout
}
}
if (options.onStderr) {
const newStderr = options.onStderr(stderr)
if (newStderr && _.isString(newStderr)) {
stderr = newStderr
}
}
// snapshot the stdout!
if (options.snapshot) {
// enable callback to modify stdout
const ostd = options.onStdout
if (ostd) {
const newStdout = ostd(stdout)
if (newStdout && _.isString(newStdout)) {
stdout = newStdout
}
}
// if we have browser in the stdout make
// sure its legit
const matches = browserNameVersionRe.exec(stdout)

View File

@@ -0,0 +1,62 @@
describe('lots of requests', () => {
describe('test isolation', () => {
describe('test isolation on', { testIsolation: true }, () => {
it('test 1', () => {
cy.visit('/lots-of-requests?test=1&i=1')
})
it('test 2', () => {
cy.visit('/lots-of-requests?test=2&i=1')
cy.get('#done').should('contain', 'Done')
})
})
describe('test isolation off', { testIsolation: false }, () => {
it('test 3', () => {
cy.visit('/lots-of-requests?test=3&i=1')
})
it('test 4', () => {
cy.get('#done').should('contain', 'Done')
})
})
describe('test isolation back on', { testIsolation: true }, () => {
it('test 5', () => {
cy.visit('/lots-of-requests?test=5&i=1')
})
it('test 6', () => {
cy.visit('/lots-of-requests?test=6&i=1')
cy.get('#done').should('contain', 'Done')
})
})
})
describe('multiple visits in one test', { testIsolation: true }, () => {
it('test 7', () => {
cy.visit('/lots-of-requests?test=7&i=1')
cy.visit('/lots-of-requests?test=7&i=2')
cy.get('#done').should('contain', 'Done')
})
})
describe('navigation in one test', { testIsolation: true }, () => {
it('test 8', () => {
cy.visit('/lots-of-requests?test=8&i=1')
cy.get('a').click()
cy.get('#done').should('contain', 'Done')
})
})
describe('network error', { testIsolation: true, browser: '!webkit' }, () => {
beforeEach(() => {
cy.intercept('GET', '/1mb?test=9&i=1&j=8', { forceNetworkError: true })
})
it('test 9', () => {
cy.visit('/lots-of-requests?test=9&i=1')
cy.get('#done').should('contain', 'Done')
})
})
})

View File

@@ -0,0 +1,76 @@
const systemTests = require('../lib/system-tests').default
const onServer = (app) => {
app.get('/lots-of-requests', (req, res) => {
const test = req.query.test
const i = req.query.i
res.send(
`<html>
<head>
<title>Lots of Requests</title>
<script>
for (let j = 0; j < 50; j++) {
fetch('/1mb?test=${test}&i=${i}&j=' + j).catch(() => {})
}
</script>
</head>
<body>
<a href="/lots-of-requests?test=${test}&i=2">Visit</a>
<script>
fetch('/1mb?test=${test}&i=${i}&last=1')
.then((response) => {
const html = '<div id="done">Done</div>'
document.body.insertAdjacentHTML('beforeend', html)
})
.catch(() => {})
</script>
</body>
</html>`,
)
})
app.get('/1mb', (req, res) => {
return res.type('text').send('x'.repeat(1024 * 1024))
})
}
describe('e2e proxy correlation spec', () => {
const timedOutRequests = (stderr) => {
const matches = stderr.matchAll(/Never received pre-request or url without pre-request for request (.*) after waiting/g)
// filter out all non-localhost requests since we only care about ones that came from the app,
// browsers make requests that don't have pre-requests for various reasons
// e.g. https://clientservices.googleapis.com/* and https://accounts.google.com/* in chrome
// https://firefox.settings.services.mozilla.com/v1/ and https://tracking-protection.cdn.mozilla.net/* in firefox
return [...matches].filter((match) => match[1].includes('localhost')).map((match) => match[1])
}
systemTests.setup({
servers: {
port: 3500,
onServer,
},
settings: {
e2e: {
baseUrl: 'http://localhost:3500',
},
},
})
systemTests.it('correctly correlates requests', {
spec: 'proxy_correlation.cy.js',
processEnv: {
DEBUG: 'cypress:proxy:http:util:prerequests',
},
config: {
experimentalWebKitSupport: true,
defaultCommandTimeout: 10000,
},
onStderr (stderr) {
const requests = timedOutRequests(stderr)
expect(requests).to.be.empty
},
})
})