You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by hg...@apache.org on 2026/07/02 22:08:46 UTC

(iggy) 39/43: fix(server-ng): frame raw PAT reply with a result section

This is an automated email from the ASF dual-hosted git repository.

hubcio pushed a commit to branch http-listener-rebased
in repository https://gitbox.apache.org/repos/asf/iggy.git

commit 4cadd9e9b88719bde5a8ac4c4054294862579439
Author: Hubert Gruszecki <h....@gmail.com>
AuthorDate: Thu Jul 2 22:19:32 2026 +0200

    fix(server-ng): frame raw PAT reply with a result section
    
    The spliced CreatePersonalAccessToken reply carried a bare
    RawPersonalAccessTokenResponse body, but the SDK strips a leading
    result section from every metadata reply (split_metadata_result), so a
    successful create over the binary transports decoded the token bytes
    as a garbage result code and failed. Prefix the success section like
    any committed metadata reply and strip it in the HTTP token decode.
    Pre-existing; surfaced by the authentication scenario once the
    duplicate-name path stopped masking replies.
---
 core/server-ng/src/http.rs      | 13 ++++++-------
 core/server-ng/src/responses.rs | 22 ++++++++++++++++------
 2 files changed, 22 insertions(+), 13 deletions(-)

diff --git a/core/server-ng/src/http.rs b/core/server-ng/src/http.rs
index 1f5579505..0fd2b846b 100644
--- a/core/server-ng/src/http.rs
+++ b/core/server-ng/src/http.rs
@@ -3000,14 +3000,13 @@ fn decode_consumer_group_details(payload: &[u8]) -> Result<ConsumerGroupDetails,
     Ok(ConsumerGroupDetails::from(response))
 }
 
-/// Extract the raw one-time token from a [`build_raw_pat_reply`] output. That
-/// reply's body is a bare `RawPersonalAccessTokenResponse` (no leading result
-/// section, unlike a committed metadata reply), so decode it straight past the
-/// header.
+/// Extract the raw one-time token from a [`build_raw_pat_reply`] output. The
+/// spliced reply is framed like any committed metadata reply (a success result
+/// section, then the `RawPersonalAccessTokenResponse`) so the SDK's
+/// `split_metadata_result` can decode it; strip the section the same way here.
 fn decode_raw_pat_token(reply: &Message<GenericHeader>) -> Result<String, WriteError> {
-    let size = reply.header().size as usize;
-    let body = reply.as_slice().get(HEADER_SIZE..size).unwrap_or_default();
-    let response = RawPersonalAccessTokenResponse::decode_from(body)
+    let payload = committed_payload(reply)?;
+    let response = RawPersonalAccessTokenResponse::decode_from(payload)
         .map_err(|_| WriteError::Rejected(IggyError::InvalidCommand))?;
     Ok(response.token.as_str().to_string())
 }
diff --git a/core/server-ng/src/responses.rs b/core/server-ng/src/responses.rs
index e41659bd5..26946adb1 100644
--- a/core/server-ng/src/responses.rs
+++ b/core/server-ng/src/responses.rs
@@ -26,14 +26,14 @@
 use crate::bootstrap::ServerNgShard;
 use crate::session_manager::SessionManager;
 use crate::wire::{transport_kind_to_wire, usize_to_u32};
-use bytes::Bytes;
+use bytes::{BufMut, Bytes, BytesMut};
 use consensus::{MetadataHandle, VsrConsensus};
 use iggy_binary_protocol::codes::{
     GET_CLUSTER_METADATA_CODE, GET_CONSUMER_GROUP_CODE, GET_CONSUMER_GROUPS_CODE, GET_STATS_CODE,
     GET_STREAM_CODE, GET_STREAMS_CODE, GET_TOPIC_CODE, GET_TOPICS_CODE, GET_USER_CODE,
     GET_USERS_CODE,
 };
-use iggy_binary_protocol::consensus::result_code;
+use iggy_binary_protocol::consensus::{RESULT_COUNT_LEN, result_code};
 use iggy_binary_protocol::primitives::consumer::WireConsumer;
 use iggy_binary_protocol::requests::consumer_groups::{
     GetConsumerGroupRequest, GetConsumerGroupsRequest,
@@ -917,13 +917,20 @@ pub(crate) fn build_raw_pat_reply(
         return Ok(committed);
     }
     let token = WireName::new(raw.as_str()).map_err(|_| IggyError::InvalidFormat)?;
-    let body = RawPersonalAccessTokenResponse { token }.to_bytes();
+    let response = RawPersonalAccessTokenResponse { token };
+    // The SDK strips a leading result section from every metadata reply
+    // (`split_metadata_result`), so the spliced body must carry the success
+    // section like any committed metadata reply; a bare token body would be
+    // read as a garbage result code.
+    let mut body = BytesMut::with_capacity(RESULT_COUNT_LEN + response.encoded_size());
+    body.put_u32_le(0);
+    response.encode(&mut body);
     let reply = build_reply_from_bytes(
         request_header,
         request_header.client,
         request_header.session,
         commit,
-        &body,
+        &body.freeze(),
     );
     Ok(reply.into_generic())
 }
@@ -1134,8 +1141,11 @@ mod tests {
                 .expect("splice succeeds");
         let header_len = std::mem::size_of::<ReplyHeader>();
         let body = &reply.as_slice()[header_len..reply.header().size as usize];
-        let response =
-            RawPersonalAccessTokenResponse::decode_from(body).expect("token body decodes");
+        // Framed like every committed metadata reply: the SDK reads the result
+        // section first, then decodes the token payload past it.
+        assert_eq!(result_code(body), Some(0));
+        let response = RawPersonalAccessTokenResponse::decode_from(&body[RESULT_COUNT_LEN..])
+            .expect("token body decodes");
         assert_eq!(response.token.as_str(), "raw-token");
     }