Keep http.status_code when the Lambda AppSec context is missing - #12623
Merged
claponcet merged 2 commits intoSep 24, 2026
Conversation
processResponseData moved its RequestContext guard above the parse and tightened it to also require APPSEC slot data. Because DDSpanContext.getRequestContext() returns `this` and is never null for a real span, the live half of that guard is the new APPSEC check — so an exception inside processRequestStart (caught and swallowed there, while CURRENT_TRIGGER_TYPE is already set to the HTTP trigger) now costs the invocation span its http.status_code and error flag as well. Split the guard instead: compute the AppSec-context flag up front, keep publishing the status code, and gate only the WAF response callbacks on it. This is the previous ordering with the stricter condition retained. Also renames processResponseDataDoesNothingWhenSpanHasNoRequestContext, which asserted nothing and whose name described behaviour the span never had, and adds coverage for the non-null-RequestContext-without-APPSEC case that the regression actually goes through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The comment distinguishing a null trigger type (processRequestStart never ran) from a non-HTTP one (ran, trigger unsupported) was dropped while the requestContext guard below it was reworked. Both branches are still there and still behave differently, so the explanation still applies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
Contributor
There was a problem hiding this comment.
Contributor
🟢 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. |
claponcet
merged commit Sep 24, 2026
ae8ced6
into
clara.poncet/lambda-appsec-telemetry-parity
606 of 609 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Does This Do
This is a review of #12587, delivered as a PR into its branch so the one code change I'm proposing is reviewable as a diff rather than described in prose. Merge it, cherry-pick it, or close it — the discussion items below need no code from me.
Two commits:
http.status_codewhen the Lambda AppSec context is missing — the one behaviour regression I found, plus regression coverage.Motivation
1.
processResponseData's guard move silently madehttp.status_codeconditional on AppSecIn
LambdaAppSecHandler.processResponseData, theRequestContextguard moved from after the parse/status block to before the wholetry, and was tightened to also require APPSEC slot data:DDSpanContext.getRequestContext()returnsthis(DDSpanContext.java:841-843) and is never null for a real span, so therequestContext == nullhalf was already effectively dead in production. The live half is the new APPSEC check.That case is reachable. In
processRequestStart,CURRENT_TRIGGER_TYPEis set to the real HTTP trigger type at line 99, andprocessAppSecRequestDataruns at line 113 inside atrywhosecatch (Exception)at line 118 returnsnull. So if AppSec request processing throws, the span is created with an HTTP trigger type recorded but no AppSec context. On the way out, the new guard returns before ever reaching:Net effect: an AppSec failure at request start now also costs the invocation span its status code and error flag — tracing data that previously survived. That's the same coupling this PR's own commit
03452084 "Preserve Lambda resource name on AppSec failure"exists to prevent, which is why it reads as unintentional. It also runs against the grain of68be58c0 "Infer status 200 for implicit-success Lambda responses"on master.The fix
You can't keep the early exit and keep the status code —
statusCodeonly exists afterparseResponse, inside thetry. So the guard is split into a flag plus a later gate: the tracing tag is published as before, and only the WAF callbacks are gated. This is the previous ordering with the stricter APPSEC condition retained....then, immediately before the callback block:
The response is parsed even when no AppSec context exists. That is exactly the pre-PR cost, not a new one, on a path that only happens when AppSec request processing already threw.
Evidence the regression is real
processResponseDataStillPublishesStatusWhenSpanHasNoAppSecContextwas run against this PR's code without the production fix:With the fix, it passes. The second assertion (
verify(tracer, never()).getCallbackProvider(APPSEC)) keeps the original intent honest — the WAF callbacks must still be skipped, and the tracer must not even be consulted.processResponseDataDoesNothingWhenSpanHasNoRequestContextis renamed to...PublishesStatusButNoWafEventsWhenSpanHasNoRequestContextand given real assertions. Its body was// no exception expected— it asserted nothing, and its name described behaviour the span never had on master either. That empty assertion is why the change slipped through.2. Restored comment
Dropped while the
requestContextguard below it was reworked. Both branches are still present and still behave differently, so the explanation still applies.Additional Notes
Everything below is discussion, not code.
The bug #12587 fixes is more serious than its description claims
The description calls the pre-PR gap "could omit the existing WAF telemetry." It's more than that.
GatewayBridge.onRequestEndedends withctx.close()(GatewayBridge.java:1110), andAppSecRequestContext.close()callscloseWafContext()— releasing the native ddwaf context. That code carries a telemetry log for exactly this situation:Pre-PR, a throwing handler never reached
notifyAppSecEnd, sorequestEndednever fired and the native WAF context was never released. Lambda reuses the execution environment across invocations, so that is native memory accumulating per throwing invocation for the life of the container, not just a missing metric.Same shape for
CURRENT_TRIGGER_TYPE:processRequestEndremoves it (line 132), so pre-PR it leaked across invocations on the reused handler thread. A later invocation whereprocessRequestStartbailed at lines 80-83 (AppSec deactivated) — before thesetat line 85 — would then read a stale trigger type inprocessResponseData.Both are stronger justifications than what's written and worth two lines in Motivation; they change how a reviewer weighs the risk.
component: aws-lambdais only present on the AppSec pathapplyHttpTagsis reached only when AppSec is active, the trigger is HTTP, andprocessRequestStartproduced aTagContext. For the telemetry goal that is sufficient —GatewayBridge:1127-1128derivesframeworkfromTags.COMPONENTon the span tags — but as a span tagcomponentnow appears inconsistently: present for AppSec HTTP invocations, absent otherwise. Nothing else sets it on this span (LambdaHandlerDecoratoronly holds the span name). Worth considering setting it unconditionally in the instrumentation and letting AppSec just read it.Related: the tests now pin (
LambdaAppSecHandlerTest:2706-2707) that inner instrumentation can overwritecomponent. So a Spring handler inside Lambda attributes API Security telemetry tospring-web-controller, notaws-lambda. Probably the right answer — but it meansframework:aws-lambdais not guaranteed, which the description implies it is. One sentence would cover it.New user-visible tag on the error path
testStreamingHandlerWithErrornow asserts_dd.appsec.unsupported_event_type = 1, which falls out of firingnotifyAppSecEndon the throw path. It is consistent with the non-throwing case, so it is correct — but it is a new tag appearing on error spans for non-HTTP-trigger Lambdas, and it is not mentioned in the description.Test hygiene nit
GatewayBridgeSpecification.groovy:236— if theas TagContextcast fails,processRequestEndnever runs and theCURRENT_TRIGGER_TYPEThreadLocal leaks into later tests on that thread. Acleanup:clearing it would make it robust.What's good
Worth saying, because the risky parts are handled well. Nulling
resulton the throw path is a clean way to reuse one call site for both outcomes —processResponseData'sinstanceof ByteArrayOutputStreamcheck rejects it immediately.nestedHandlerFinalizesAppSecRequestOncecovers the real hazard of an unconditionalnotifyAppSecEnd(the call-depth guard).responseCallbacksReceiveNoDataWhenHandlerThrowswith the new write-then-throw handler proves a partial response is not analysed.notifyAppSecEndFinalizesRequestWhenResponseProcessingThrowsexercises theCoreTracertry/finally through a realCoreTracerrather than a mock. And movingsetResourceNameintofinallyis genuinely reachable —processRequestEndhas no try/catch, andprocessResponseData's catch isException, notThrowable.Verification
Run locally on this branch (JDK 21):
dd-trace-coredatadog.trace.lambda.*LambdaAppSecHandlerTest156)aws-java-lambda-handler-1.2:testLambdaHandlerInstrumentationV0Test17, 0 failuresaws-java-lambda-handler-1.2:forkedTestLambdaHandlerInstrumentationV1ForkedTest17, 0 failuresappsec:test --tests '*GatewayBridgeSpecification*'dd-trace-core:spotlessJavaCheck🤖 Generated with Claude Code