Skip to content

proposal: express error-response API in terms of error codes (#4756) - #131

Open
k-wall wants to merge 9 commits into
kroxylicious:mainfrom
k-wall:proposal/error-response-error-codes
Open

proposal: express error-response API in terms of error codes (#4756)#131
k-wall wants to merge 9 commits into
kroxylicious:mainfrom
k-wall:proposal/error-response-error-codes

Conversation

@k-wall

@k-wall k-wall commented Aug 25, 2026

Copy link
Copy Markdown
Member

Design proposal to re-express the short-circuit error-response entry points on the public Filter/Router API in terms of Errors codes rather than the kafka-clients ApiException hierarchy.

  • RequestFilterResultBuilder.errorResponse and RouterContext.respondWithError gain (Errors) and (Errors, @Nullable String message) overloads.
  • The exception-based overloads are removed outright (clean break) — no deprecation window, no transitional signature — taking the last kafka-clients exception type off the public API surface in one step.
  • Removes the last places where the kafka-clients exception hierarchy leaks into the public API surface, complementing proposal 116 – Own the Kafka Protocol API Surface.

Note

Direction change following review. An earlier revision deprecated the exception overloads and widened them from ApiException to java.lang.Throwable to preserve source compatibility. Review discussion (thanks @robobario, @tombentley, @SamBarker) prompted that thought that the transitional machinery — a runtime type-guard, a deprecation window, and japicmp bookkeeping — buys little: filter authors must already edit source in 0.24.0 to migrate off Kafka's *Data classes (proposal 116), so the source break rides along with a change they are making anyway. The proposal now does a clean break, and the deprecate-and-widen approach is captured under Rejected alternatives.

Relates to kroxylicious/kroxylicious#4756 and implementation is in kroxylicious/kroxylicious#4757.

Proposal to re-express RequestFilterResultBuilder.errorResponse and
RouterContext.respondWithError in terms of Errors codes rather than
kafka-clients ApiException, removing the last exception types from the
public API surface. Relates to kroxylicious#4756 and complements
proposal 116.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
@k-wall
k-wall requested a review from a team as a code owner August 25, 2026 17:16
Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
The concern is that ApiException on the public API is inconsistent with
the error-code vocabulary used elsewhere in the API; how the exception
is used internally is not the point.

Assisted-by: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
@k-wall k-wall moved this from Must Do to In Progress in Release 0.24.0 Aug 25, 2026
@k-wall k-wall moved this from In Progress to Blocked in Release 0.24.0 Aug 25, 2026

@robobario robobario left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1, looks comprehensive to me thanks @k-wall.

The runtime lose a small amount of debug information in that we had an exception stack trace prior, but I think the message should compensate and Filters should be logging their own problems if they're truly caused by some problem, rather than everyday Kafka Protocol semantics that use error codes.

Comment on lines +56 to +60
- **Enables the owned-`Errors` payoff.** Once the API speaks in `Errors` codes rather than exception
instances, the `Errors` type itself can later be swapped for a Kroxylicious-owned enum (the follow
on to #4752/#4755). That swap is what ultimately allows the ~150 vendored `ApiException` subclasses
to be dropped from the owned surface entirely — the real payoff described in #4756. It is only
reachable once the *shape* of the API no longer demands an exception.

@robobario robobario Aug 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just wondering what the migration here looks like, since we still have these Throwable signatures established filters will continue to compile while sending down kafka-clients Exceptions. Will we maintain a translation from kafka-clients error class -> kroxylicious Errors enum until we delete the Throwable signature?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If the Filter author is using the kafka-client's exceptions, then they must have the kafka-client on the classpath.
So the runtime can use a reflective technique to get the recover the error code from the exception. This frees the kroxylicious-runtime of the explicit dependency and eases the transition path.

However, after sleeping on it, I am wondering if the Throwable stuff is really worth it. The filter author already going to have to make a code change for 0.24 anyway (Data classes etc) and if they are using the Filters APIs with ApiException, they'll need to add the kafka-client to their class path.

Maybe we just accept the fact they'll need to change:

                return context.requestFilterResultBuilder().errorResponse(header, request, Errors.GROUP_AUTHORIZATION_FAILED.exception()).completed();

to:

                return context.requestFilterResultBuilder().errorResponse(header, request, Errors.GROUP_AUTHORIZATION_FAILED).completed();
            }

Pulling the plaster off quickly in one go :)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I have kroxylicious/kroxylicious#4745 which hopes to make the existing migration fully automatic.

It could probably automate the highlighted example but I'm not sure it can catch all cases. (after a day fighting with open-rewrite I'm not best inclined to try and be clever with it yet.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If openrewrite can help with the vanilla case, great. If there are a few edge cases that Filter Authors need to handle themselves (new SomeApiException(msg)) so be it.

@SamBarker SamBarker left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

I think thats a good escape hatch from our current migration quagmire but has the bonus of narrowing our public API surface.

Comment on lines +144 to +146
- **Binary compatibility:** the exception-typed overloads are *removed* at the bytecode level (the
parameter type changes from `ApiException` to `Throwable`, which is a different method descriptor).
A pre-compiled plugin that was linked against `errorResponse(..., ApiException)` would fail at link

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I asked Gemini about this.

Yes, the runtime can reasonably detect or mitigate this NoSuchMethodError both proactively at startup and reactively at runtime.

Proactive Option: Startup Bytecode Scanning (Recommended)
During plugin loading and validation (e.g., via ServiceLoader or extension initialization), the runtime can scan the bytecode of registered filter classes using a lightweight library like ASM or ClassGraph (which proxy runtimes often already embed):

How it works: Inspect the class file's constant pool or method instruction references (invokevirtual/invokeinterface) for calls matching the exact old signature descriptor: errorResponse(..., Lorg/apache/kafka/common/errors/ApiException;).

Outcome: If detected, the runtime halts startup immediately with a clear, actionable log: "Plugin 'X' was compiled against an older version of Kroxylicious and calls a removed binary signature. Please recompile against version 0.24.0+."

Why it helps: It converts a delayed, silent runtime failure that only surfaces when an error condition occurs into a predictable, fail-fast startup check.

Reactive Option: LinkageError Catch Blocks
Because NoSuchMethodError is a java.lang.Error (a subclass of LinkageError), unhandled calls will bypass standard catch (Exception e) blocks in the filter execution engine.

How it works: The runtime's pipeline executor wraps filter method invocations with a catch block for NoSuchMethodError or LinkageError.

Outcome: Rather than causing an unhandled thread crash or opaque error response, the runtime catches the error, inspects the stack trace, and logs a targeted diagnostic pointing directly to the breaking API change.

I think its suggestion of proactive scanning is interesting and something we should think about, probably alongside the Api Versioning proposals. however I think we have too much complexity in flight right now to pull on that thread.

I do wonder if the reactive version is worth looking at. Catch all linkageErrors or NoSuchMethodErrors in the safe invoker and provide a blanket suggestion that the filter was compiled against a different API versions?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've removed the deprecated methods from the proposal. I decided they were bringing too little benefit to justify the complexity.

Comment thread proposals/131-error-response-api-in-terms-of-error-codes.md Outdated
Comment thread proposals/131-error-response-api-in-terms-of-error-codes.md Outdated
Comment thread proposals/131-error-response-api-in-terms-of-error-codes.md Outdated
@k-wall

k-wall commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

+1, looks comprehensive to me thanks @k-wall.

The runtime lose a small amount of debug information in that we had an exception stack trace prior

True, but the runtime has no valid use-case to look at ay chained exception. Anything chained belonged to the Filter. It was at most informational.

, but I think the message should compensate and Filters should be logging their own problems if they're truly caused by some problem, rather than everyday Kafka Protocol semantics that use error codes.

Yes.

k-wall and others added 2 commits August 26, 2026 12:50
Following review, drop the transitional Throwable overload: remove the
ApiException overloads outright rather than deprecating and widening them.
Filter authors already edit source in 0.24.0 to migrate off Kafka's *Data
classes (proposal 116), so the source break rides along with that change.

Also address review feedback: reframe the current-situation around API
inconsistency rather than difficulty, add a Non-goals section covering the
runtime's continued kafka-clients dependency and the Filter error contract,
and note the same-release timing minimises inconvenience.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
Proofread the current-situation and non-goals sections: fix a comma splice
and Errors/short wording, tidy the proposal-116 vendoring paragraph, and
correct the mapper description to use *RequestData/*Request terminology.

Add an explicit non-goal that vendoring an owned Errors class is delivered
separately; this proposal's API uses Kafka's Errors enum, to be swapped for
the Kroxylicious-owned one later.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
@k-wall
k-wall force-pushed the proposal/error-response-error-codes branch from 303a674 to 010f0d1 Compare August 26, 2026 11:50
@k-wall
k-wall requested review from SamBarker and robobario August 26, 2026 11:58
@k-wall

k-wall commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

@tombentley @robobario @SamBarker thanks for the feedback, I've updated the proposal. Please re-review.

k-wall and others added 3 commits August 26, 2026 13:13
Resolve internal inconsistencies found in review:
- drop the "No runtime churn" section (contradicted the mapper non-goal)
- clarify why removing ApiException from the signatures does not yet drop
  the ~150 subclasses (Kafka's Errors enum still references them)
- describe the future owned type consistently as the Kroxylicious-owned
  Errors enum (a vendored copy), not a new abstraction
- align the subclass count to ~150 and soften the proposal-116 framing
- note respondWithError gains the same overloads
- remove the redundant forward-compatibility bullet

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
… dep

Under proposal 116 ApiException would be vendored into kroxylicious-api, so
keeping these methods on ApiException forces the owned API to vendor the
~150-strong exception hierarchy rather than leaving a lingering kafka-clients
compile-time dependency. Reword the second motivation bullet accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
Signed-off-by: Keith Wall <kwall@apache.org>
@k-wall
k-wall requested a review from tombentley August 26, 2026 13:29
The new Errors overloads must denote an actual error, so Errors.NONE (the
absence-of-error sentinel) is rejected with IllegalArgumentException. The
errorCode parameter is @nonnull (inherited from package-info), so null is a
contract violation. Add a Validation subsection and a runtime-contract
compatibility bullet, and name the parameter errorCode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Keith Wall <kwall@apache.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Blocked

Development

Successfully merging this pull request may close these issues.

4 participants