Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix failures and correlation in proxy #28094

Merged
merged 9 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.3.2

_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. Addressed in [#28094](https://github.com/cypress-io/cypress/pull/28094).
ryanthemanuel marked this conversation as resolved.
Show resolved Hide resolved

_Released 10/18/2023_

**Bugfixes:**
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,
}

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

Choose a reason for hiding this comment

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

This coercion is needed because ctx is HttpMiddlewareCtx<any>, which causes ctx to be any.

Removing this templated any results in a couple of typescript errors that may be indicative of bugs; one of which is in deferSourceMapRewrite which should be returning a string instead of void (unless the fn definition on L#320 is more correct than the type definition on L#68). Can resolving the ambiguous definition of ctx be rolled into this PR, or is that out of scope?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think at this point given I want to try and get this in for tomorrow, that I'll call this out of scope for now. It's a good find though. This can definitely be cleaned up at some point.


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)
ryanthemanuel marked this conversation as resolved.
Show resolved Hide resolved
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