Skip to content

Commit

Permalink
fix: proxy issues with service workers and clean up at end of specs (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanthemanuel committed Oct 18, 2023
1 parent a96b9b1 commit ff89ffa
Show file tree
Hide file tree
Showing 26 changed files with 466 additions and 87 deletions.
3 changes: 3 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ _Released 10/25/2023 (PENDING)_

**Bugfixes:**

- Fixed a performance regression in `13.3.1` with proxy correlation timeouts and requests issued from service workers. Fixes [#28054](https://github.com/cypress-io/cypress/issues/28054) and [#28056](https://github.com/cypress-io/cypress/issues/28056).
- Fixed an issue where proxy correlation would leak over from a previous spec causing performance problems, `cy.intercept` problems, and Test Replay asset capturing issues. Addressed in [#28060](https://github.com/cypress-io/cypress/pull/28060).
- Fixed an issue where redirects of requests that knowingly don't have CDP traffic should also be assumed to not have CDP traffic. Addressed in [#28060](https://github.com/cypress-io/cypress/pull/28060).
- Fixed an issue with Accept Encoding headers by forcing gzip when no accept encoding header is sent and using identity if gzip is not sent. Fixes [#28025](https://github.com/cypress-io/cypress/issues/28025).

**Dependency Updates:**
Expand Down
9 changes: 9 additions & 0 deletions packages/proxy/lib/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type HttpMiddlewareCtx<T> = {
getCookieJar: () => CookieJar
deferSourceMapRewrite: (opts: { js: string, url: string }) => string
getPreRequest: (cb: GetPreRequestCb) => void
addPendingUrlWithoutPreRequest: (url: string) => void
getAUTUrl: Http['getAUTUrl']
setAUTUrl: Http['setAUTUrl']
simulatedCookies: SerializableAutomationCookie[]
Expand Down Expand Up @@ -326,6 +327,9 @@ export class Http {
getPreRequest: (cb) => {
this.preRequests.get(ctx.req, ctx.debug, cb)
},
addPendingUrlWithoutPreRequest: (url) => {
this.preRequests.addPendingUrlWithoutPreRequest(url)
},
protocolManager: this.protocolManager,
}

Expand Down Expand Up @@ -411,6 +415,7 @@ export class Http {
reset () {
this.buffers.reset()
this.setAUTUrl(undefined)
this.preRequests.reset()
}

setBuffer (buffer) {
Expand All @@ -433,4 +438,8 @@ export class Http {
this.protocolManager = protocolManager
this.preRequests.setProtocolManager(protocolManager)
}

setPreRequestTimeout (timeout: number) {
this.preRequests.setPreRequestTimeout(timeout)
}
}
3 changes: 2 additions & 1 deletion packages/proxy/lib/http/request-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,9 @@ const CorrelateBrowserPreRequest: RequestMiddleware = async function () {
}

this.debug('waiting for prerequest')
this.getPreRequest(((browserPreRequest) => {
this.getPreRequest((({ browserPreRequest, noPreRequestExpected }) => {
this.req.browserPreRequest = browserPreRequest
this.req.noPreRequestExpected = noPreRequestExpected
copyResourceTypeAndNext()
}))
}
Expand Down
5 changes: 5 additions & 0 deletions packages/proxy/lib/http/response-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,11 @@ const MaybeSendRedirectToClient: ResponseMiddleware = function () {
return this.next()
}

// If we're redirecting from a request that doesn't expect to have a preRequest (e.g. download links), we need to treat the redirected url as such as well.
if (this.req.noPreRequestExpected) {
this.addPendingUrlWithoutPreRequest(newUrl)
}

setInitialCookie(this.res, this.remoteStates.current(), true)

this.debug('redirecting to new url %o', { statusCode, newUrl })
Expand Down
91 changes: 77 additions & 14 deletions packages/proxy/lib/http/util/prerequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ process.once('exit', () => {
debug('metrics: %o', metrics)
})

export type GetPreRequestCb = (browserPreRequest?: BrowserPreRequestWithTimings) => void
export type CorrelationInformation = {
browserPreRequest?: BrowserPreRequestWithTimings
noPreRequestExpected?: boolean
}

export type GetPreRequestCb = (correlationInformation: CorrelationInformation) => void

type PendingRequest = {
ctxDebug
callback: GetPreRequestCb
callback?: GetPreRequestCb
timeout: NodeJS.Timeout
timedOut?: boolean
proxyRequestReceivedTimestamp: number
Expand Down Expand Up @@ -74,6 +79,13 @@ class QueueMap<T> {
this.queues[queueKey].splice(i, 1)
if (this.queues[queueKey].length === 0) delete this.queues[queueKey]
}

forEach (fn: (value: T) => void) {
Object.values(this.queues).forEach((queue) => {
queue.forEach(fn)
})
}

get length () {
return Object.values(this.queues).reduce((prev, cur) => prev + cur.length, 0)
}
Expand Down Expand Up @@ -148,11 +160,16 @@ export class PreRequests {
debugVerbose('Incoming pre-request %s matches pending request. %o', key, browserPreRequest)
if (!pendingRequest.timedOut) {
clearTimeout(pendingRequest.timeout)
pendingRequest.callback({
...browserPreRequest,
...timings,
pendingRequest.callback?.({
browserPreRequest: {
...browserPreRequest,
...timings,
},
noPreRequestExpected: false,
})

delete pendingRequest.callback

return
}

Expand All @@ -179,7 +196,11 @@ export class PreRequests {
if (pendingRequest) {
debugVerbose('Handling %s without a CDP prerequest', key)
clearTimeout(pendingRequest.timeout)
pendingRequest.callback()
pendingRequest.callback?.({
noPreRequestExpected: true,
})

delete pendingRequest.callback

return
}
Expand All @@ -196,6 +217,19 @@ export class PreRequests {
}

get (req: CypressIncomingRequest, ctxDebug, callback: GetPreRequestCb) {
// The initial request that loads the service worker does not get sent to CDP and it happens prior
// to the service worker target being added. Thus, we need to explicitly ignore it. We determine
// it's the service worker request via the `sec-fetch-dest` header
if (req.headers['sec-fetch-dest'] === 'serviceworker') {
ctxDebug('Ignoring request with sec-fetch-dest: serviceworker', req.proxiedUrl)

callback({
noPreRequestExpected: true,
})

return
}

const proxyRequestReceivedTimestamp = performance.now() + performance.timeOrigin

metrics.proxyRequestsReceived++
Expand All @@ -206,12 +240,15 @@ export class PreRequests {
metrics.immediatelyMatchedRequests++
ctxDebug('Incoming request %s matches known pre-request: %o', key, pendingPreRequest)
callback({
...pendingPreRequest.browserPreRequest,
cdpRequestWillBeSentTimestamp: pendingPreRequest.cdpRequestWillBeSentTimestamp,
cdpRequestWillBeSentReceivedTimestamp: pendingPreRequest.cdpRequestWillBeSentReceivedTimestamp,
proxyRequestReceivedTimestamp,
cdpLagDuration: pendingPreRequest.cdpRequestWillBeSentReceivedTimestamp - pendingPreRequest.cdpRequestWillBeSentTimestamp,
proxyRequestCorrelationDuration: Math.max(pendingPreRequest.cdpRequestWillBeSentReceivedTimestamp - proxyRequestReceivedTimestamp, 0),
browserPreRequest: {
...pendingPreRequest.browserPreRequest,
cdpRequestWillBeSentTimestamp: pendingPreRequest.cdpRequestWillBeSentTimestamp,
cdpRequestWillBeSentReceivedTimestamp: pendingPreRequest.cdpRequestWillBeSentReceivedTimestamp,
proxyRequestReceivedTimestamp,
cdpLagDuration: pendingPreRequest.cdpRequestWillBeSentReceivedTimestamp - pendingPreRequest.cdpRequestWillBeSentTimestamp,
proxyRequestCorrelationDuration: Math.max(pendingPreRequest.cdpRequestWillBeSentReceivedTimestamp - proxyRequestReceivedTimestamp, 0),
},
noPreRequestExpected: false,
})

return
Expand All @@ -222,7 +259,9 @@ export class PreRequests {
if (pendingUrlWithoutPreRequests) {
metrics.immediatelyMatchedRequests++
ctxDebug('Incoming request %s matches known pending url without pre request', key)
callback()
callback({
noPreRequestExpected: true,
})

return
}
Expand All @@ -235,7 +274,11 @@ export class PreRequests {
ctxDebug('Never received pre-request or url without pre-request for request %s after waiting %sms. Continuing without one.', key, this.requestTimeout)
metrics.unmatchedRequests++
pendingRequest.timedOut = true
callback()
callback({
noPreRequestExpected: false,
})

delete pendingRequest.callback
}, this.requestTimeout),
}

Expand All @@ -245,4 +288,24 @@ export class PreRequests {
setProtocolManager (protocolManager: ProtocolManagerShape) {
this.protocolManager = protocolManager
}

setPreRequestTimeout (requestTimeout: number) {
this.requestTimeout = requestTimeout
}

reset () {
this.pendingPreRequests = new QueueMap<PendingPreRequest>()

// Clear out the pending requests timeout callbacks first then clear the queue
this.pendingRequests.forEach(({ callback, timeout }) => {
clearTimeout(timeout)
metrics.unmatchedRequests++
callback?.({
noPreRequestExpected: false,
})
})

this.pendingRequests = new QueueMap<PendingRequest>()
this.pendingUrlsWithoutPreRequests = new QueueMap<PendingUrlWithoutPreRequest>()
}
}
4 changes: 4 additions & 0 deletions packages/proxy/lib/network-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,8 @@ export class NetworkProxy {
setProtocolManager (protocolManager) {
this.http.setProtocolManager(protocolManager)
}

setPreRequestTimeout (timeout) {
this.http.setPreRequestTimeout(timeout)
}
}
1 change: 1 addition & 0 deletions packages/proxy/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type CypressIncomingRequest = Request & {
abort: () => void
requestId: string
browserPreRequest?: BrowserPreRequestWithTimings
noPreRequestExpected?: boolean
body?: string
responseTimeout?: number
followRedirect?: boolean
Expand Down
Loading

4 comments on commit ff89ffa

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ff89ffa Oct 18, 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.2/linux-arm64/develop-ff89ffa2b2ef36d02bff0588bb0582cfa8a6002d/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ff89ffa Oct 18, 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.2/linux-x64/develop-ff89ffa2b2ef36d02bff0588bb0582cfa8a6002d/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ff89ffa Oct 18, 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.2/darwin-x64/develop-ff89ffa2b2ef36d02bff0588bb0582cfa8a6002d/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on ff89ffa Oct 18, 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.2/darwin-arm64/develop-ff89ffa2b2ef36d02bff0588bb0582cfa8a6002d/cypress.tgz

Please sign in to comment.