Skip to content

Commit

Permalink
fix: fix failures and correlation in proxy (#28094)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanthemanuel authored Oct 23, 2023
1 parent 74a06c5 commit d960686
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 13 deletions.
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ _Released 10/24/2023 (PENDING)_

**Bugfixes:**

- Fixed a performance problem with proxy correlation when requests get aborted and then get miscorrelated with follow up requests. Fixed in [#28094](https://github.com/cypress-io/cypress/pull/28094).
- Fixed a regression in [10.0.0](#10.0.0), where search would not find a spec if the file name contains "-" or "\_", but search prompt contains " " instead (e.g. search file "spec-file.cy.ts" with prompt "spec file"). Fixes [#25303](https://github.com/cypress-io/cypress/issues/25303).

## 13.3.2
Expand Down
20 changes: 16 additions & 4 deletions packages/proxy/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import ErrorMiddleware from './error-middleware'
import RequestMiddleware from './request-middleware'
import ResponseMiddleware from './response-middleware'
import { HttpBuffers } from './util/buffers'
import { GetPreRequestCb, PreRequests } from './util/prerequests'
import { GetPreRequestCb, PendingRequest, PreRequests } from './util/prerequests'

import type EventEmitter from 'events'
import type CyServer from '@packages/server'
Expand Down Expand Up @@ -63,10 +63,12 @@ type HttpMiddlewareCtx<T> = {
stage: HttpStages
debug: Debug.Debugger
middleware: HttpMiddlewareStacks
pendingRequest: PendingRequest | undefined
getCookieJar: () => CookieJar
deferSourceMapRewrite: (opts: { js: string, url: string }) => string
getPreRequest: (cb: GetPreRequestCb) => void
getPreRequest: (cb: GetPreRequestCb) => PendingRequest | undefined
addPendingUrlWithoutPreRequest: (url: string) => void
removePendingRequest: (pendingRequest: PendingRequest) => void
getAUTUrl: Http['getAUTUrl']
setAUTUrl: Http['setAUTUrl']
simulatedCookies: SerializableAutomationCookie[]
Expand Down Expand Up @@ -325,15 +327,25 @@ export class Http {
getAUTUrl: this.getAUTUrl,
setAUTUrl: this.setAUTUrl,
getPreRequest: (cb) => {
this.preRequests.get(ctx.req, ctx.debug, cb)
return this.preRequests.get(ctx.req, ctx.debug, cb)
},
addPendingUrlWithoutPreRequest: (url) => {
this.preRequests.addPendingUrlWithoutPreRequest(url)
},
removePendingRequest: (pendingRequest: PendingRequest) => {
this.preRequests.removePendingRequest(pendingRequest)
},
protocolManager: this.protocolManager,

Check failure on line 338 in packages/proxy/lib/http/index.ts

View workflow job for this annotation

GitHub Actions / update-v8-snapshot-cache (ubuntu-latest)

Duplicate key 'addPendingUrlWithoutPreRequest'
}

const onError = (error: Error): Promise<void> => {
const pendingRequest = ctx.pendingRequest as PendingRequest | undefined

if (pendingRequest) {
delete ctx.pendingRequest
ctx.removePendingRequest(pendingRequest)
}

ctx.error = error
if (ctx.req.browserPreRequest && !ctx.req.browserPreRequest.errorHandled) {
ctx.req.browserPreRequest.errorHandled = true
Expand Down Expand Up @@ -427,7 +439,7 @@ export class Http {
}

removePendingBrowserPreRequest (requestId: string) {
this.preRequests.removePending(requestId)
this.preRequests.removePendingPreRequest(requestId)
}

addPendingUrlWithoutPreRequest (url: string) {
Expand Down
9 changes: 8 additions & 1 deletion packages/proxy/lib/http/request-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
}

this.debug('waiting for prerequest')
this.getPreRequest((({ browserPreRequest, noPreRequestExpected }) => {
this.pendingRequest = this.getPreRequest((({ browserPreRequest, noPreRequestExpected }) => {
this.req.browserPreRequest = browserPreRequest
this.req.noPreRequestExpected = noPreRequestExpected
copyResourceTypeAndNext()
Expand Down Expand Up @@ -455,6 +455,13 @@ const SendRequestOutgoing: RequestMiddleware = function () {
this.debug('request aborted')
// if the request is aborted, close out the middleware span and http span. the response middleware did not run

const pendingRequest = this.pendingRequest

if (pendingRequest) {
delete this.pendingRequest
this.removePendingRequest(pendingRequest)
}

this.reqMiddlewareSpan?.setAttributes({
requestAborted: true,
})
Expand Down
22 changes: 17 additions & 5 deletions packages/proxy/lib/http/util/prerequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export type CorrelationInformation = {

export type GetPreRequestCb = (correlationInformation: CorrelationInformation) => void

type PendingRequest = {
export type PendingRequest = {
key: string
ctxDebug
callback?: GetPreRequestCb
timeout: NodeJS.Timeout
Expand Down Expand Up @@ -74,10 +75,12 @@ class QueueMap<T> {
})
}
removeExact (queueKey: string, value: T) {
const i = this.queues[queueKey].findIndex((v) => v === value)
const i = this.queues[queueKey]?.findIndex((v) => v === value)

this.queues[queueKey].splice(i, 1)
if (this.queues[queueKey].length === 0) delete this.queues[queueKey]
if (i > -1) {
this.queues[queueKey].splice(i, 1)
if (this.queues[queueKey].length === 0) delete this.queues[queueKey]
}
}

forEach (fn: (value: T) => void) {
Expand Down Expand Up @@ -210,7 +213,7 @@ export class PreRequests {
})
}

removePending (requestId: string) {
removePendingPreRequest (requestId: string) {
this.pendingPreRequests.removeMatching(({ browserPreRequest }) => {
return (browserPreRequest.requestId.includes('-retry-') && !browserPreRequest.requestId.startsWith(`${requestId}-`)) || (!browserPreRequest.requestId.includes('-retry-') && browserPreRequest.requestId !== requestId)
})
Expand Down Expand Up @@ -267,6 +270,7 @@ export class PreRequests {
}

const pendingRequest: PendingRequest = {
key,
ctxDebug,
callback,
proxyRequestReceivedTimestamp: performance.now() + performance.timeOrigin,
Expand All @@ -283,6 +287,8 @@ export class PreRequests {
}

this.pendingRequests.push(key, pendingRequest)

return pendingRequest
}

setProtocolManager (protocolManager: ProtocolManagerShape) {
Expand All @@ -293,6 +299,12 @@ export class PreRequests {
this.requestTimeout = requestTimeout
}

removePendingRequest (pendingRequest: PendingRequest) {
this.pendingRequests.removeExact(pendingRequest.key, pendingRequest)
clearTimeout(pendingRequest.timeout)
delete pendingRequest.callback
}

reset () {
this.pendingPreRequests = new QueueMap<PendingPreRequest>()

Expand Down
25 changes: 23 additions & 2 deletions packages/proxy/test/unit/http/util/prerequests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ describe('http/util/prerequests', () => {

expectPendingCounts(0, 3)

preRequests.removePending('1235')
preRequests.removePendingPreRequest('1235')

expectPendingCounts(0, 2)
})
Expand All @@ -222,7 +222,7 @@ describe('http/util/prerequests', () => {

expectPendingCounts(0, 6)

preRequests.removePending('1235')
preRequests.removePendingPreRequest('1235')

expectPendingCounts(0, 2)
})
Expand All @@ -236,6 +236,27 @@ describe('http/util/prerequests', () => {
expect(cbServiceWorker).to.be.calledWith()
})

it('removes a pending request', () => {
const cb = sinon.stub()

const firstPreRequest = preRequests.get({ proxiedUrl: 'foo', method: 'GET', headers: {} } as CypressIncomingRequest, () => {}, cb)
const secondPreRequest = preRequests.get({ proxiedUrl: 'foo', method: 'GET', headers: {} } as CypressIncomingRequest, () => {}, cb)

expectPendingCounts(2, 0)

preRequests.removePendingRequest(firstPreRequest!)

expectPendingCounts(1, 0)

preRequests.removePendingRequest(firstPreRequest!)

expectPendingCounts(1, 0)

preRequests.removePendingRequest(secondPreRequest!)

expectPendingCounts(0, 0)
})

it('resets the queues', () => {
let callbackCalled = false

Expand Down
107 changes: 106 additions & 1 deletion packages/server/test/integration/http_requests_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('Routes', () => {

Fixtures.scaffold()

this.setup = async (initialUrl, obj = {}, spec) => {
this.setup = async (initialUrl, obj = {}, spec, shouldCorrelatePreRequests = false) => {
if (_.isObject(initialUrl)) {
obj = initialUrl
initialUrl = null
Expand Down Expand Up @@ -170,6 +170,7 @@ describe('Routes', () => {
createRoutes,
testingType: 'e2e',
exit: false,
shouldCorrelatePreRequests: () => shouldCorrelatePreRequests,
})
.spread(async (port) => {
const automationStub = {
Expand All @@ -187,6 +188,8 @@ describe('Routes', () => {
this.session = session(this.srv)

this.proxy = `http://localhost:${port}`

this.networkProxy = this.server._networkProxy
}),
])
})
Expand Down Expand Up @@ -983,6 +986,108 @@ describe('Routes', () => {
})
})

context('basic request with correlation', () => {
beforeEach(function () {
return this.setup('http://www.github.com', undefined, undefined, true)
})

it('properly correlates when CDP failures come first', function () {
this.timeout(1500)

nock(this.server.remoteStates.current().origin)
.get('/')
.reply(200, 'hello from bar!', {
'Content-Type': 'text/html',
})

this.networkProxy.addPendingBrowserPreRequest({
requestId: '1',
method: 'GET',
url: 'http://www.github.com/',
})

this.networkProxy.removePendingBrowserPreRequest({
requestId: '1',
})

const requestPromise = this.rp({
url: 'http://www.github.com/',
headers: {
'Cookie': '__cypress.initial=true',
'Accept-Encoding': 'identity',
},
})

this.networkProxy.addPendingBrowserPreRequest({
requestId: '1',
method: 'GET',
url: 'http://www.github.com/',
})

return requestPromise.then((res) => {
expect(res.statusCode).to.eq(200)

expect(res.body).to.include('hello from bar!')
})
})

it('properly correlates when proxy failure come first', function () {
this.networkProxy.setPreRequestTimeout(50)
// If this takes longer than the Promise.delay and the prerequest timeout then the second
// call has hit the prerequest timeout which is a problem
this.timeout(150)

nock(this.server.remoteStates.current().origin)
.get('/')
.delay(50)
.once()
.reply(200, 'hello from bar!', {
'Content-Type': 'text/html',
})

this.rp({
url: 'http://www.github.com/',
headers: {
'Cookie': '__cypress.initial=true',
'Accept-Encoding': 'identity',
},
// Timeout needs to be less than the prerequest timeout + the nock delay
timeout: 75,
}).catch(() => {})

// Wait 100 ms to make sure the request times out
return Promise.delay(100).then(() => {
nock(this.server.remoteStates.current().origin)
.get('/')
.once()
.reply(200, 'hello from baz!', {
'Content-Type': 'text/html',
})

// This should not immediately correlate. If it does, then the next request will timeout
this.networkProxy.addPendingBrowserPreRequest({
requestId: '1',
method: 'GET',
url: 'http://www.github.com/',
})

const followupRequestPromise = this.rp({
url: 'http://www.github.com/',
headers: {
'Cookie': '__cypress.initial=true',
'Accept-Encoding': 'identity',
},
})

return followupRequestPromise.then((res) => {
expect(res.statusCode).to.eq(200)

expect(res.body).to.include('hello from baz!')
})
})
})
})

context('gzip', () => {
beforeEach(function () {
return this.setup('http://www.github.com')
Expand Down

5 comments on commit d960686

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on d960686 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.3.3/linux-arm64/develop-d9606868c5201fc7dde5645094b756602b65f4f7/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on d960686 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.3.3/linux-x64/develop-d9606868c5201fc7dde5645094b756602b65f4f7/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on d960686 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.3.3/darwin-x64/develop-d9606868c5201fc7dde5645094b756602b65f4f7/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on d960686 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.3.3/darwin-arm64/develop-d9606868c5201fc7dde5645094b756602b65f4f7/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on d960686 Oct 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release build at https://on.cypress.io/advanced-installation#Install-pre-release-version

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/13.3.3/win32-x64/develop-d9606868c5201fc7dde5645094b756602b65f4f7/cypress.tgz

Please sign in to comment.