fix(httplib): finish the span when a RASP block raises through the wrapper - #20073
Conversation
…apper [APPSEC-70027] Exploit Prevention blocks an outgoing request by raising BlockingException, which derives from BaseException so that no intermediate handler can swallow a blocking decision. _wrap_request and _wrap_putrequest guard the wrapped call with "except Exception", so the block goes straight past them. The span has already been created and activated by tracer.trace() and attached to the connection, so it is never finished: it is never flushed, it stays current for the rest of the thread's life, and every later span on that thread is parented to it. Measured with the integration and appsec both patched, blocking from HTTPConnection.request: span created: True span finished: False still current: True Shutting down tracer with 1 spans. These spans will not be sent to Datadog Reachable on main today, but only in one patch order. When appsec patches the attribute after the integration, its wrapt wrapper sits outside _wrap_request and the block raises before the span exists; when appsec patches first, the integration wraps appsec's wrapper and the block happens inside the span. patch order main httplib contrib first clean, no span created appsec first leaked Catching BaseException and re-raising is what the rest of the tracer already does for span cleanup - redis_utils, valkey_utils, elasticsearch, asgi, pyramid, tornado and tracer.py all do the same. _wrap_getresponse already used try/finally and was unaffected. The two tests raise a real BlockingException from inside the wrapped call, so they need no network and no appsec setup. Both fail without the fix. contrib::httplib green on py3.12. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codeowners resolved asResolved from the full PR diff against No remaining files require a CODEOWNERS review. |
Circular import analysis
|
Dependency direction analysis
|
…877] The httplib span leak is a pre-existing bug in shared integration code, not part of this migration: on main it already strands the span whenever appsec patched http.client before the integration did. Only the second order was safe, because appsec's wrapt wrapper then sat outside _wrap_request and the block raised before the span existed. It belongs on its own, so it moves to #20073 (APPSEC-70027) with tests that raise a real BlockingException from inside the wrapped call and therefore need neither the network nor appsec. The regression test added here goes with it - its contract is the same one those two cover, and keeping a copy that cannot pass until #20073 lands would only make this branch red. This branch now depends on #20073: a wrapping context lives in the target's own bytecode, so it is always innermost and the block always raises inside the span, which removes the order that used to avoid the leak. Merge main in once #20073 has landed. test_common_modules 95 passed on py3.13. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bf12a2503d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…arly [APPSEC-70027] Connections are reused, and _wrap_putrequest adopts whatever _datadog_span the connection already carries. Only _wrap_getresponse ever detached it, so all four exception handlers finished the span and left it attached: the next request on that connection then reused an already-finished span and reported nothing. Reproduced before fixing: assert not hasattr(conn, "_datadog_span") E assert not True The four handlers had drifted into four slightly different copies of finish-the -span, so they collapse into one _finish_span helper that also detaches. That covers the two configuration-error handlers as well as the two around the wrapped call, since the defect was identical in all of them. Also reworks the BaseException comment. It described what the old code got wrong, which stops making sense once this is merged, so it now states the invariant instead, and carries the AIDEV-NOTE anchor that AGENTS.md asks for on non-obvious lifecycle knowledge. The two existing tests read the span back off the connection, which no longer works now that it is detached; they assert through pop_spans instead, which is the better check anyway. contrib::httplib 102 passed on py3.12. No-oping the delattr fails the new test. Reported by christophe-papazian, dubloom and Codex on #20073. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
Jira: APPSEC-70027
Exploit Prevention blocks an outgoing request by raising
BlockingException, which derives fromBaseExceptionso that no intermediate handler can swallow a blocking decision._wrap_requestand_wrap_putrequestguard the wrapped call withexcept Exception:The block goes straight past that handler. The span has already been created and activated by
tracer.trace()and attached to the connection, so it is never finished: it is never flushed, it stays current for the rest of the thread's life, and every later span on that thread is parented to it.Measured with the integration and appsec both patched, blocking from
HTTPConnection.request:Order dependence
Reachable on
maintoday, but only in one patch order:mainWhen appsec patches the attribute after the integration, its wrapt wrapper sits outside
_wrap_requestand the block raises before the span exists. When appsec patches first, the integration wraps appsec's wrapper and the block happens inside the span.Why now
The AppSec RASP migration to
WrappingContext(#19989, APPSEC-69878) removes the order that avoids this. A wrapping context lives in the target's own bytecode, so it is always innermost and the block always raises inside the span. #19989 depends on this landing first. Found by Codex review on that PR, split out here because it is a pre-existing bug in shared integration code rather than part of the migration.Fix
Catch
BaseExceptionaround the wrapped call in both wrappers and re-raise. That is what the rest of the tracer already does for span cleanup —redis_utils,valkey_utils,elasticsearch,asgi,pyramid,tornadoandtracer.py._wrap_getresponsealready usedtry/finallyand was unaffected.Testing
Two tests in
tests/contrib/httplib/test_httplib.pyraise a realBlockingExceptionfrom inside the wrapped call, so they need no network and no appsec setup:test_span_is_finished_when_the_request_raises_a_base_exceptiontest_span_is_finished_when_putrequest_raises_a_base_exceptionBoth assert the span is finished and no span is left current. Mutation-checked — reverting to
except Exceptionfails both.contrib::httplibgreen on py3.12.scripts/lint fmtclean.Checklist
Reviewer Checklist
🤖 Generated with Claude Code