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: correlate prerequests in order instead of reverse order #27892

Merged
merged 16 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need a test for the errorHandled logic?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

// 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