Repository navigation
Stop AppSecInterceptor from silently retrying failed okhttp requests - #12242
gh-worker-dd-mergequeue-cf854d[bot] merged 11 commits into
Conversation
chain.proceed(request) was wrapped in the same try/catch that guards the AppSec request/response hooks, so any IOException from the real network call (e.g. ConnectException) was swallowed and the request was silently retried via chain.proceed(chain.request()). This double- executes non-idempotent requests on transient network failures and surfaces the retry's own failure as an unhandled error blamed on the interceptor. Narrow the try/catch to only cover the AppSec hooks so genuine I/O failures propagate normally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
Covers both okhttp-2.2 and okhttp-3.0 AppSecInterceptor.intercept(): asserts an IOException from chain.proceed() propagates without being swallowed/retried, using Mockito + AgentTracer.forceRegister instead of Groovy/Spock. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
More details
The network call is outside both hook exception handlers. The first I/O failure now passes to the caller, with no silent retry in either OkHttp integration.
🤖 Datadog Autotest · Commit 161655c · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
There was a problem hiding this comment.
A sampled response body can become closed or empty when the response hook fails after body capture. Both interceptor copies return the consumed original response instead of the rebuilt response.
🤖 Datadog Autotest · Commit bfe37e7 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest
amarziali
left a comment
There was a problem hiding this comment.
Thanks for having fixed that. I left a minor improvement
span.getTag(Tags.HTTP_URL) can return null, and .toString() on it throws an NPE that silently skips the AppSec request hook for that call. Null-check instead of relying on the catch-all to swallow it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@ygree friendly nudge — this one's been waiting on your review, would appreciate a look when you have a chance. |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c6a82ccee1
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🛡️ Codex Security Review · Automatically triggeredSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
ygree
left a comment
There was a problem hiding this comment.
IDM approved. Please also check codex's finding: https://github.com/DataDog/dd-trace-java/pull/12242/changes#r3984229637
onResponse() reads and closes the original response body, then rebuilds a Response with a fresh body before calling publish(). If publish()'s WAF/ gateway callback throws a non-blocking exception, that exception used to propagate out of onResponse() entirely, and intercept()'s outer catch fell back to the original (already-drained/closed) response instead of the rebuilt one -- handing the caller an empty or closed body. Guard publish() locally so a non-blocking failure there no longer discards the rebuilt response; BlockingException still propagates as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…plicate-request' into dougqh/fix-appsec-interceptor-duplicate-request
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|
…12242) Stop AppSecInterceptor from silently retrying failed okhttp requests chain.proceed(request) was wrapped in the same try/catch that guards the AppSec request/response hooks, so any IOException from the real network call (e.g. ConnectException) was swallowed and the request was silently retried via chain.proceed(chain.request()). This double- executes non-idempotent requests on transient network failures and surfaces the retry's own failure as an unhandled error blamed on the interceptor. Narrow the try/catch to only cover the AppSec hooks so genuine I/O failures propagate normally. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Add JUnit5 regression test for AppSecInterceptor silent-retry fix Covers both okhttp-2.2 and okhttp-3.0 AppSecInterceptor.intercept(): asserts an IOException from chain.proceed() propagates without being swallowed/retried, using Mockito + AgentTracer.forceRegister instead of Groovy/Spock. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Merge branch 'master' into dougqh/fix-appsec-interceptor-duplicate-request Merge branch 'master' into dougqh/fix-appsec-interceptor-duplicate-request Merge branch 'master' into dougqh/fix-appsec-interceptor-duplicate-request Guard against NPE reading HTTP_URL tag in AppSecInterceptor span.getTag(Tags.HTTP_URL) can return null, and .toString() on it throws an NPE that silently skips the AppSec request hook for that call. Null-check instead of relying on the catch-all to swallow it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Merge branch 'master' into dougqh/fix-appsec-interceptor-duplicate-request Merge branch 'master' into dougqh/fix-appsec-interceptor-duplicate-request Merge branch 'master' into dougqh/fix-appsec-interceptor-duplicate-request Preserve rebuilt response body when the AppSec response hook fails onResponse() reads and closes the original response body, then rebuilds a Response with a fresh body before calling publish(). If publish()'s WAF/ gateway callback throws a non-blocking exception, that exception used to propagate out of onResponse() entirely, and intercept()'s outer catch fell back to the original (already-drained/closed) response instead of the rebuilt one -- handing the caller an empty or closed body. Guard publish() locally so a non-blocking failure there no longer discards the rebuilt response; BlockingException still propagates as before. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Merge remote-tracking branch 'origin/dougqh/fix-appsec-interceptor-duplicate-request' into dougqh/fix-appsec-interceptor-duplicate-request Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
What Does This Do
Narrows the
try/catchinAppSecInterceptor.intercept()(bothokhttp-2.2andokhttp-3.0) so it only guards the AppSec-specific hook logic (sampleRequest/onRequest/onResponse), not the realchain.proceed()network call. Genuine I/O failures now propagate normally on the first (and only) attempt, exactly as they would without this instrumentation.Motivation
AppSecInterceptor.intercept()previously wrapped the realchain.proceed(request)call inside the sametry/catch (Exception e)that guards the AppSec hooks. AnyIOExceptionfrom the actual outbound request (e.g.java.net.ConnectExceptionon a transient network failure) was caught by that handler, logged at debug, and the request was silently retried viachain.proceed(chain.request()). This means:IOExceptionpropagates unhandled and gets misattributed toAppSecInterceptor.interceptin error tracking, when the real cause is just an ordinary network failure.Found via Datadog Error Tracking issue 7d2a0a46-aab1-11f0-a468-da7ad0900002 (38k+ occurrences) — automated root cause analysis confirmed the stack shows
ConnectExceptionpropagating throughAppSecInterceptor:57/58, matching this code path exactly. Same shape exists in the okhttp2 sibling (db728b88-b44c-11f0-a740-da7ad0900002).Additional Notes
AppSecInterceptorbehavior itself in either module; happy to add one covering the retry regression if desired.Test plan
./gradlew :dd-java-agent:instrumentation:okhttp:okhttp-3.0:compileJava :dd-java-agent:instrumentation:okhttp:okhttp-2.2:compileJava— BUILD SUCCESSFUL./gradlew :dd-java-agent:instrumentation:okhttp:okhttp-3.0:spotlessApply :dd-java-agent:instrumentation:okhttp:okhttp-2.2:spotlessApply— applied, no additional changes./gradlew :dd-java-agent:instrumentation:okhttp:okhttp-3.0:test :dd-java-agent:instrumentation:okhttp:okhttp-2.2:test— BUILD SUCCESSFUL, existing instrumentation tests pass🤖 Generated with Claude Code