Skip to content

Propagate non-recoverable JSON conversion failures instead of demoting them to a generic 500 - #1436

Closed
apoupard wants to merge 1 commit into
spring-cloud:mainfrom
komune-io:fix/nonrecoverable-conversion-exception
Closed

Propagate non-recoverable JSON conversion failures instead of demoting them to a generic 500#1436
apoupard wants to merge 1 commit into
spring-cloud:mainfrom
komune-io:fix/nonrecoverable-conversion-exception

Conversation

@apoupard

@apoupard apoupard commented Aug 5, 2026

Copy link
Copy Markdown

Summary

GH-1429 (#ef533f377) already made convertNonMessageInputIfNecessary rethrow (rather than swallow) a JSON conversion failure when the request's Content-Type is JSON (failOnJsonError), fixing a misleading ClassCastException into a real Jackson exception in the logs. That's a great fix for the log message - but the rethrown exception is still wrapped as a plain IllegalStateException by convertInputPublisherIfNecessary, indistinguishable from any other conversion failure. Two consequences:

  • It still resolves to a generic 500 for any web layer that doesn't parse exception messages to guess what went wrong.
  • There's no way for a downstream consumer (a web framework's exception resolver, a custom @ControllerAdvice, an app's own error handler) to recognize "this converter deliberately, definitively rejected this input" without inspecting the message/cause chain.

This PR adds NonRecoverableConversionException, a narrow MessageConversionException subtype thrown only from the failOnJsonError path, and lets it propagate unwrapped through convertInputPublisherIfNecessary instead of being folded into the generic IllegalStateException bucket. Concretely, this lets an app register something like:

@ExceptionHandler(MessageConversionException.class)
ResponseEntity<?> handleBadInput(MessageConversionException ex) {
    return ResponseEntity.badRequest().body(...);
}

and get a real 400 for malformed input, instead of digging through IllegalStateException causes.

  • No new dependency: MessageConversionException already lives in spring-messaging, a non-optional dependency of spring-cloud-function-context.
  • No behavior change for any other exception type or code path - only the failOnJsonError branch's exception type changes.

Known follow-up (intentionally out of scope here)

A function declared as Function<Message<T>, R> (explicit Message-wrapped type) never reaches this path at all: convertInputMessageIfNecessary deliberately returns the original, unconverted message whenever the target type is itself Message<T> (to support legitimate no-conversion-needed cases like KafkaNull), which silently masks the same class of failure a different way. Functions declared over the plain payload type - the common case, and the only shape the new PojoFunctionIntegrationTests covers - are unaffected and fixed by this change. Happy to open a separate issue/PR for the Message<T> case if useful; it needs its own design discussion since the current fallback is deliberate, not an oversight.

Test plan

  • SimpleFunctionRegistryTests#testReactivePojoFunctionPropagatesNonRecoverableConversionExceptionForMalformedJson (new) - removal-proofed: fails without the fix, passes with it.
  • SimpleFunctionRegistryTests#testReactiveFunctionMessages (existing, valid-JSON case) - still green, confirming no behavior change on the success path.
  • Full spring-cloud-function-context + spring-cloud-function-web suite: 199 tests, 0 failures, 0 errors (including the new PojoFunctionIntegrationTests).

…ead of demoting them to a generic 500

spring-cloudGH-1429/ef533f377 already made convertNonMessageInputIfNecessary rethrow
(rather than swallow) a JSON conversion failure when the request's
Content-Type is JSON (failOnJsonError), fixing a misleading
ClassCastException into a real Jackson exception in the logs. But that
rethrown exception is still wrapped as a plain IllegalStateException by
convertInputPublisherIfNecessary, indistinguishable from any other
conversion failure - so it still resolves to a generic 500, and there is
no way for a downstream consumer (a web framework's exception resolver,
a custom @ControllerAdvice, etc.) to recognize "this was a deliberate,
non-recoverable rejection" without inspecting the message/cause chain.

Add NonRecoverableConversionException, a narrow MessageConversionException
subtype thrown only from the failOnJsonError path, and let it propagate
unwrapped through convertInputPublisherIfNecessary. This adds no new
dependency (MessageConversionException already lives in spring-messaging,
a non-optional dependency of this module) and changes no existing
behavior for any other exception type.

Known follow-up, intentionally out of scope here: a function declared as
Function<Message<T>, R> (explicit Message-wrapped type) never reaches
this path at all - convertInputMessageIfNecessary deliberately returns
the original, unconverted message whenever the target type is itself
Message<T> (to support legitimate no-conversion-needed cases like
KafkaNull), which silently masks the same class of failure. Functions
declared over the plain payload type (the common case, and the only
shape spring-cloud-function's own new PojoFunctionIntegrationTests
covers) are unaffected and fixed by this change.
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.

1 participant