Skip to content

Commit 9bc3715

Browse files
flotwigBlue F
and
Blue F
authored
fix(proxy/prerequests): fix duplicate key behavior, fallthrough (#23227)
Co-authored-by: Blue F <[email protected]>
1 parent 3e76ed0 commit 9bc3715

File tree

2 files changed

+90
-30
lines changed

2 files changed

+90
-30
lines changed

packages/proxy/lib/http/util/prerequests.ts

Lines changed: 75 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,50 @@ export type GetPreRequestCb = (browserPreRequest?: BrowserPreRequest) => void
2424
type PendingRequest = {
2525
ctxDebug
2626
callback: GetPreRequestCb
27-
timeout: ReturnType<typeof setTimeout>
27+
timeout: NodeJS.Timeout
28+
}
29+
30+
type PendingPreRequest = {
31+
browserPreRequest: BrowserPreRequest
32+
timestamp: number
33+
}
34+
35+
/**
36+
* Data structure that organizes items with duplicated keys into stacks.
37+
*/
38+
class StackMap<T> {
39+
private stacks: Record<string, Array<T>> = {}
40+
push (stackKey: string, value: T) {
41+
if (!this.stacks[stackKey]) this.stacks[stackKey] = []
42+
43+
this.stacks[stackKey].push(value)
44+
}
45+
pop (stackKey: string): T | undefined {
46+
const stack = this.stacks[stackKey]
47+
48+
if (!stack) return
49+
50+
const item = stack.pop()
51+
52+
if (stack.length === 0) delete this.stacks[stackKey]
53+
54+
return item
55+
}
56+
removeMatching (filterFn: (value: T) => boolean) {
57+
Object.entries(this.stacks).forEach(([stackKey, stack]) => {
58+
this.stacks[stackKey] = stack.filter(filterFn)
59+
if (this.stacks[stackKey].length === 0) delete this.stacks[stackKey]
60+
})
61+
}
62+
removeExact (stackKey: string, value: T) {
63+
const i = this.stacks[stackKey].findIndex((v) => v === value)
64+
65+
this.stacks[stackKey].splice(i, 1)
66+
if (this.stacks[stackKey].length === 0) delete this.stacks[stackKey]
67+
}
68+
get length () {
69+
return Object.values(this.stacks).reduce((prev, cur) => prev + cur.length, 0)
70+
}
2871
}
2972

3073
// This class' purpose is to match up incoming "requests" (requests from the browser received by the http proxy)
@@ -35,9 +78,8 @@ type PendingRequest = {
3578
// ever comes in, we don't want to block proxied requests indefinitely.
3679
export class PreRequests {
3780
requestTimeout: number
38-
pendingPreRequests: Record<string, BrowserPreRequest> = {}
39-
pendingRequests: Record<string, PendingRequest> = {}
40-
prerequestTimestamps: Record<string, number> = {}
81+
pendingPreRequests = new StackMap<PendingPreRequest>()
82+
pendingRequests = new StackMap<PendingRequest>()
4183
sweepInterval: ReturnType<typeof setInterval>
4284

4385
constructor (requestTimeout = 500) {
@@ -55,59 +97,63 @@ export class PreRequests {
5597
this.sweepInterval = setInterval(() => {
5698
const now = Date.now()
5799

58-
Object.entries(this.prerequestTimestamps).forEach(([key, timestamp]) => {
100+
this.pendingPreRequests.removeMatching(({ timestamp, browserPreRequest }) => {
59101
if (timestamp + this.requestTimeout * 2 < now) {
60-
debugVerbose('timed out unmatched pre-request %s: %o', key, this.pendingPreRequests[key])
102+
debugVerbose('timed out unmatched pre-request: %o', browserPreRequest)
61103
metrics.unmatchedPreRequests++
62-
delete this.pendingPreRequests[key]
63-
delete this.prerequestTimestamps[key]
104+
105+
return false
64106
}
107+
108+
return true
65109
})
66110
}, this.requestTimeout * 2)
67111
}
68112

69113
addPending (browserPreRequest: BrowserPreRequest) {
70114
metrics.browserPreRequestsReceived++
71115
const key = `${browserPreRequest.method}-${browserPreRequest.url}`
116+
const pendingRequest = this.pendingRequests.pop(key)
72117

73-
if (this.pendingRequests[key]) {
118+
if (pendingRequest) {
74119
debugVerbose('Incoming pre-request %s matches pending request. %o', key, browserPreRequest)
75-
clearTimeout(this.pendingRequests[key].timeout)
76-
this.pendingRequests[key].callback(browserPreRequest)
77-
delete this.pendingRequests[key]
120+
clearTimeout(pendingRequest.timeout)
121+
pendingRequest.callback(browserPreRequest)
122+
123+
return
78124
}
79125

80126
debugVerbose('Caching pre-request %s to be matched later. %o', key, browserPreRequest)
81-
this.pendingPreRequests[key] = browserPreRequest
82-
this.prerequestTimestamps[key] = Date.now()
127+
this.pendingPreRequests.push(key, {
128+
browserPreRequest,
129+
timestamp: Date.now(),
130+
})
83131
}
84132

85133
get (req: CypressIncomingRequest, ctxDebug, callback: GetPreRequestCb) {
86134
metrics.proxyRequestsReceived++
87135
const key = `${req.method}-${req.proxiedUrl}`
136+
const pendingPreRequest = this.pendingPreRequests.pop(key)
88137

89-
if (this.pendingPreRequests[key]) {
138+
if (pendingPreRequest) {
90139
metrics.immediatelyMatchedRequests++
91-
ctxDebug('Incoming request %s matches known pre-request: %o', key, this.pendingPreRequests[key])
92-
callback(this.pendingPreRequests[key])
93-
94-
delete this.pendingPreRequests[key]
95-
delete this.prerequestTimestamps[key]
140+
ctxDebug('Incoming request %s matches known pre-request: %o', key, pendingPreRequest)
141+
callback(pendingPreRequest.browserPreRequest)
96142

97143
return
98144
}
99145

100-
const timeout = setTimeout(() => {
101-
callback()
102-
ctxDebug('Never received pre-request for request %s after waiting %sms. Continuing without one.', key, this.requestTimeout)
103-
metrics.unmatchedRequests++
104-
delete this.pendingRequests[key]
105-
}, this.requestTimeout)
106-
107-
this.pendingRequests[key] = {
146+
const pendingRequest: PendingRequest = {
108147
ctxDebug,
109148
callback,
110-
timeout,
149+
timeout: setTimeout(() => {
150+
ctxDebug('Never received pre-request for request %s after waiting %sms. Continuing without one.', key, this.requestTimeout)
151+
metrics.unmatchedRequests++
152+
this.pendingRequests.removeExact(key, pendingRequest)
153+
callback()
154+
}, this.requestTimeout),
111155
}
156+
157+
this.pendingRequests.push(key, pendingRequest)
112158
}
113159
}

packages/proxy/test/unit/http/util/prerequests.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import sinon from 'sinon'
66
describe('http/util/prerequests', () => {
77
let preRequests: PreRequests
88

9+
function expectPendingCounts (pendingRequests: number, pendingPreRequests: number) {
10+
expect(preRequests.pendingRequests.length).to.eq(pendingRequests, 'wrong number of pending requests')
11+
expect(preRequests.pendingPreRequests.length).to.eq(pendingPreRequests, 'wrong number of pending prerequests')
12+
}
13+
914
beforeEach(() => {
1015
preRequests = new PreRequests(10)
1116
})
@@ -18,19 +23,26 @@ describe('http/util/prerequests', () => {
1823
// should match in reverse order
1924
preRequests.addPending({ requestId: '1234', url: 'foo', method: 'WRONGMETHOD' } as BrowserPreRequest)
2025
preRequests.addPending({ requestId: '1234', url: 'foo', method: 'GET' } as BrowserPreRequest)
26+
const thirdPreRequest = { requestId: '1234', url: 'foo', method: 'GET' } as BrowserPreRequest
27+
28+
preRequests.addPending(thirdPreRequest)
29+
expectPendingCounts(0, 3)
2130

2231
const cb = sinon.stub()
2332

2433
preRequests.get({ proxiedUrl: 'foo', method: 'GET' } as CypressIncomingRequest, () => {}, cb)
2534

2635
const { args } = cb.getCall(0)
2736

28-
expect(args[0]).to.include({ requestId: '1234', url: 'foo', method: 'GET' })
37+
expect(args[0]).to.eq(thirdPreRequest)
38+
39+
expectPendingCounts(0, 2)
2940
})
3041

3142
it('synchronously matches a pre-request added after the request', (done) => {
3243
const cb = (preRequest) => {
3344
expect(preRequest).to.include({ requestId: '1234', url: 'foo', method: 'GET' })
45+
expectPendingCounts(0, 0)
3446
done()
3547
}
3648

@@ -41,6 +53,7 @@ describe('http/util/prerequests', () => {
4153
it('invokes a request callback after a timeout if no pre-request occurs', (done) => {
4254
const cb = (preRequest) => {
4355
expect(preRequest).to.be.undefined
56+
expectPendingCounts(0, 0)
4457
done()
4558
}
4659

@@ -57,6 +70,7 @@ describe('http/util/prerequests', () => {
5770
setTimeout(() => {
5871
const cb = (preRequest) => {
5972
expect(preRequest).to.be.undefined
73+
expectPendingCounts(0, 0)
6074
done()
6175
}
6276

0 commit comments

Comments
 (0)