Skip to content

fix(stdlib): Instrument response body read for chunked HTTP responses#6202

Open
sentrivana wants to merge 10 commits intomasterfrom
ivana/requests-missing-instrumentation
Open

fix(stdlib): Instrument response body read for chunked HTTP responses#6202
sentrivana wants to merge 10 commits intomasterfrom
ivana/requests-missing-instrumentation

Conversation

@sentrivana
Copy link
Copy Markdown
Contributor

@sentrivana sentrivana commented May 5, 2026

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 during read(), leaving that time uninstrumented.

Defer span completion to HTTPResponse.read() for responses with a body (chunked or Content-Length > 0), with HTTPResponse.close() as a safety net for responses that are never read.

⚠️ Note that this means we might report some requests to be longer than they actually were, since in some cases we only close them once the GC gets to them (and 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 this close() 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().

sentrivana and others added 2 commits May 5, 2026 13:30
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>
Comment thread sentry_sdk/integrations/stdlib.py
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

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)
File Patch % Lines
stdlib.py 49.75% ⚠️ 99 Missing and 15 partials

Generated by Codecov Action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Codecov Results 📊

6 passed | Total: 6 | Pass Rate: 100% | Execution Time: 1.01s

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

❌ Patch coverage is 18.18%. Project has 17057 uncovered lines.
❌ Project coverage is 21.31%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
stdlib.py 28.79% ⚠️ 141 Missing and 2 partials
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        -1

Generated by Codecov Action

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 5, 2026

Comment thread sentry_sdk/integrations/stdlib.py Outdated
Comment thread sentry_sdk/integrations/stdlib.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread tests/integrations/stdlib/test_httplib.py Outdated
sentrivana and others added 3 commits May 5, 2026 14:04
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>
@sentrivana sentrivana marked this pull request as ready for review May 5, 2026 12:22
@sentrivana sentrivana requested a review from a team as a code owner May 5, 2026 12:22
Comment thread sentry_sdk/integrations/stdlib.py
@sentrivana sentrivana marked this pull request as draft May 5, 2026 12:30
@sentrivana sentrivana marked this pull request as ready for review May 5, 2026 12:32
Comment thread sentry_sdk/integrations/stdlib.py
return event


def _finish_span(span: "Union[Span, StreamedSpan]") -> None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +1222 to +1227
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"
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sentry does not fully instrument requests library requests

2 participants