From 05a7ebd3da4444955de0779c0602d0b859abfa52 Mon Sep 17 00:00:00 2001 From: Godfrey Chan Date: Tue, 17 Dec 2024 14:49:47 -0800 Subject: [PATCH] feat(instrumentation-fetch)!: passthrough original response to `applyCustomAttributes` hook Previously, the fetch instrumentation code unconditionally clones every `fetch()` response in order to preserve the ability for the `applyCustomAttributes` hook to consume the response body. This is fundamentally unsound, as it forces the browser to buffer and retain the response body until it is fully received and read, which crates unnecessary memory pressure on large or long-running response streams. In extreme cases, this is effectively a memory leak and can cause the browser tab to crash. Fixes #4888 --- experimental/CHANGELOG.md | 3 ++ .../src/fetch.ts | 3 +- .../test/fetch.test.ts | 46 +++++++++++-------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index d36f0c55c3c..3bf7cf156db 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to experimental packages in this project will be documented ### :boom: Breaking Change +* feat(instrumentation-fetch)!: passthrough original response to `applyCustomAttributes` hook [#5281](https://github.com/open-telemetry/opentelemetry-js/pull/5281) @chancancode + * Previously, the fetch instrumentation code unconditionally clones every `fetch()` response in order to preserve the ability for the `applyCustomAttributes` hook to consume the response body. This is fundamentally unsound, as it forces the browser to buffer and retain the response body until it is fully received and read, which crates unnecessary memory pressure on large or long-running response streams. In extreme cases, this is effectively a memory leak and can cause the browser tab to crash. If your use case for `applyCustomAttributes` requires access to the response body, please chime in on [#5293](https://github.com/open-telemetry/opentelemetry-js/issues/5293). + ### :rocket: (Enhancement) ### :bug: (Bug Fix) diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts index ca19b316593..6a41c5337db 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts @@ -372,7 +372,6 @@ export class FetchInstrumentation extends InstrumentationBase { if (done) { - endSpanOnSuccess(span, resClone4Hook); + endSpanOnSuccess(span, response); } else { read(); } diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts index f0c1830592b..2418b653b59 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts @@ -930,25 +930,33 @@ describe('fetch', () => { }; await prepare(url, applyCustomAttributes); - assert.ok(request.method === 'GET'); - assert.ok(response.status === 200); - }); - - it('get response body from callback arguments response', async () => { - let response: any; - const applyCustomAttributes: FetchCustomAttributeFunction = async ( - span, - req, - res - ) => { - if (res instanceof Response) { - response = res; - } - }; - - await prepare(url, applyCustomAttributes); - const rsp = await response.json(); - assert.strictEqual(rsp.isServerResponse, true); + assert.strictEqual(request.method, 'GET'); + assert.ok(lastResponse !== undefined); + assert.strictEqual(response, lastResponse); + assert.strictEqual(response.status, 200); + + /* + Note: this confirms that nothing *in the instrumentation code* + consumed the response body; it doesn't guarantee that the response + object passed to the `applyCustomAttributes` hook will always have + a consumable body – in fact, this is typically *not* the case: + + ```js + // user code: + let response = await fetch("foo"); + let json = await response.json(); // <- user code consumes the body on `response` + // ... + + { + // ...this is called sometime later... + applyCustomAttributes(span, request, response) { + // too late! + response.bodyUsed // => true + } + } + ``` + */ + assert.strictEqual(response.bodyUsed, false); }); });