Skip to content

Commit 04e74d7

Browse files
authored
feat(instrumentation-fetch)! Passthrough original request to applyCustomAttributes (#5281)
1 parent 42eb824 commit 04e74d7

File tree

3 files changed

+31
-21
lines changed

3 files changed

+31
-21
lines changed

experimental/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ All notable changes to experimental packages in this project will be documented
77

88
### :boom: Breaking Change
99

10+
* feat(instrumentation-fetch)!: passthrough original response to `applyCustomAttributes` hook [#5281](https://github.com/open-telemetry/opentelemetry-js/pull/5281) @chancancode
11+
* 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).
12+
1013
### :rocket: (Enhancement)
1114

1215
### :bug: (Bug Fix)

experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -372,15 +372,14 @@ export class FetchInstrumentation extends InstrumentationBase<FetchInstrumentati
372372
): void {
373373
try {
374374
const resClone = response.clone();
375-
const resClone4Hook = response.clone();
376375
const body = resClone.body;
377376
if (body) {
378377
const reader = body.getReader();
379378
const read = (): void => {
380379
reader.read().then(
381380
({ done }) => {
382381
if (done) {
383-
endSpanOnSuccess(span, resClone4Hook);
382+
endSpanOnSuccess(span, response);
384383
} else {
385384
read();
386385
}

experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -930,25 +930,33 @@ describe('fetch', () => {
930930
};
931931

932932
await prepare(url, applyCustomAttributes);
933-
assert.ok(request.method === 'GET');
934-
assert.ok(response.status === 200);
935-
});
936-
937-
it('get response body from callback arguments response', async () => {
938-
let response: any;
939-
const applyCustomAttributes: FetchCustomAttributeFunction = async (
940-
span,
941-
req,
942-
res
943-
) => {
944-
if (res instanceof Response) {
945-
response = res;
946-
}
947-
};
948-
949-
await prepare(url, applyCustomAttributes);
950-
const rsp = await response.json();
951-
assert.strictEqual(rsp.isServerResponse, true);
933+
assert.strictEqual(request.method, 'GET');
934+
assert.ok(lastResponse !== undefined);
935+
assert.strictEqual(response, lastResponse);
936+
assert.strictEqual(response.status, 200);
937+
938+
/*
939+
Note: this confirms that nothing *in the instrumentation code*
940+
consumed the response body; it doesn't guarantee that the response
941+
object passed to the `applyCustomAttributes` hook will always have
942+
a consumable body – in fact, this is typically *not* the case:
943+
944+
```js
945+
// user code:
946+
let response = await fetch("foo");
947+
let json = await response.json(); // <- user code consumes the body on `response`
948+
// ...
949+
950+
{
951+
// ...this is called sometime later...
952+
applyCustomAttributes(span, request, response) {
953+
// too late!
954+
response.bodyUsed // => true
955+
}
956+
}
957+
```
958+
*/
959+
assert.strictEqual(response.bodyUsed, false);
952960
});
953961
});
954962

0 commit comments

Comments
 (0)