fix(inspector): restore client console and response bodies

This commit is contained in:
imccyu
2026-08-27 16:16:27 +08:00
parent 008ae0c01e
commit daf3858759
4 changed files with 57 additions and 17 deletions
@@ -117,19 +117,9 @@ export function installFetchObserver(
responseClone.body,
options.maxResponseBodyBytes,
options.maxChunkBytes,
AbortSignal.any([controller.signal, request.signal]),
controller.signal,
(data) => { publisher.publish('fetch/response-body-chunk', { requestId, data }) },
).then((outcome) => {
if (request.signal.aborted) {
publisher.publish('fetch/error', {
requestId,
message: request.signal.reason === undefined
? 'AbortError: request aborted during response body capture'
: renderError(request.signal.reason),
canceled: true,
})
return
}
publisher.publish('fetch/end', {
requestId,
capturedBytes: outcome.capturedBytes,
@@ -210,7 +200,7 @@ async function captureBody(
}
return { capturedBytes, truncated }
} catch (error) {
return { capturedBytes, truncated, captureError: renderError(error) }
return { capturedBytes, truncated: true, captureError: renderError(error) }
} finally {
signal.removeEventListener('abort', abort)
reader.releaseLock()
@@ -143,8 +143,6 @@ function assertClientEvaluationOptions(request: Parameters<RuntimeBackend['evalu
if (request.throwOnSideEffect === true) throw new Error('Client Runtime does not support throwOnSideEffect')
if (request.serializationOptions !== undefined) throw new Error('Client Runtime does not support serializationOptions')
if (request.disableBreaks === true) throw new Error('Client Runtime does not support disableBreaks')
if (request.replMode === true) throw new Error('Client Runtime does not support replMode')
if (request.userGesture === true) throw new Error('Client Runtime does not support userGesture')
if (request.allowUnsafeEvalBlockedByCSP === true) {
throw new Error('Client Runtime cannot bypass the page Content Security Policy')
}
@@ -147,8 +147,28 @@ describe.skipIf(!built)('Inspector built Client in Chromium', () => {
const context = event.params?.context as Record<string, unknown> | undefined
return String(context?.name).startsWith('Client —')
})
const contextId = (contextEvent.params?.context as Record<string, unknown>).id
const context = contextEvent.params?.context as Record<string, unknown>
const contextId = context.id
const uniqueContextId = context.uniqueId
expect(contextId).toBeTypeOf('number')
expect(uniqueContextId).toBeTypeOf('string')
const evaluated = await cdp.call('Runtime.evaluate', {
expression: 'globalThis.__inspectorConsoleEvaluation = { answer: 6 * 7 }',
objectGroup: 'console',
includeCommandLineAPI: true,
silent: false,
returnByValue: false,
generatePreview: true,
userGesture: true,
awaitPromise: false,
replMode: true,
allowUnsafeEvalBlockedByCSP: false,
uniqueContextId,
})
expect(evaluated.error).toBeUndefined()
expect(asRecord(evaluated.result?.result).objectId).toMatch(/^runtime:/u)
expect(await page.evaluate(() => Reflect.get(globalThis, '__inspectorConsoleEvaluation'))).toEqual({ answer: 42 })
await page.evaluate(() => {
const value = { browser: true, nested: { ready: true } }
@@ -79,7 +79,7 @@ describe('full fetch observer', () => {
expect(payload(records, 'fetch/end')).toMatchObject({ capturedBytes: 4, responseBodyTruncated: true })
})
it('reports cancellation after response headers as a canceled request', async () => {
it('retains a truncated response when the caller cancels after response headers', async () => {
const records: InspectorRecordInput[] = []
Object.defineProperty(globalThis, 'fetch', {
value: vi.fn(async (request: Request) => new Response(new ReadableStream<Uint8Array>({
@@ -103,9 +103,41 @@ describe('full fetch observer', () => {
const response = await fetch('https://example.test/cancel-body', { signal: abort.signal })
abort.abort()
await expect(response.text()).rejects.toThrow()
await vi.waitFor(() => { expect(records.some(record => record.topic === 'fetch/error')).toBe(true) })
await vi.waitFor(() => { expect(records.some(record => record.topic === 'fetch/end')).toBe(true) })
expect(decodeChunks(records, 'fetch/response-body-chunk')).toBe('first')
expect(payload(records, 'fetch/end')).toMatchObject({
capturedBytes: 5,
responseBodyTruncated: true,
responseCaptureError: 'AbortError: aborted',
})
expect(records.some(record => record.topic === 'fetch/error')).toBe(false)
})
it('reports a fetch rejected before response headers as a canceled request', async () => {
const records: InspectorRecordInput[] = []
Object.defineProperty(globalThis, 'fetch', {
value: vi.fn(async (request: Request) => await new Promise<Response>((_resolve, reject) => {
request.signal.addEventListener('abort', () => {
reject(new DOMException('aborted', 'AbortError'))
}, { once: true })
})),
writable: true,
configurable: true,
})
observer = installFetchObserver({
publish(topic: string, payload: InspectorJsonValue, monotonicMs = performance.now()) {
records.push({ topic, payload, monotonicMs })
},
}, { maxRequestBodyBytes: 1_024, maxResponseBodyBytes: 1_024, maxChunkBytes: 4 })
const abort = new AbortController()
const pending = fetch('https://example.test/cancel-before-response', { signal: abort.signal })
abort.abort()
await expect(pending).rejects.toThrow()
expect(payload(records, 'fetch/error')).toMatchObject({ canceled: true })
expect(records.some(record => record.topic === 'fetch/response')).toBe(false)
expect(records.some(record => record.topic === 'fetch/end')).toBe(false)
})
})