Skip to content

Stop flagging JAX-RS/Spring MVC spans as errors when the exception maps to a non-5xx response - #12248

Open
katzj wants to merge 4 commits into
DataDog:masterfrom
katzj:katzj/fix-error-flag-on-mapped-exceptions
Open

katzj wants to merge 4 commits into
DataDog:masterfrom
katzj:katzj/fix-error-flag-on-mapped-exceptions

Conversation

@katzj

@katzj katzj commented Aug 20, 2026

Copy link
Copy Markdown

What Does This Do

  • JAX-RS/Jakarta-RS: when a resource method throws a WebApplicationException (or a
    subclass, e.g. NotFoundException), the jax-rs.request/jakarta-rs.request span's
    error flag is now decided from the status embedded in the exception's Response,
    instead of unconditionally being marked as an error.
  • Spring MVC: same idea for @ResponseStatus, ResponseStatusException, and (Spring 6+)
    ErrorResponse (e.g. NoResourceFoundException) on the spring-web-controller span.
  • In both cases, the status is checked against the same "server error status" set already
    used for the root HTTP span (DD_TRACE_HTTP_SERVER_ERROR_STATUSES), so behavior stays
    consistent with whatever a user has already configured there.
  • Adds an opt-in DD_TRACE_RESPONSE_STATUS_EXCEPTIONS / trace.response-status.exceptions
    config for exceptions that don't follow any of the above conventions - a comma-separated
    list of fully.qualified.ExceptionClass#accessorMethod entries. When a thrown exception
    (or a subclass of one) matches, the named no-arg accessor is invoked reflectively and its
    numeric return value is used the same way. This is opt-in and precise on purpose: it only
    ever reflects on classes/methods a user explicitly named, rather than guessing at common
    accessor names across arbitrary exceptions (which risks silently clearing a genuine error).

Motivation

JAX-RS and Spring MVC commonly signal a non-2xx response by throwing an exception that the
framework's own exception-mapping machinery turns into a normal HTTP response (e.g. a 404).
The tracer was flagging the resource/controller span as an error unconditionally whenever
such an exception was thrown, even though the actual HTTP response was a routine non-5xx -
flooding error tracking with non-actionable "errors" for expected control flow. The root
span was never affected by this (it already reflects the real status code correctly), which
is why this showed up as a healthy root span next to an errored child span.

Solves #7141
Solves #7288

Additional Notes

  • No new default behavior for exceptions that don't carry a recognizable status - they're
    still flagged as errors as before.
  • Public docs for the new DD_TRACE_RESPONSE_STATUS_EXCEPTIONS config will need a follow-up
    update in the Datadog docs site.

Contributor Checklist

@katzj

katzj commented Aug 20, 2026

Copy link
Copy Markdown
Author

Probably should be labeled with tag: ai generated, type: bug fix, inst: jax-rs, inst: spring but don't have permissions to do so

@katzj
katzj marked this pull request as ready for review August 20, 2026 19:45
@katzj
katzj requested review from a team as code owners August 20, 2026 19:45
@katzj
katzj requested review from bric3 and jordan-wong and removed request for a team August 20, 2026 19:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40720eabb7

ℹ️ 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".

katzj added 2 commits August 21, 2026 09:59
…to a non-5xx response

JAX-RS and Spring MVC resource/controller methods commonly signal a
non-2xx response by throwing an exception (e.g. NotFoundException,
ResponseStatusException, or a custom exception annotated with
@ResponseStatus) that the framework's own exception-mapping machinery
turns into a normal HTTP response. The tracer previously flagged the
resource/controller span as an error unconditionally whenever such an
exception was thrown, even when the framework maps it to a routine
4xx (or other non-5xx) status - flooding error tracking with
non-actionable "errors" for expected control flow.

These exceptions already carry their intended status (WebApplicationException's
embedded Response, ResponseStatusException/ErrorResponse, or the
@ResponseStatus annotation), so the fix decides the error flag from
that status against the same "server error" set used for the root
HTTP span, instead of unconditionally erroring.

@ResponseStatus's value() and code() attributes are @aliasfor each
other, but plain reflection on the annotation proxy doesn't resolve
that aliasing - if a caller sets only code(), value() still reports
its own default (INTERNAL_SERVER_ERROR) rather than the value mirrored
from code(). Both Spring decorators now read code() first (reflectively
on the Spring 3.1 classpath, which predates code()'s introduction in
4.2) and fall back to value() only when code() is left at its default.
…TTP status

Some applications signal a response status via their own exception
hierarchy and a generic exception-handling advice, rather than any of
the JAX-RS/Spring conventions already handled (WebApplicationException,
ResponseStatusException, ErrorResponse, @ResponseStatus). Those
exceptions still get unconditionally flagged as errors even when they
map to a routine non-5xx response, since the tracer has no way to know
what status they carry.

DD_TRACE_RESPONSE_STATUS_EXCEPTIONS / trace.response-status.exceptions
lets users declare a list of fully.qualified.ExceptionClass#accessorMethod
entries; when a thrown exception (or a subclass of one) matches, the
named no-arg accessor is invoked reflectively and its numeric return
value is used the same way as the built-in status extraction. This
keeps the change fully opt-in and precise - it only ever reflects on
classes/methods a user explicitly named, rather than guessing at common
accessor names across arbitrary exceptions and risking a genuine error
being silently cleared.

A misconfigured accessor could return a value outside the valid HTTP
status range (e.g. -1 as an unknown-status sentinel). That value would
otherwise flow straight into Config#getHttpServerErrorStatuses, a
BitSet indexed by status code, which throws on a negative index,
aborting normal error handling for the span entirely. The accessor
return value is now validated as a plausible HTTP status (100-599)
before use, falling back to normal error handling otherwise.
@katzj
katzj force-pushed the katzj/fix-error-flag-on-mapped-exceptions branch from 40720ea to 93e9b3b Compare August 21, 2026 13:59
@katzj

katzj commented Aug 27, 2026

Copy link
Copy Markdown
Author

@bric3 @jordan-wong 👋 hi, it's been a week which is longer than the provided expectations so just wanted to check in to see how I can help move this along

@dougqh dougqh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few things I noticed while reading through the reflective status-extraction path. Nothing blocking IMO, but flagging for a look.

return status;
}
}
} catch (Throwable ignored) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Catching Throwable here also swallows Errors (e.g. OutOfMemoryError, StackOverflowError) coming out of invoke, not just the reflection failures (IllegalAccessException, InvocationTargetException) the comment describes. Might be worth narrowing to ReflectiveOperationException (or re-throwing Error) so a genuine JVM-level failure isn't silently downgraded to "misconfigured entry".

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Left this as-is — catch (Throwable ignored) around reflective calls is the established idiom throughout this package (BaseDecorator, HttpServerDecorator, HttpClientDecorator all do the same for their own reflection/callback paths), so narrowing just this call site would be inconsistent with everything around it for limited benefit.

if (httpStatus instanceof HttpStatus) {
return ((HttpStatus) httpStatus).value();
}
} catch (Throwable ignored) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same broad catch (Throwable ignored) concern as in ConfiguredResponseStatusExceptions.invoke — this also swallows Errors, not just reflection failures on RESPONSE_STATUS_EXCEPTION_GET_STATUS.invoke.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same reasoning as the ConfiguredResponseStatusExceptions thread — left as-is to stay consistent with the rest of the package.

if (code != HttpStatus.INTERNAL_SERVER_ERROR) {
return code.value();
}
} catch (Throwable ignored) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same broad catch (Throwable ignored) concern here on RESPONSE_STATUS_CODE.invoke — swallows Errors along with the intended reflection failures.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same reasoning as the ConfiguredResponseStatusExceptions thread — left as-is to stay consistent with the rest of the package.

…or error mapping

The bounds check added for ConfiguredResponseStatusExceptions didn't
cover each decorator's own built-in status extraction, so an
out-of-range status from there could still throw IndexOutOfBoundsException
out of BitSet.get() and get silently swallowed by BaseDecorator.onError,
leaving spans with exception tags but never flagged as errors. Extract
the shared validate/flag/fallback tail of doOnError into one
MappedExceptionStatus helper used by all 5 decorators, closing the gap
in one place instead of five.
Covers each JAX-RS/Jakarta-RS decorator own extractResponseStatus path
(not just the ConfiguredResponseStatusExceptions path, which already
had one) with a WebApplicationException carrying a negative status,
asserting it falls back to normal error handling instead of throwing.

This branch has not been deployed

No deployments
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.

2 participants