Skip to content

KAFKA-21160 improve RequestHeader parsing on the broker - #23665

Open
m1a2st wants to merge 21 commits into
apache:trunkfrom
m1a2st:KAFKA-21160
Open

m1a2st wants to merge 21 commits into
apache:trunkfrom
m1a2st:KAFKA-21160

Conversation

@m1a2st

@m1a2st m1a2st commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

RequestHeader.parse() derives the header version from the request API
version and parses the entire header using that schema. For an API
version unknown to the broker, it falls back to the latest supported
header version. If a future version adds a non-tagged field, parsing
fails with InvalidRequestException and closes the connection, so a
newer client sending ApiVersionsRequest never receives the v0
UNSUPPORTED_VERSION response needed to downgrade.

For unsupported API versions, RequestHeader.parse() now reads only
fields with a fixed layout across header versions (API key, API
version, correlation ID, and client ID), leaving the rest unparsed.
Supported versions continue to be parsed as before.

Reviewers: Jun Rao junrao@gmail.com, Andrew Schofield
aschofield@confluent.io, Chia-Ping Tsai chia7712@gmail.com

@github-actions github-actions Bot added triage PRs from the community core Kafka Broker clients labels Oct 1, 2026

@junrao junrao 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.

@m1a2st : Thanks for the PR. Left a few comments.

Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
@github-actions github-actions Bot removed the triage PRs from the community label Oct 10, 2026
@m1a2st

m1a2st commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for @junrao comments, I have addressed all.

@junrao junrao 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.

@m1a2st : Thanks for the updated PR. A few more comments.

Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/resources/common/message/RequestHeader.json Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
@m1a2st

m1a2st commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @junrao for the review. I've addressed all the comments.

@junrao junrao 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.

@m1a2st : Thanks for the updated PR. A few more minor comments.

Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestContext.java Outdated
Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
Comment thread clients/src/main/resources/common/message/RequestHeader.json Outdated
Comment thread clients/src/main/resources/common/message/RequestHeader.json Outdated
@m1a2st

m1a2st commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @junrao for the review. I've addressed all the comments.

@junrao junrao 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.

@m1a2st : Thanks for the updated PR. Just one more minor comment.

@AndrewJSchofield : Does this change look good to you too?

Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestContext.java Outdated
@AndrewJSchofield
AndrewJSchofield self-requested a review October 16, 2026 16:24
@AndrewJSchofield

Copy link
Copy Markdown
Member

I'll take a look in the next few days.

@AndrewJSchofield AndrewJSchofield 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.

Thanks for the PR. This looks like a useful hardening of the parsing logic. I do think it will be a very, very long time before releases before 4.5.0 are not supported though.

@@ -137,11 +137,27 @@ public ResponseHeader toResponseHeader() {
return new ResponseHeader(data.correlationId(), apiKey().responseHeaderVersion(apiVersion()));

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.

Isn't it theoretically possible that apiKey().isVersionSupported(apiVersion()) is false in which case the response header version is not really known? I suppose the response header version should be 1 in this case, and we must never add non-tagged fields to the response header.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In practice, only ApiVersionsRequest can reach toResponseHeader() with an unsupported version. For other APIs, Processor#parseRequestHeader rejects unsupported versions with UnsupportedVersionException, and RequestContext#parseRequest also throws in this case.

For ApiVersions, responseHeaderVersion() is hard-coded to return 0 for all versions (KIP-511), so the response header version is always well-defined.

@junrao junrao Oct 20, 2026 •

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.

@AndrewJSchofield : Good point. Claude suggested the following way to tighten this up.

RequestContext is the only production caller of toResponseHeader() (:159, :173). Compute both the body version and the header version from the same supported version:

public Send buildResponseSend(AbstractResponse body) {
    short version = apiVersion();
    return body.toSend(responseHeader(version), version);
}
public ByteBuffer buildResponseEnvelopePayload(AbstractResponse body) {
    short version = apiVersion();
    return body.serializeWithHeader(responseHeader(version), version);
}
/**
 * The response header follows the version the response is written at, which for an unsupported
 * ApiVersionsRequest is v0 rather than the version of the request.
 */
private ResponseHeader responseHeader(short responseVersion) {
    return new ResponseHeader(header.correlationId(), header.apiKey().responseHeaderVersion(responseVersion));
}

As a related issue, currently, KafkaApis uses 4 different ways to obtain the api version. They all agree except for unsupport ApiVersion.

Accessor | Returns | Why
-- | -- | --
request.header.apiVersion | N (6) | data.requestApiVersion(), the version in the request header
request.context.apiVersion() | 0 | isUnsupportedApiVersionsRequest() is true, so it returns 0, the version the response is written at (RequestContext.java:180-185)
request.context.requestVersion() | N (6) | header.apiVersion(), returned as an int (RequestContext.java:217-219)
request.body[ApiVersionsRequest].version | 0 | parseRequest builds new ApiVersionsRequest(new ApiVersionsRequestData(), (short) 0, header.apiVersion()) (RequestContext.java:131), so the body is a v0 request and its body isn't parsed

It would be useful to document what each api means and when to use which.

We probably can change request.context.apiVersion() to sth like request.context.effectiveApiVersion() and document it that this is the version parsed by the server. KafkaApis should typically use this or request.body[ApiVersionsRequest].version to get api version.

Document that request.context.requestVersion() is the way for getting the original Api Version sent by the client. This should only be used in the following in KafkaApis where the original version is needed.

if (!apiVersionManager.isApiEnabled(request.header.apiKey, request.header.apiVersion))

@m1a2st m1a2st Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My understanding is that the original client version and the effective version serve different purposes. For an unsupported ApiVersionsRequest, the original version is N, while the effective version is 0. The current code still works because ApiVersions always uses response header v0, but deriving both formats from the effective version makes the response serialization more consistent.

Based on this, I’ve updated both response serialization paths to use the effective version. KafkaApis now uses the effective version, for handler logic, while retaining the original version for the initial API-enabled check.

Please let me know if I missed anything. Thanks!

Comment thread clients/src/main/resources/common/message/RequestHeader.json Outdated
throw new InvalidRequestException("Unsupported api with key " + apiKeyId + " (" + apiKey.name + ") and version " + apiVersion);

short headerVersion = apiKey.requestHeaderVersion(apiVersion);
short headerVersion = apiKey.isVersionSupported(apiVersion) ? apiKey.requestHeaderVersion(apiVersion) : (short) 1;

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.

Should we hardcode the parsing of the four common fields instead of relying on the generated code? What we need is “read only the first four fields”, and header v1 only happens to match that today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd prefer to keep the parsing in the generated code. Hand-written parsing would duplicate the decoding logic outside the schema.

I agree that v1 only happens to match the four common fields today, so I've made that explicit, RequestHeader.json now specifies that the first four fields must retain their order and encoding, and that v1 must not be removed because brokers use it to parse headers with unsupported API versions.

Comment thread clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java Outdated
@chia7712

Copy link
Copy Markdown
Member

I do think it will be a very, very long time before releases before 4.5.0 are not supported though.

I hope we are still here when that day comes :)

@junrao junrao 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.

@m1a2st : Thanks for the updated PR. Just a minor comment.

Comment thread clients/src/main/resources/common/message/RequestHeader.json Outdated

@junrao junrao 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.

@m1a2st : Thanks for updated PR. Left one reply to a previous comment.

@junrao junrao 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.

@m1a2st : Thanks for the updated PR. A few more comments.


/**
* Get the version of this AbstractRequest object.
* Returns the API version of this request object, which determines its body format.

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.

AbstractRequest is a generic class and not used just by request handlers. version() should be self explanatory, i.e., it's the version associated with this request object. We can just remove the comment here.


/**
* The response header follows the version used to serialize the response body. For an unsupported
* ApiVersionsRequest, this is v0 rather than the original request version.

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.

This is a generic method and is self explanatory. We can remove the comment.

* Request handlers should use this version or {@link AbstractRequest#version()} for version-dependent behavior.
*
* @see #requestVersion()
* @see AbstractRequest#version()

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.

How about the following?

Returns the API version parsed by the server. This is usually the API version in the original client request. The only exception is an unsupported {@link ApiVersionsRequest}, whose API version is set to 0 after parsing. The response should be sent and serialized using this version.

To retrieve the API version in the original client request, use #requestVersion() instead.

* {@link #effectiveApiVersion()} for an unsupported {@link ApiVersionsRequest}. Use this when the original
* version is needed, such as checking whether the requested API version is enabled.
*
* @see RequestHeader#apiVersion()

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.

We can remove this line.

* Server-side request handlers should use {@link RequestContext#effectiveApiVersion()} or
* {@link AbstractRequest#version()} for version-dependent behavior, and {@link RequestContext#requestVersion()}
* when the original client version is needed.
*/

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.

The API should be self explanatory. The server side details are explained in RequestContext already. We can remove this comment.

We can probably document RequestContext.header that this is the original header from the client request. For unsupported ApiVerionRequest, use effectiveApiVersion() to get the actual parsed API version.

val groupResponses = new ArrayBuffer[OffsetFetchResponseData.OffsetFetchResponseGroup](futures.size)
futures.foreach(future => groupResponses += future.get())
requestHelper.sendMaybeThrottle(request, new OffsetFetchResponse.Builder(groupResponses.asJava).build(request.context.apiVersion))
requestHelper.sendMaybeThrottle(request, new OffsetFetchResponse.Builder(groupResponses.asJava).build(request.context.effectiveApiVersion))

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.

Probably use offsetFetchRequest.version here?

}

/**
* Returns the original API version sent by the client in the request header. This can differ from

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.

It seems that all usage of requestVersion() in GroupCoordinatorService, GroupMetadataManager and OffsetMetadataManager should be using effectiveApiVersion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The coordinator components receive AuthorizableRequestContext, which only exposes requestVersion() and defines it as the original version from the request header.

I think the possible approaches are:

  • Add effectiveApiVersion() to AuthorizableRequestContext, which would extend the public authorizer API.
  • Introduce an internal interface exposing the effective version, which would require updating the coordinator signatures.
  • Use an instanceof check with a fallback to requestVersion(), although other implementations would still return the original version.

Would you prefer keeping these calls as-is for this PR and addressing the coordinator context separately?

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.

        CreateDelegationTokenResponse.prepareResponse(request.context.requestVersion, requestThrottleMs,
        CreateDelegationTokenResponse.prepareResponse(request.context.requestVersion, requestThrottleMs,
               request.context.requestVersion,

I think those parts related to preparing the response should use effectiveApiVersion instead.

return new ResponseHeader(header.correlationId(), header.apiKey().responseHeaderVersion(responseVersion));
}

private boolean isUnsupportedApiVersionsRequest() {

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.

Should we consolidate the fallback logic into ApiKeys? For example:

public enum ApiKeys {
    ...
    /**
     * ApiVersions is used for version discovery, so the server accepts any version of it.
     * An unsupported version is handled as this version, and the client falls back to parsing
     * the response with it (KIP-511). This version must never be removed.
     */
    public static final short API_VERSIONS_FALLBACK_VERSION = 0;

    /**
     * Returns true if a request with the given version must be handled as
     * {@link #API_VERSIONS_FALLBACK_VERSION} instead of being parsed.
     */
    public boolean isFallbackVersion(short version) {
        return this == API_VERSIONS && !isVersionSupported(version);
    }

    public boolean isVersionEnabled(short apiVersion, boolean enableUnstableLastVersion) {
        // ApiVersions accepts any version, see isFallbackVersion
        if (this == API_VERSIONS) return true;
        ...
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants