junrao commented on code in PR #23665:
URL: https://github.com/apache/kafka/pull/23665#discussion_r4252952021


##########
clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java:
##########
@@ -137,11 +137,28 @@ public ResponseHeader toResponseHeader() {
         return new ResponseHeader(data.correlationId(), 
apiKey().responseHeaderVersion(apiVersion()));
     }
 
+    /**
+     * Parses a request header from the given buffer. On return, the buffer is 
positioned at the start of the

Review Comment:
   the buffer is positioned => for a supported request API version, the buffer 
is positioned



##########
clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java:
##########
@@ -137,11 +137,28 @@ public ResponseHeader toResponseHeader() {
         return new ResponseHeader(data.correlationId(), 
apiKey().responseHeaderVersion(apiVersion()));
     }
 
+    /**
+     * Parses a request header from the given buffer. On return, the buffer is 
positioned at the start of the
+     * request body.
+     * <p>
+     * If the request API version is not supported by this node, the 
corresponding header version may also be unknown.
+     * In this case, the header is parsed using version 1, which contains only 
the fields common to all header versions:
+     * API key, API version, correlation ID, and client ID. Any remaining 
header bytes, including tagged fields,
+     * are left unread.
+     * This allows the broker to return an error response for requests with 
unsupported versions, particularly
+     * {@link ApiVersionsRequest} from newer clients. The request body must 
not be parsed in this case because the buffer

Review Comment:
   The broker actually doesn't send unsupported versions for unsupported 
ApiVersionsRequest. It generates a V0 ApiVersionsResponse instead. The client 
will parse the response as the expected version and on parsing exception, fall 
to parsing it as V0. 



##########
clients/src/main/resources/common/message/RequestHeader.json:
##########
@@ -40,6 +40,11 @@
     // ApiVersionsRequest, even if it is from a newer version.
     // Since the client is sending the ApiVersionsRequest in order to discover 
what
     // versions are supported, the client does not know the best version to 
use.
+    //
+    // When the request api version is unsupported, the request header is 
parsed with version 1,
+    // i.e. only the fields above are read (see RequestHeader#parse). Brokers 
without that change
+    // parse the header with the version derived from the request api version 
instead. So, any new
+    // field added to the header must be a tagged field until all supported 
releases have that change.

Review Comment:
   How about the following?
   
   Currently, we can't add new non-tagged fields to the request header for 
ApiVersionRequest. This is because the old releases have a bug on the broker 
that parses unknown request header with the first 4 fields and the tagged 
fields. If a new client sends an ApiVersionRequest with a new non-tagged field 
in the header to an old broker, the older broker will fail parsing the new 
field as the tagged fields and will throw an InvalidRequestException instead of 
sending a V0 response. Therefore, the new client can never discover the support 
API versions on the old broker. We have since fixed the broker to parse 
unsupported request header with only the first 4 fields. This will allow us to 
add non-tagged fields to the request header in the future, as long as the order 
of the first 4 fields are preserved. However, we need to wait until all 
supported releases have that fix. See  KAFKA-21160 for more details.



##########
clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java:
##########
@@ -153,8 +170,9 @@ public static RequestHeader parse(ByteBuffer buffer) {
                 throw new InvalidRequestException("Unsupported api with key " 
+ apiKeyId + " (" + apiKey.name + ") and version " + apiVersion);
 
             short headerVersion = apiKey.requestHeaderVersion(apiVersion);
+            short parseHeaderVersion = apiKey.isVersionSupported(apiVersion) ? 
headerVersion : (short) 1;
             buffer.position(bufferStartPositionForHeader);
-            final RequestHeaderData headerData = new RequestHeaderData(new 
ByteBufferAccessor(buffer), headerVersion);
+            final RequestHeaderData headerData = new RequestHeaderData(new 
ByteBufferAccessor(buffer), parseHeaderVersion);

Review Comment:
   We should use parseHeaderVersion when constructing RequestHeader below, 
right?



##########
clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java:
##########
@@ -137,11 +137,28 @@ public ResponseHeader toResponseHeader() {
         return new ResponseHeader(data.correlationId(), 
apiKey().responseHeaderVersion(apiVersion()));
     }
 
+    /**
+     * Parses a request header from the given buffer. On return, the buffer is 
positioned at the start of the
+     * request body.
+     * <p>
+     * If the request API version is not supported by this node, the 
corresponding header version may also be unknown.
+     * In this case, the header is parsed using version 1, which contains only 
the fields common to all header versions:
+     * API key, API version, correlation ID, and client ID. Any remaining 
header bytes, including tagged fields,
+     * are left unread.

Review Comment:
   We probably should add in that case, the returned header will have false in 
header.isApiVersionSupported() and the caller is expected to check that before 
parsing the rest of the bytes in the request.



##########
clients/src/test/java/org/apache/kafka/common/requests/RequestHeaderTest.java:
##########
@@ -104,6 +106,74 @@ public void testClientInstanceIdIsUnsetBelowTheV3Header() {
         assertEquals(header, RequestHeader.parse(buffer));
     }
 
+    @Test
+    public void testParseUnsupportedApiVersionOnlyReadsCommonHeaderFields() {
+        short apiVersion = (short) (ApiKeys.API_VERSIONS.latestVersion() + 1);
+        byte[] unknownField = unknownHeaderField();
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS, apiVersion, 
123,
+            "client", unknownField);
+        int commonFieldsSize = buffer.remaining() - unknownField.length;
+
+        RequestHeader header = RequestHeader.parse(buffer);
+        assertEquals(ApiKeys.API_VERSIONS, header.apiKey());
+        assertEquals(apiVersion, header.apiVersion());
+        assertEquals(123, header.correlationId());
+        assertEquals("client", header.clientId());
+        assertFalse(header.isApiVersionSupported());
+        assertEquals(ApiKeys.API_VERSIONS.requestHeaderVersion(apiVersion), 
header.headerVersion());
+        // Only the common fields are consumed and the unknown bytes are left 
in the buffer.
+        assertEquals(commonFieldsSize, header.size());
+        assertEquals(commonFieldsSize, buffer.position());
+        assertEquals(unknownField.length, buffer.remaining());
+    }
+
+    @Test
+    public void testParseSupportedApiVersionRejectsInvalidTaggedFields() {
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS,
+            ApiKeys.API_VERSIONS.latestVersion(), 123, "client", 
unknownHeaderField());
+        assertThrows(InvalidRequestException.class, () -> 
RequestHeader.parse(buffer));
+    }
+
+    @Test
+    public void testParseUnsupportedApiVersionWithNullClientId() {
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS, 
Short.MAX_VALUE, 123,
+            null, new byte[0]);
+        RequestHeader header = RequestHeader.parse(buffer);
+        assertEquals("", header.clientId());

Review Comment:
   Perhaps add a comment why the clientId is empty instead of null.



##########
clients/src/test/java/org/apache/kafka/common/requests/RequestContextTest.java:
##########
@@ -63,6 +65,34 @@ public void testSerdeUnsupportedApiVersionRequest() throws 
Exception {
         requestBuffer.putInt(29034);
         requestBuffer.flip();
 
+        verifyUnsupportedApiVersionsRequestSerde(context, requestBuffer, 
correlationId);
+    }
+
+    @Test
+    public void testSerdeUnsupportedApiVersionRequestWithUnknownHeaderFields() 
throws Exception {
+        int correlationId = 23423;
+        short apiVersion = (short) (ApiKeys.API_VERSIONS.latestVersion() + 1);
+
+        // A newer client may use a header version unknown to this broker, 
with fields after the client id which
+        // cannot be parsed. The header parser only reads the fields common to 
every header version, and the rest
+        // of the buffer is ignored since the request is treated as v0.
+        byte[] unknownBytes = new byte[24];
+        Arrays.fill(unknownBytes, (byte) 0xFF);
+        ByteBuffer requestBuffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS, apiVersion,
+            correlationId, "client", unknownBytes);

Review Comment:
   This is an existing issue, but we can just do `request.getErrorResponse(0, 
Errors.UNSUPPORTED_VERSION.exception())`.



##########
clients/src/test/java/org/apache/kafka/common/requests/RequestHeaderTest.java:
##########
@@ -104,6 +106,74 @@ public void testClientInstanceIdIsUnsetBelowTheV3Header() {
         assertEquals(header, RequestHeader.parse(buffer));
     }
 
+    @Test
+    public void testParseUnsupportedApiVersionOnlyReadsCommonHeaderFields() {
+        short apiVersion = (short) (ApiKeys.API_VERSIONS.latestVersion() + 1);
+        byte[] unknownField = unknownHeaderField();
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS, apiVersion, 
123,
+            "client", unknownField);
+        int commonFieldsSize = buffer.remaining() - unknownField.length;
+
+        RequestHeader header = RequestHeader.parse(buffer);
+        assertEquals(ApiKeys.API_VERSIONS, header.apiKey());
+        assertEquals(apiVersion, header.apiVersion());
+        assertEquals(123, header.correlationId());
+        assertEquals("client", header.clientId());
+        assertFalse(header.isApiVersionSupported());
+        assertEquals(ApiKeys.API_VERSIONS.requestHeaderVersion(apiVersion), 
header.headerVersion());
+        // Only the common fields are consumed and the unknown bytes are left 
in the buffer.
+        assertEquals(commonFieldsSize, header.size());
+        assertEquals(commonFieldsSize, buffer.position());
+        assertEquals(unknownField.length, buffer.remaining());
+    }
+
+    @Test
+    public void testParseSupportedApiVersionRejectsInvalidTaggedFields() {
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS,
+            ApiKeys.API_VERSIONS.latestVersion(), 123, "client", 
unknownHeaderField());
+        assertThrows(InvalidRequestException.class, () -> 
RequestHeader.parse(buffer));
+    }
+
+    @Test
+    public void testParseUnsupportedApiVersionWithNullClientId() {
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.API_VERSIONS, 
Short.MAX_VALUE, 123,
+            null, new byte[0]);
+        RequestHeader header = RequestHeader.parse(buffer);
+        assertEquals("", header.clientId());
+        assertEquals(123, header.correlationId());
+        assertFalse(header.isApiVersionSupported());
+        assertEquals(0, buffer.remaining());
+    }
+
+    @Test
+    public void testParseUnsupportedVersionOfOtherApi() {
+        ByteBuffer buffer = 
RequestTestUtils.serializeRequestHeaderPrefix(ApiKeys.FETCH, Short.MAX_VALUE, 
5, "client",
+            unknownHeaderField());
+        RequestHeader header = RequestHeader.parse(buffer);
+        assertEquals(ApiKeys.FETCH, header.apiKey());
+        assertEquals(Short.MAX_VALUE, header.apiVersion());
+        assertEquals(5, header.correlationId());
+        assertEquals("client", header.clientId());
+        assertFalse(header.isApiVersionSupported());

Review Comment:
   Should we assert buffer.position() and remaining(), as the ApiVersions test 
does?



##########
clients/src/main/java/org/apache/kafka/common/requests/RequestContext.java:
##########
@@ -114,6 +115,12 @@ public RequestAndSize parseRequest(ByteBuffer buffer) {
             // Unsupported ApiVersion requests are treated as v0 requests and 
are not parsed
             ApiVersionsRequest apiVersionsRequest = new ApiVersionsRequest(new 
ApiVersionsRequestData(), (short) 0, header.apiVersion());
             return new RequestAndSize(apiVersionsRequest, 0);
+        } else if (!header.isApiVersionSupported()) {

Review Comment:
   Let's document the exceptions in the javadoc of the method. Since it now 
also throws UnsupportedVersionException, it's better to handle it explicitly in 
the callers SaslServerAuthenticator.handleKafkaRequest() and 
EnvelopUtils.parseForwardedRequest() so that we can get the same error message.
   
   Also, the isApiVersionSupported() check in 
SaslServerAuthenticator.handleSaslToken (:454-458) is already unreachable and 
could be removed.



##########
clients/src/main/java/org/apache/kafka/common/requests/RequestHeader.java:
##########
@@ -153,8 +170,9 @@ public static RequestHeader parse(ByteBuffer buffer) {
                 throw new InvalidRequestException("Unsupported api with key " 
+ apiKeyId + " (" + apiKey.name + ") and version " + apiVersion);
 
             short headerVersion = apiKey.requestHeaderVersion(apiVersion);
+            short parseHeaderVersion = apiKey.isVersionSupported(apiVersion) ? 
headerVersion : (short) 1;

Review Comment:
   Would it be better to just do `short headerVersion = 
apiKey.isVersionSupported(apiVersion) ? apiKey.requestHeaderVersion(apiVersion) 
: (short) 1;`?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to