@@ -3,7 +3,6 @@ import type {
3
3
BrowserPreRequest ,
4
4
} from '@packages/proxy'
5
5
import Debug from 'debug'
6
- import _ from 'lodash'
7
6
8
7
const debug = Debug ( 'cypress:proxy:http:util:prerequests' )
9
8
const debugVerbose = Debug ( 'cypress-verbose:proxy:http:util:prerequests' )
@@ -12,101 +11,103 @@ const metrics: any = {
12
11
browserPreRequestsReceived : 0 ,
13
12
proxyRequestsReceived : 0 ,
14
13
immediatelyMatchedRequests : 0 ,
15
- eventuallyReceivedPreRequest : [ ] ,
16
- neverReceivedPreRequest : [ ] ,
14
+ unmatchedRequests : 0 ,
15
+ unmatchedPreRequests : 0 ,
17
16
}
18
17
19
18
process . once ( 'exit' , ( ) => {
20
19
debug ( 'metrics: %o' , metrics )
21
20
} )
22
21
23
- function removeOne < T > ( a : Array < T > , predicate : ( v : T ) => boolean ) : T | void {
24
- for ( let i = a . length - 1 ; i >= 0 ; i -- ) {
25
- const v = a [ i ]
26
-
27
- if ( predicate ( v ) ) {
28
- a . splice ( i , 1 )
29
-
30
- return v
31
- }
32
- }
33
- }
22
+ export type GetPreRequestCb = ( browserPreRequest ?: BrowserPreRequest ) => void
34
23
35
- function matches ( preRequest : BrowserPreRequest , req : Pick < CypressIncomingRequest , 'proxiedUrl' | 'method' > ) {
36
- return preRequest . method === req . method && preRequest . url === req . proxiedUrl
24
+ type PendingRequest = {
25
+ ctxDebug
26
+ callback : GetPreRequestCb
27
+ timeout : ReturnType < typeof setTimeout >
37
28
}
38
29
39
- export type GetPreRequestCb = ( browserPreRequest ?: BrowserPreRequest ) => void
30
+ // This class' purpose is to match up incoming "requests" (requests from the browser received by the http proxy)
31
+ // with "pre-requests" (events received by our browser extension indicating that the browser is about to make a request).
32
+ // Because these come from different sources, they can be out of sync, arriving in either order.
40
33
34
+ // Basically, when requests come in, we want to provide additional data read from the pre-request. but if no pre-request
35
+ // ever comes in, we don't want to block proxied requests indefinitely.
41
36
export class PreRequests {
42
- pendingBrowserPreRequests : Array < BrowserPreRequest > = [ ]
43
- requestsPendingPreRequestCbs : Array < {
44
- cb : ( browserPreRequest : BrowserPreRequest ) => void
45
- method : string
46
- proxiedUrl : string
47
- } > = [ ]
48
-
49
- get ( req : CypressIncomingRequest , ctxDebug , cb : GetPreRequestCb ) {
50
- metrics . proxyRequestsReceived ++
51
-
52
- const pendingBrowserPreRequest = removeOne ( this . pendingBrowserPreRequests , ( browserPreRequest ) => {
53
- return matches ( browserPreRequest , req )
54
- } )
55
-
56
- if ( pendingBrowserPreRequest ) {
57
- metrics . immediatelyMatchedRequests ++
58
-
59
- ctxDebug ( 'matches pending pre-request %o' , pendingBrowserPreRequest )
37
+ requestTimeout : number
38
+ pendingPreRequests : Record < string , BrowserPreRequest > = { }
39
+ pendingRequests : Record < string , PendingRequest > = { }
40
+ prerequestTimestamps : Record < string , number > = { }
41
+ sweepInterval : ReturnType < typeof setInterval >
42
+
43
+ constructor ( requestTimeout = 500 ) {
44
+ // If a request comes in and we don't have a matching pre-request after this timeout,
45
+ // we invoke the request callback to tell the server to proceed (we don't want to block
46
+ // user requests indefinitely).
47
+ this . requestTimeout = requestTimeout
48
+
49
+ // Discarding prerequests on the other hand is not urgent, so we do it on a regular interval
50
+ // rather than with a separate timer for each one.
51
+ // 2 times the requestTimeout is arbitrary, chosen to give plenty of time and
52
+ // make sure we don't discard any pre-requests prematurely but that we don't leak memory over time
53
+ // if a large number of pre-requests don't match up
54
+ // fixes: https://github.com/cypress-io/cypress/issues/17853
55
+ this . sweepInterval = setInterval ( ( ) => {
56
+ const now = Date . now ( )
57
+
58
+ Object . entries ( this . prerequestTimestamps ) . forEach ( ( [ key , timestamp ] ) => {
59
+ if ( timestamp + this . requestTimeout * 2 < now ) {
60
+ debugVerbose ( 'timed out unmatched pre-request %s: %o' , key , this . pendingPreRequests [ key ] )
61
+ metrics . unmatchedPreRequests ++
62
+ delete this . pendingPreRequests [ key ]
63
+ delete this . prerequestTimestamps [ key ]
64
+ }
65
+ } )
66
+ } , this . requestTimeout * 2 )
67
+ }
60
68
61
- return cb ( pendingBrowserPreRequest )
62
- }
69
+ addPending ( browserPreRequest : BrowserPreRequest ) {
70
+ metrics . browserPreRequestsReceived ++
71
+ const key = `${ browserPreRequest . method } -${ browserPreRequest . url } `
63
72
64
- const timeout = setTimeout ( ( ) => {
65
- metrics . neverReceivedPreRequest . push ( { url : req . proxiedUrl } )
66
- ctxDebug ( '500ms passed without a pre-request, continuing request with an empty pre-request field!' )
67
-
68
- remove ( )
69
- cb ( )
70
- } , 500 )
71
-
72
- const startedMs = Date . now ( )
73
- const remove = _ . once ( ( ) => removeOne ( this . requestsPendingPreRequestCbs , ( v ) => v === requestPendingPreRequestCb ) )
74
-
75
- const requestPendingPreRequestCb = {
76
- cb : ( browserPreRequest ) => {
77
- const afterMs = Date . now ( ) - startedMs
78
-
79
- metrics . eventuallyReceivedPreRequest . push ( { url : browserPreRequest . url , afterMs } )
80
- ctxDebug ( 'received pre-request after %dms %o' , afterMs , browserPreRequest )
81
- clearTimeout ( timeout )
82
- remove ( )
83
- cb ( browserPreRequest )
84
- } ,
85
- proxiedUrl : req . proxiedUrl ,
86
- method : req . method ,
73
+ if ( this . pendingRequests [ key ] ) {
74
+ 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 ]
87
78
}
88
79
89
- this . requestsPendingPreRequestCbs . push ( requestPendingPreRequestCb )
80
+ debugVerbose ( 'Caching pre-request %s to be matched later. %o' , key , browserPreRequest )
81
+ this . pendingPreRequests [ key ] = browserPreRequest
82
+ this . prerequestTimestamps [ key ] = Date . now ( )
90
83
}
91
84
92
- addPending ( browserPreRequest : BrowserPreRequest ) {
93
- if ( this . pendingBrowserPreRequests . indexOf ( browserPreRequest ) !== - 1 ) {
94
- return
95
- }
96
-
97
- metrics . browserPreRequestsReceived ++
85
+ get ( req : CypressIncomingRequest , ctxDebug , callback : GetPreRequestCb ) {
86
+ metrics . proxyRequestsReceived ++
87
+ const key = `${ req . method } -${ req . proxiedUrl } `
98
88
99
- const requestPendingPreRequestCb = removeOne ( this . requestsPendingPreRequestCbs , ( req ) => {
100
- return matches ( browserPreRequest , req )
101
- } )
89
+ if ( this . pendingPreRequests [ key ] ) {
90
+ metrics . immediatelyMatchedRequests ++
91
+ ctxDebug ( 'Incoming request %s matches known pre-request: %o' , key , this . pendingPreRequests [ key ] )
92
+ callback ( this . pendingPreRequests [ key ] )
102
93
103
- if ( requestPendingPreRequestCb ) {
104
- debugVerbose ( 'immediately matched pre-request %o' , browserPreRequest )
94
+ delete this . pendingPreRequests [ key ]
95
+ delete this . prerequestTimestamps [ key ]
105
96
106
- return requestPendingPreRequestCb . cb ( browserPreRequest )
97
+ return
107
98
}
108
99
109
- debugVerbose ( 'queuing pre-request to be matched later %o %o' , browserPreRequest , this . pendingBrowserPreRequests )
110
- this . pendingBrowserPreRequests . push ( browserPreRequest )
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 ] = {
108
+ ctxDebug,
109
+ callback,
110
+ timeout,
111
+ }
111
112
}
112
113
}
0 commit comments