From c613cadf738836b2f5b59cb51ae480220cf5967b Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 21 Nov 2025 18:40:07 -0500 Subject: [PATCH 1/2] fix: prevent interception of requests when Studio is not opened --- packages/proxy/lib/http/response-middleware.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 930495d1e42..31f651b77b0 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -937,7 +937,10 @@ const MaybeInjectServiceWorker: ResponseMiddleware = function () { } const GzipBody: ResponseMiddleware = async function () { - if (this.protocolManager && this.req.browserPreRequest?.requestId) { + // Only process streams through protocol manager if it's actually enabled + // Protocol manager is only enabled when Studio is active (setupProtocol() has been called) + // This prevents intercepting streams when Studio panel is not open + if (this.protocolManager?.isProtocolEnabled && this.req.browserPreRequest?.requestId) { const preRequest = this.req.browserPreRequest const requestId = getOriginalRequestId(preRequest.requestId) From 303e99001f619108803d463e66118b0153a6795b Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 24 Nov 2025 11:19:37 -0500 Subject: [PATCH 2/2] don't use protocol in response-middleware if not in Studio + AI --- .../proxy/lib/http/response-middleware.ts | 137 +++++++++++++----- packages/server/lib/cloud/protocol.ts | 14 ++ packages/server/lib/project-base.ts | 1 + packages/types/src/protocol.ts | 3 + 4 files changed, 116 insertions(+), 39 deletions(-) diff --git a/packages/proxy/lib/http/response-middleware.ts b/packages/proxy/lib/http/response-middleware.ts index 31f651b77b0..dda3b565e29 100644 --- a/packages/proxy/lib/http/response-middleware.ts +++ b/packages/proxy/lib/http/response-middleware.ts @@ -788,22 +788,44 @@ const ClearCyInitialCookie: ResponseMiddleware = function () { const MaybeEndWithEmptyBody: ResponseMiddleware = function () { if (httpUtils.responseMustHaveEmptyBody(this.req, this.incomingRes)) { - if (this.protocolManager && this.req.browserPreRequest?.requestId) { - const requestId = getOriginalRequestId(this.req.browserPreRequest.requestId) - - this.protocolManager.responseEndedWithEmptyBody({ - requestId, - isCached: this.incomingRes.statusCode === 304, - timings: { - cdpRequestWillBeSentTimestamp: this.req.browserPreRequest.cdpRequestWillBeSentTimestamp, - cdpRequestWillBeSentReceivedTimestamp: this.req.browserPreRequest.cdpRequestWillBeSentReceivedTimestamp, - proxyRequestReceivedTimestamp: this.req.browserPreRequest.proxyRequestReceivedTimestamp, - cdpLagDuration: this.req.browserPreRequest.cdpLagDuration, - proxyRequestCorrelationDuration: this.req.browserPreRequest.proxyRequestCorrelationDuration, - }, - }) + // For empty body responses (204, 304, HEAD), we must end the response immediately + // Optionally notify protocol manager if it's enabled and Studio is active + const protocolManager = this.protocolManager + + if (protocolManager && this.req.browserPreRequest?.requestId) { + const isEnabled = protocolManager.isProtocolEnabled + const mode = (protocolManager as any).mode as 'record' | 'studio' | undefined + + // For studio mode, verify Studio is actually active before processing + // isProtocolEnabled already checks if _protocol exists (set in setupProtocol) + // setupProtocol is only called when Studio is initialized and canAccessStudioAI is true + // So if isProtocolEnabled is true in studio mode, Studio must be active + // However, we add an extra safety check for dbPath to ensure Studio is fully initialized + const hasDbPath = !!protocolManager.dbPath + const isStudioActive = mode === 'studio' ? hasDbPath : true + const shouldProcess = isEnabled && isStudioActive + + if (shouldProcess) { + const browserPreRequest = this.req.browserPreRequest + const requestId = getOriginalRequestId(browserPreRequest.requestId) + + this.debug('response-middleware:response-ended-with-empty-body:protocol-notification %o', { requestId }) + + protocolManager.responseEndedWithEmptyBody({ + requestId, + isCached: this.incomingRes.statusCode === 304, + timings: { + cdpRequestWillBeSentTimestamp: browserPreRequest.cdpRequestWillBeSentTimestamp, + cdpRequestWillBeSentReceivedTimestamp: browserPreRequest.cdpRequestWillBeSentReceivedTimestamp, + proxyRequestReceivedTimestamp: browserPreRequest.proxyRequestReceivedTimestamp, + cdpLagDuration: browserPreRequest.cdpLagDuration, + proxyRequestCorrelationDuration: browserPreRequest.proxyRequestCorrelationDuration, + }, + }) + } } + // Always end empty body responses, regardless of protocol manager state this.res.end() return this.end() @@ -937,36 +959,73 @@ const MaybeInjectServiceWorker: ResponseMiddleware = function () { } const GzipBody: ResponseMiddleware = async function () { - // Only process streams through protocol manager if it's actually enabled - // Protocol manager is only enabled when Studio is active (setupProtocol() has been called) - // This prevents intercepting streams when Studio panel is not open - if (this.protocolManager?.isProtocolEnabled && this.req.browserPreRequest?.requestId) { - const preRequest = this.req.browserPreRequest + // Call protocol manager if it exists and we have a browser pre-request + // The protocol manager's invokeSync will return undefined if _protocol doesn't exist, + // so it's safe to call even when the protocol is not enabled + // However, for studio mode, we only want to actually process/record when Studio is active + const protocolManager = this.protocolManager + + if (protocolManager && this.req.browserPreRequest?.requestId) { + const preRequest = this.req.browserPreRequest! const requestId = getOriginalRequestId(preRequest.requestId) + const mode = (protocolManager as any).mode as 'record' | 'studio' | undefined + const isEnabled = protocolManager.isProtocolEnabled + const hasDbPath = !!protocolManager.dbPath - const span = telemetry.startSpan({ name: 'gzip:body:protocol-notification', parentSpan: this.resMiddlewareSpan, isVerbose }) - - const resultingStream = this.protocolManager.responseStreamReceived({ - requestId, - responseHeaders: this.incomingRes.headers, - isAlreadyGunzipped: this.isGunzipped, - responseStream: this.incomingResStream, - res: this.res, - timings: { - cdpRequestWillBeSentTimestamp: preRequest.cdpRequestWillBeSentTimestamp, - cdpRequestWillBeSentReceivedTimestamp: preRequest.cdpRequestWillBeSentReceivedTimestamp, - proxyRequestReceivedTimestamp: preRequest.proxyRequestReceivedTimestamp, - cdpLagDuration: preRequest.cdpLagDuration, - proxyRequestCorrelationDuration: preRequest.proxyRequestCorrelationDuration, - }, - }) + // For studio mode, only actually process if Studio is active (dbPath exists) + // For record mode, process if protocol is enabled + // Note: We still call responseStreamReceived even if shouldProcess is false, + // because invokeSync will return undefined safely, but we skip the telemetry/logging + const shouldProcess = mode === 'studio' + ? (isEnabled && hasDbPath) // Studio mode: need both enabled and dbPath + : isEnabled // Record mode: just need enabled - if (resultingStream) { - this.incomingResStream = resultingStream.on('error', this.onError).once('close', () => { - span?.end() + if (shouldProcess) { + const span = telemetry.startSpan({ name: 'gzip:body:protocol-notification', parentSpan: this.resMiddlewareSpan, isVerbose }) + + this.debug('response-middleware:gzip:body:protocol-notification %o', { requestId }) + + const resultingStream = protocolManager.responseStreamReceived({ + requestId, + responseHeaders: this.incomingRes.headers, + isAlreadyGunzipped: this.isGunzipped, + responseStream: this.incomingResStream, + res: this.res, + timings: { + cdpRequestWillBeSentTimestamp: preRequest.cdpRequestWillBeSentTimestamp, + cdpRequestWillBeSentReceivedTimestamp: preRequest.cdpRequestWillBeSentReceivedTimestamp, + proxyRequestReceivedTimestamp: preRequest.proxyRequestReceivedTimestamp, + cdpLagDuration: preRequest.cdpLagDuration, + proxyRequestCorrelationDuration: preRequest.proxyRequestCorrelationDuration, + }, }) + + if (resultingStream) { + this.incomingResStream = resultingStream.on('error', this.onError).once('close', () => { + span?.end() + }) + } else { + span?.end() + } } else { - span?.end() + // Protocol manager exists but shouldn't process (e.g., studio mode without active Studio) + // Still call responseStreamReceived - it will return undefined safely via invokeSync + // This maintains the original behavior and ensures stream flow is not disrupted + protocolManager.responseStreamReceived({ + requestId, + responseHeaders: this.incomingRes.headers, + isAlreadyGunzipped: this.isGunzipped, + responseStream: this.incomingResStream, + res: this.res, + timings: { + cdpRequestWillBeSentTimestamp: preRequest.cdpRequestWillBeSentTimestamp, + cdpRequestWillBeSentReceivedTimestamp: preRequest.cdpRequestWillBeSentReceivedTimestamp, + proxyRequestReceivedTimestamp: preRequest.proxyRequestReceivedTimestamp, + cdpLagDuration: preRequest.cdpLagDuration, + proxyRequestCorrelationDuration: preRequest.proxyRequestCorrelationDuration, + }, + }) + // resultingStream will be undefined, so we continue with original stream } } diff --git a/packages/server/lib/cloud/protocol.ts b/packages/server/lib/cloud/protocol.ts index 5d4c848b803..6e3643b41d1 100644 --- a/packages/server/lib/cloud/protocol.ts +++ b/packages/server/lib/cloud/protocol.ts @@ -49,11 +49,24 @@ export class ProtocolManager implements ProtocolManagerShape { private AppCaptureProtocol: AppCaptureProtocolConstructor | undefined private options: ProtocolManagerOptions | undefined + private _canAccessStudioAI: boolean = false get isProtocolEnabled (): boolean { return !!this._protocol } + get mode (): 'record' | 'studio' | undefined { + return this.options?.mode + } + + get canAccessStudioAI (): boolean { + return this._canAccessStudioAI + } + + setCanAccessStudioAI (canAccess: boolean): void { + this._canAccessStudioAI = canAccess + } + get networkEnableOptions () { return this.isProtocolEnabled ? { maxTotalBufferSize: 0, @@ -451,6 +464,7 @@ export class ProtocolManager implements ProtocolManagerShape { this._runId = undefined this._errors = [] this._protocol = undefined + this._canAccessStudioAI = false } /** diff --git a/packages/server/lib/project-base.ts b/packages/server/lib/project-base.ts index f7d64b2905b..e0718ea9140 100644 --- a/packages/server/lib/project-base.ts +++ b/packages/server/lib/project-base.ts @@ -513,6 +513,7 @@ export class ProjectBase extends EE { } this.protocolManager = studio.protocolManager + this.protocolManager.setCanAccessStudioAI(canAccessStudioAI) this.protocolManager.setupProtocol() this.protocolManager.beforeSpec({ ...this.spec, diff --git a/packages/types/src/protocol.ts b/packages/types/src/protocol.ts index 8aabd1e8245..409c452d24a 100644 --- a/packages/types/src/protocol.ts +++ b/packages/types/src/protocol.ts @@ -128,6 +128,9 @@ export type AfterSpecDurations = { export interface ProtocolManagerShape extends AppCaptureProtocolCommon { isProtocolEnabled: boolean + mode?: 'record' | 'studio' + canAccessStudioAI: boolean + setCanAccessStudioAI(canAccess: boolean): void networkEnableOptions?: { maxTotalBufferSize: number, maxResourceBufferSize: number, maxPostDataSize: number } setupProtocol(): void prepareProtocol (script: string, options: ProtocolManagerOptions): Promise