fix(browser): log CDP outbound overflow before terminating the client

The outbound queue terminated the automation client silently on overflow, so
the client saw a socket close indistinguishable from a crash. Surface the cap
that tripped and the backlog held when it did.

The queue dropped its backlog before invoking onOverflow, so the counters were
already zero at the callback. Snapshot them first and pass them through.
This commit is contained in:
Neil
2026-09-19 14:57:10 -07:00
parent d12d1e0a06
commit c1ff3b784b
2 changed files with 38 additions and 8 deletions
+11 -1
View File
@@ -34,7 +34,17 @@ export class CdpClientResponseWriter {
byteLengthOf: (frame) => Buffer.byteLength(frame),
getBufferedAmount: () => client.bufferedAmount,
isWritable: () => client.readyState === WebSocket.OPEN,
onOverflow: () => client.terminate(),
onOverflow: (evidence) => {
// The client only sees an abrupt socket close, so name the cap here.
console.warn('[cdp] outbound queue overflow; terminating automation client:', {
cap: evidence.cap,
queuedBytes: evidence.queuedBytes,
queuedFrames: evidence.queuedFrames,
maxQueuedBytes: evidence.maxQueuedBytes,
maxQueuedFrames: evidence.maxQueuedFrames
})
client.terminate()
},
// Preserve large PDF/screenshot replies on a draining connection; queued bursts stay capped.
maxFrameBytes: Number.POSITIVE_INFINITY,
maxDrainFramesPerTurn: 128
+27 -7
View File
@@ -10,6 +10,16 @@
// Generic over the frame type so it serves both the text reply path (encrypted
// base64 strings) and the binary send path (Uint8Array frames).
/** Which hard bound tripped, plus the backlog held when it did. */
export type WsOutboundOverflowEvidence = {
cap: 'maxQueuedBytes' | 'maxQueuedFrames' | 'maxFrameBytes' | 'claimQueuedBytes' | 'sendFailed'
queuedBytes: number
queuedFrames: number
maxQueuedBytes: number
maxQueuedFrames: number
maxFrameBytes: number
}
export type WsOutboundBackpressureQueueOptions<TFrame> = {
/** Send a frame on the wire. Called only when under the soft cap. */
send: (frame: TFrame) => void
@@ -25,8 +35,9 @@ export type WsOutboundBackpressureQueueOptions<TFrame> = {
* Called once when queued bytes exceed maxQueuedBytes — the link is wedged.
* The caller should tear the connection down so a fresh subscription can
* replay an authoritative snapshot. The queue drops its backlog afterward.
* Receives the backlog measured before that drop, so callers can report it.
*/
onOverflow: () => void
onOverflow: (evidence: WsOutboundOverflowEvidence) => void
/** Soft cap: stop draining onto the wire while bufferedAmount is above this. */
softCapBytes?: number
/** Hard cap on bytes held in this queue before onOverflow fires. */
@@ -135,13 +146,22 @@ export function createWsOutboundBackpressureQueue<TFrame>(
stopTimer()
}
const failOverflow = (): void => {
const failOverflow = (cap: WsOutboundOverflowEvidence['cap']): void => {
if (disposed || overflowed) {
return
}
overflowed = true
// Snapshot before dropBacklog() zeroes the counters.
const evidence: WsOutboundOverflowEvidence = {
cap,
queuedBytes: queued,
queuedFrames,
maxQueuedBytes,
maxQueuedFrames,
maxFrameBytes
}
dropBacklog()
options.onOverflow()
options.onOverflow(evidence)
}
const sendFrame = (frame: TFrame): boolean => {
@@ -149,7 +169,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
options.send(frame)
return true
} catch {
failOverflow()
failOverflow('sendFailed')
return false
}
}
@@ -243,7 +263,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
}
const bytes = options.byteLengthOf(frame)
if (!Number.isFinite(bytes) || bytes < 0 || bytes > maxFrameBytes) {
failOverflow()
failOverflow('maxFrameBytes')
return { accepted: false, queued: false, cancel: () => false }
}
// Fast path: nothing parked and the wire is under the cap — send directly.
@@ -261,7 +281,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
}
const queuedBytesClaim = options.claimQueuedBytes?.(bytes)
if (options.claimQueuedBytes && !queuedBytesClaim) {
failOverflow()
failOverflow('claimQueuedBytes')
return { accepted: false, queued: false, cancel: () => false }
}
const entry: QueueEntry = {
@@ -274,7 +294,7 @@ export function createWsOutboundBackpressureQueue<TFrame>(
queued += bytes
queuedFrames += 1
if (queued > maxQueuedBytes || queuedFrames > maxQueuedFrames) {
failOverflow()
failOverflow(queued > maxQueuedBytes ? 'maxQueuedBytes' : 'maxQueuedFrames')
return { accepted: false, queued: false, cancel: () => false }
}
if (timer === null) {