Skip to content

Commit

Permalink
fix: correlate prerequests in order instead of reverse order (#27892)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanthemanuel authored Sep 27, 2023
1 parent 0871b03 commit 8da1e5c
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 29 deletions.
8 changes: 8 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
<!-- See the ../guides/writing-the-cypress-changelog.md for details on writing the changelog. -->
## 13.3.1

_Released 10/03/2023 (PENDING)_

**Bugfixes:**

- Fixed an issue where requests were correlated in the wrong order in the proxy. This could cause an issue where the wrong request is used for `cy.intercept` or assets (e.g. stylesheets or images) may not properly be available in Test Replay. Addressed in [#27892](https://github.com/cypress-io/cypress/pull/27892).

## 13.3.0

_Released 09/27/2023_
Expand Down
4 changes: 3 additions & 1 deletion packages/proxy/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,12 +331,14 @@ export class Http {

const onError = (error: Error): Promise<void> => {
ctx.error = error
if (ctx.req.browserPreRequest) {
if (ctx.req.browserPreRequest && !ctx.req.browserPreRequest.errorHandled) {
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),
errorHandled: false,
}

ctx.debug('Re-using pre-request data %o', preRequest)
Expand Down
46 changes: 23 additions & 23 deletions packages/proxy/lib/http/util/prerequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,40 +33,40 @@ type PendingPreRequest = {
}

/**
* Data structure that organizes items with duplicated keys into stacks.
* Data structure that organizes items with duplicated keys into queues.
*/
class StackMap<T> {
private stacks: Record<string, Array<T>> = {}
push (stackKey: string, value: T) {
if (!this.stacks[stackKey]) this.stacks[stackKey] = []
class QueueMap<T> {
private queues: Record<string, Array<T>> = {}
push (queueKey: string, value: T) {
if (!this.queues[queueKey]) this.queues[queueKey] = []

this.stacks[stackKey].push(value)
this.queues[queueKey].push(value)
}
pop (stackKey: string): T | undefined {
const stack = this.stacks[stackKey]
shift (queueKey: string): T | undefined {
const queue = this.queues[queueKey]

if (!stack) return
if (!queue) return

const item = stack.pop()
const item = queue.shift()

if (stack.length === 0) delete this.stacks[stackKey]
if (queue.length === 0) delete this.queues[queueKey]

return item
}
removeMatching (filterFn: (value: T) => boolean) {
Object.entries(this.stacks).forEach(([stackKey, stack]) => {
this.stacks[stackKey] = stack.filter(filterFn)
if (this.stacks[stackKey].length === 0) delete this.stacks[stackKey]
Object.entries(this.queues).forEach(([queueKey, queue]) => {
this.queues[queueKey] = queue.filter(filterFn)
if (this.queues[queueKey].length === 0) delete this.queues[queueKey]
})
}
removeExact (stackKey: string, value: T) {
const i = this.stacks[stackKey].findIndex((v) => v === value)
removeExact (queueKey: string, value: T) {
const i = this.queues[queueKey].findIndex((v) => v === value)

this.stacks[stackKey].splice(i, 1)
if (this.stacks[stackKey].length === 0) delete this.stacks[stackKey]
this.queues[queueKey].splice(i, 1)
if (this.queues[queueKey].length === 0) delete this.queues[queueKey]
}
get length () {
return Object.values(this.stacks).reduce((prev, cur) => prev + cur.length, 0)
return Object.values(this.queues).reduce((prev, cur) => prev + cur.length, 0)
}
}

Expand All @@ -79,8 +79,8 @@ class StackMap<T> {
export class PreRequests {
requestTimeout: number
sweepInterval: number
pendingPreRequests = new StackMap<PendingPreRequest>()
pendingRequests = new StackMap<PendingRequest>()
pendingPreRequests = new QueueMap<PendingPreRequest>()
pendingRequests = new QueueMap<PendingRequest>()
sweepIntervalTimer: NodeJS.Timeout

constructor (
Expand Down Expand Up @@ -119,7 +119,7 @@ export class PreRequests {
addPending (browserPreRequest: BrowserPreRequest) {
metrics.browserPreRequestsReceived++
const key = `${browserPreRequest.method}-${browserPreRequest.url}`
const pendingRequest = this.pendingRequests.pop(key)
const pendingRequest = this.pendingRequests.shift(key)

if (pendingRequest) {
debugVerbose('Incoming pre-request %s matches pending request. %o', key, browserPreRequest)
Expand All @@ -145,7 +145,7 @@ export class PreRequests {
get (req: CypressIncomingRequest, ctxDebug, callback: GetPreRequestCb) {
metrics.proxyRequestsReceived++
const key = `${req.method}-${req.proxiedUrl}`
const pendingPreRequest = this.pendingPreRequests.pop(key)
const pendingPreRequest = this.pendingPreRequests.shift(key)

if (pendingPreRequest) {
metrics.immediatelyMatchedRequests++
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 @@ -59,6 +59,7 @@ export type BrowserPreRequest = {
headers: { [key: string]: string | string[] }
resourceType: ResourceType
originalResourceType: string | undefined
errorHandled?: boolean
}

/**
Expand Down
31 changes: 31 additions & 0 deletions packages/proxy/test/unit/http/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,37 @@ describe('http', function () {
})
})

it('ensures not to create fake pending browser pre requests on multiple errors', function () {
incomingRequest.callsFake(function () {
this.req.browserPreRequest = {
errorHandled: 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-ignore
.handleHttpRequest({ method: 'GET', proxiedUrl: 'url' }, { on, off })
.then(function () {
expect(incomingRequest).to.be.calledOnce
expect(incomingResponse).to.not.be.called
expect(http.addPendingBrowserPreRequest).to.not.be.called
expect(error).to.be.calledOnce
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 = {}
Expand Down
10 changes: 5 additions & 5 deletions packages/proxy/test/unit/http/util/prerequests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ describe('http/util/prerequests', () => {
})

it('synchronously matches a pre-request that existed at the time of the request', () => {
// should match in reverse order
// should match in order
preRequests.addPending({ requestId: '1234', url: 'foo', method: 'WRONGMETHOD' } as BrowserPreRequest)
preRequests.addPending({ requestId: '1234', url: 'foo', method: 'GET' } as BrowserPreRequest)
const thirdPreRequest = { requestId: '1234', url: 'foo', method: 'GET' } as BrowserPreRequest
const secondPreRequest = { requestId: '1234', url: 'foo', method: 'GET' } as BrowserPreRequest

preRequests.addPending(thirdPreRequest)
preRequests.addPending(secondPreRequest)
preRequests.addPending({ requestId: '1234', url: 'foo', method: 'GET' } as BrowserPreRequest)
expectPendingCounts(0, 3)

const cb = sinon.stub()
Expand All @@ -34,7 +34,7 @@ describe('http/util/prerequests', () => {

const { args } = cb.getCall(0)

expect(args[0]).to.eq(thirdPreRequest)
expect(args[0]).to.eq(secondPreRequest)

expectPendingCounts(0, 2)
})
Expand Down

5 comments on commit 8da1e5c

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8da1e5c Sep 27, 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.1/linux-x64/develop-8da1e5c92fdc6280406136d8d51b15a646d3b96a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8da1e5c Sep 27, 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.1/linux-arm64/develop-8da1e5c92fdc6280406136d8d51b15a646d3b96a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8da1e5c Sep 27, 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.1/darwin-x64/develop-8da1e5c92fdc6280406136d8d51b15a646d3b96a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8da1e5c Sep 27, 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.1/darwin-arm64/develop-8da1e5c92fdc6280406136d8d51b15a646d3b96a/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on 8da1e5c Sep 27, 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.1/win32-x64/develop-8da1e5c92fdc6280406136d8d51b15a646d3b96a/cypress.tgz

Please sign in to comment.