fix(stdlib): Instrument response body read for chunked HTTP responses#6202
fix(stdlib): Instrument response body read for chunked HTTP responses#6202sentrivana wants to merge 10 commits intomasterfrom
Conversation
The HTTP client span was being finished in getresponse(), which only waits for response headers. For chunked (or large) responses, the actual data transfer happens later during read(), leaving that time uninstrumented. Defer span completion to HTTPResponse.read() for responses with a body, with HTTPResponse.close() as a safety net for responses that are never read. Responses with no body (Content-Length: 0, HEAD, 204, 304) still finish the span immediately in getresponse(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Codecov Results 📊✅ 13 passed | Total: 13 | Pass Rate: 100% | Execution Time: 10.42s All tests are passing successfully. ❌ Patch coverage is 18.60%. Project has 14987 uncovered lines. Files with missing lines (1)
Generated by Codecov Action |
Codecov Results 📊✅ 6 passed | Total: 6 | Pass Rate: 100% | Execution Time: 1.01s 📊 Comparison with Base Branch
✨ No test changes detected All tests are passing successfully. ❌ Patch coverage is 18.18%. Project has 17057 uncovered lines. Files with missing lines (1)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
- Coverage 22.11% 21.31% -0.8%
==========================================
Files 190 190 —
Lines 21789 21675 -114
Branches 7302 7619 +317
==========================================
+ Hits 4817 4618 -199
- Misses 16972 17057 +85
- Partials 386 385 -1Generated by Codecov Action |
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the `not rv` check from the read() wrapper's span-finishing condition. read(0) legitimately returns b"" without consuming the body, which would falsely trigger span completion. The fp and closed checks are sufficient to detect when the body is fully consumed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| return event | ||
|
|
||
|
|
||
| def _finish_span(span: "Union[Span, StreamedSpan]") -> None: |
There was a problem hiding this comment.
Consider this only if you foresee this function being reused in other places:
Because "finish" appears to have a specific association with the legacy transaction-based span approach, perhaps this should instead be called _complete_span, so it works in both streamed and legacy contexts.
| span = getattr(self, "_sentrysdk_span", None) | ||
|
|
||
| return rv | ||
| if span is None: |
There was a problem hiding this comment.
Could this potentially be placed within the finally block (similar to what you did below in close)?
try:
return real_read(self, *args, **kwargs)
finally:
# read() might be called multiple times to consume a single body,
# so we can't just end the span when read() is done. Instead,
# try to figure out whether the response body has been fully read.
if span and (self.fp is None or self.closed):
self._sentrysdk_span = None # type: ignore[attr-defined]
_finish_span(span)or
try:
return real_read(self, *args, **kwargs)
finally:
if span is None:
return
# read() might be called multiple times to consume a single body,
# so we can't just end the span when read() is done. Instead,
# try to figure out whether the response body has been fully read.
if self.fp is None or self.closed:
self._sentrysdk_span = None # type: ignore[attr-defined]
_finish_span(span)| spans = [item.payload for item in items if item.type == "span"] | ||
| (span,) = ( | ||
| span | ||
| for span in spans | ||
| if span["attributes"].get("sentry.origin") == "auto.http.stdlib.httplib" | ||
| ) |
There was a problem hiding this comment.
Similar to the pattern that's been introduced in other integrations for similar tests, we should access the span using array indexes (i.e. span[0]) before asserting the duration to ensure that the span of interest is where we expect it to be in the generated list of spans.
The HTTP client span in the stdlib integration was finishing in
getresponse(), which only waits for response headers, not the actual response. For chunked or large responses, the actual data transfer happens duringread(), leaving that time uninstrumented.Defer span completion to
HTTPResponse.read()for responses with a body (chunked or Content-Length > 0), withHTTPResponse.close()as a safety net for responses that are never read.close()is called). In a sense we're essentially flipping the current situation (where we report requests to be much shorter than they are, since they don't include the response part) -- but the current situation was incorrect for all spans, while thisclose()fallback should hopefully only kick in for edge cases.Responses with no body (Content-Length: 0, HEAD, 204, 304) still finish the span immediately in
getresponse().requestslibrary requests #2277