proposal: express error-response API in terms of error codes (#4756) - #131
proposal: express error-response API in terms of error codes (#4756)#131k-wall wants to merge 9 commits into
Conversation
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>
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>
robobario
left a comment
There was a problem hiding this comment.
+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.
| - **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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
LGTM
I think thats a good escape hatch from our current migration quagmire but has the bonus of narrowing our public API surface.
| - **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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I've removed the deprecated methods from the proposal. I decided they were bringing too little benefit to justify the complexity.
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.
Yes. |
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>
303a674 to
010f0d1
Compare
|
@tombentley @robobario @SamBarker thanks for the feedback, I've updated the proposal. Please re-review. |
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>
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>
Design proposal to re-express the short-circuit error-response entry points on the public Filter/Router API in terms of
Errorscodes rather than thekafka-clientsApiExceptionhierarchy.RequestFilterResultBuilder.errorResponseandRouterContext.respondWithErrorgain(Errors)and(Errors, @Nullable String message)overloads.kafka-clientsexception type off the public API surface in one step.kafka-clientsexception 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
ApiExceptiontojava.lang.Throwableto preserve source compatibility. Review discussion (thanks @robobario, @tombentley, @SamBarker) prompted that thought that the transitional machinery — a runtime type-guard, a deprecation window, andjapicmpbookkeeping — buys little: filter authors must already edit source in 0.24.0 to migrate off Kafka's*Dataclasses (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.