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

hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git


The following commit(s) were added to refs/heads/master by this push:
     new 6f1d5485d feat(cpp): add functions related to client lifecycle, and 
message headers (#3610)
6f1d5485d is described below

commit 6f1d5485d06c3c43afd2510e432b2f7c1a3ac41a
Author: Rimuksh Kansal <[email protected]>
AuthorDate: Fri Jul 17 18:05:14 2026 +0900

    feat(cpp): add functions related to client lifecycle, and message headers 
(#3610)
---
 .../features/step_definitions/messaging_steps.cpp  |   5 +-
 foreign/cpp/Cargo.toml                             |   1 +
 foreign/cpp/MODULE.bazel                           |   7 -
 foreign/cpp/MODULE.bazel.lock                      |   2 +-
 foreign/cpp/src/client.rs                          | 115 +++-
 foreign/cpp/src/lib.rs                             | 138 +++-
 foreign/cpp/src/messages.rs                        |   7 +-
 foreign/cpp/src/type_conversion.rs                 | 291 ++++++++-
 foreign/cpp/tests/client/low_level_e2e.cpp         | 691 ++++++++++++++++++++-
 foreign/cpp/tests/common/test_helpers.hpp          |  33 +
 foreign/cpp/tests/consumer_group/low_level_e2e.cpp |  34 +
 foreign/cpp/tests/message/low_level_e2e.cpp        | 443 ++++++++++++-
 foreign/cpp/tests/message/unit_tests.cpp           |  14 +-
 foreign/cpp/tests/partition/low_level_e2e.cpp      |   8 +
 foreign/cpp/tests/stream/low_level_e2e.cpp         |  50 +-
 foreign/cpp/tests/topic/low_level_e2e.cpp          |  61 +-
 16 files changed, 1788 insertions(+), 112 deletions(-)

diff --git a/bdd/cpp/features/step_definitions/messaging_steps.cpp 
b/bdd/cpp/features/step_definitions/messaging_steps.cpp
index e58592bca..df4afc7db 100644
--- a/bdd/cpp/features/step_definitions/messaging_steps.cpp
+++ b/bdd/cpp/features/step_definitions/messaging_steps.cpp
@@ -117,8 +117,9 @@ WHEN("^I send ([0-9]+) messages to stream ([0-9]+), topic 
([0-9]+), partition ([
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (int index = 0; index < message_count; ++index) {
-        iggy::ffi::IggyMessageToSend message =
-            
iggy::ffi::make_message(bdd::to_payload(bdd::expected_payload(static_cast<std::uint32_t>(index))));
+        iggy::ffi::IggyMessageToSend message = iggy::ffi::make_message(
+            
bdd::to_payload(bdd::expected_payload(static_cast<std::uint32_t>(index))),
+            rust::Vec<iggy::ffi::HeaderEntry>());
         // Assign an explicit, 1-based id so the last-sent/last-polled 
comparison is meaningful.
         message.id_lo = static_cast<std::uint64_t>(index + 1);
         messages.push_back(std::move(message));
diff --git a/foreign/cpp/Cargo.toml b/foreign/cpp/Cargo.toml
index c5a9c1674..ca0e8ad6e 100644
--- a/foreign/cpp/Cargo.toml
+++ b/foreign/cpp/Cargo.toml
@@ -30,6 +30,7 @@ crate-type = ["staticlib"]
 bytes = "1.12.0"
 cxx = "1.0.194"
 iggy = { path = "../../core/sdk" }
+iggy_binary_protocol = { path = "../../core/binary_protocol" }
 iggy_common = { path = "../../core/common" }
 # Explicitly enable the runtime + I/O drivers required by 
`Runtime::enable_all()` in lib.rs.
 # Listing the features here insulates this crate from upstream SDK feature 
changes.
diff --git a/foreign/cpp/MODULE.bazel b/foreign/cpp/MODULE.bazel
index 8e0bdddb6..4e28a2851 100644
--- a/foreign/cpp/MODULE.bazel
+++ b/foreign/cpp/MODULE.bazel
@@ -25,13 +25,6 @@ bazel_dep(name = "platforms", version = "1.1.0")
 bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
 bazel_dep(name = "rules_rust", version = "0.70.0")
 
-rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
-rust.toolchain(
-    edition = "2024",
-    versions = ["1.96.0"],
-)
-use_repo(rust, "rust_toolchains")
-
 rust_host_tools = use_extension("@rules_rust//rust:extensions.bzl", 
"rust_host_tools")
 rust_host_tools.host_tools(
     name = "rs_host_tools",
diff --git a/foreign/cpp/MODULE.bazel.lock b/foreign/cpp/MODULE.bazel.lock
index afa030326..f8d8ddc5c 100644
--- a/foreign/cpp/MODULE.bazel.lock
+++ b/foreign/cpp/MODULE.bazel.lock
@@ -487,7 +487,7 @@
     },
     "@@rules_rust+//crate_universe/private:internal_extensions.bzl%cu_nr": {
       "general": {
-        "bzlTransitiveDigest": "XJ4D0erEmT3eLY1MntKrXiRPz/VTypO8oNZjgaY/ff8=",
+        "bzlTransitiveDigest": "0UoEjDQrkPOE9U+EQx4lrz7qpp6evMn/oeanXvUNkJs=",
         "usagesDigest": "ZmL90WEq2B6/NJ8rtHAqdnDPn+/9xG/GWR5K4UU4tyo=",
         "recordedInputs": [
           "REPO_MAPPING:bazel_features+,bazel_features_globals 
bazel_features++version_extension+bazel_features_globals",
diff --git a/foreign/cpp/src/client.rs b/foreign/cpp/src/client.rs
index 0452518c7..4051d40fb 100644
--- a/foreign/cpp/src/client.rs
+++ b/foreign/cpp/src/client.rs
@@ -17,11 +17,12 @@
 
 use crate::{RUNTIME, ffi};
 use iggy::prelude::{
-    Client as IggyConnectionClient, CompressionAlgorithm as 
RustCompressionAlgorithm, Consumer,
-    ConsumerGroupClient, ConsumerOffsetClient, Identifier as RustIdentifier,
-    IggyClient as RustIggyClient, IggyClientBuilder as RustIggyClientBuilder,
-    IggyExpiry as RustIggyExpiry, IggyMessage, IggyTimestamp, MaxTopicSize as 
RustMaxTopicSize,
-    MessageClient, PartitionClient, Partitioning, PollingStrategy,
+    Client as IggyConnectionClient, ClusterClient,
+    CompressionAlgorithm as RustCompressionAlgorithm, Consumer, 
ConsumerGroupClient,
+    ConsumerOffsetClient, Identifier as RustIdentifier, IggyClient as 
RustIggyClient,
+    IggyClientBuilder as RustIggyClientBuilder, IggyExpiry as RustIggyExpiry, 
IggyMessage,
+    IggyTimestamp, MaxTopicSize as RustMaxTopicSize, MessageClient, 
PartitionClient, Partitioning,
+    Permissions as RustPermissions, PollingStrategy, SegmentClient,
     SnapshotCompression as RustSnapshotCompression, StreamClient, SystemClient 
as RustSystemClient,
     SystemSnapshotType as RustSystemSnapshotType, TopicClient, UserClient,
 };
@@ -125,6 +126,26 @@ impl Client {
         })
     }
 
+    pub fn disconnect(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .disconnect()
+                .await
+                .map_err(|error| format!("Could not disconnect: {error}"))?;
+            Ok(())
+        })
+    }
+
+    pub fn shutdown(&self) -> Result<(), String> {
+        RUNTIME.block_on(async {
+            self.inner
+                .shutdown()
+                .await
+                .map_err(|error| format!("Could not shutdown client: 
{error}"))?;
+            Ok(())
+        })
+    }
+
     pub fn get_streams(&self) -> Result<Vec<ffi::Stream>, String> {
         RUNTIME.block_on(async {
             let streams = self
@@ -633,6 +654,33 @@ impl Client {
         })
     }
 
+    pub fn delete_segments(
+        &self,
+        stream_id: ffi::Identifier,
+        topic_id: ffi::Identifier,
+        partition_id: u32,
+        segments_count: u32,
+    ) -> Result<(), String> {
+        let rust_stream_id = 
RustIdentifier::try_from(stream_id).map_err(|error| {
+            format!("Could not delete segments: invalid stream identifier: 
{error}")
+        })?;
+        let rust_topic_id = RustIdentifier::try_from(topic_id).map_err(|error| 
{
+            format!("Could not delete segments: invalid topic identifier: 
{error}")
+        })?;
+
+        RUNTIME.block_on(async {
+            self.inner
+                .delete_segments(&rust_stream_id, &rust_topic_id, 
partition_id, segments_count)
+                .await
+                .map_err(|error| {
+                    format!(
+                        "Could not delete {segments_count} segments for topic 
'{rust_topic_id}' on stream '{rust_stream_id}', partition '{partition_id}': 
{error}"
+                    )
+                })?;
+            Ok(())
+        })
+    }
+
     pub fn create_consumer_group(
         &self,
         stream_id: ffi::Identifier,
@@ -1024,6 +1072,63 @@ impl Client {
             Ok(bytes)
         })
     }
+
+    pub fn get_cluster_metadata(&self) -> Result<ffi::ClusterMetadata, String> 
{
+        RUNTIME.block_on(async {
+            let metadata = self
+                .inner
+                .get_cluster_metadata()
+                .await
+                .map_err(|error| format!("Could not get cluster metadata: 
{error}"))?;
+            Ok(ffi::ClusterMetadata::from(metadata))
+        })
+    }
+
+    pub fn update_permissions(
+        &self,
+        user_id: ffi::Identifier,
+        has_permissions: bool,
+        permissions: ffi::Permissions,
+    ) -> Result<(), String> {
+        let rust_user_id = RustIdentifier::try_from(user_id).map_err(|error| {
+            format!("Could not update permissions: invalid user identifier: 
{error}")
+        })?;
+        let rust_permissions = has_permissions
+            .then(|| RustPermissions::try_from(permissions))
+            .transpose()
+            .map_err(|error| format!("Could not update permissions: 
{error}"))?;
+
+        RUNTIME.block_on(async {
+            self.inner
+                .update_permissions(&rust_user_id, rust_permissions)
+                .await
+                .map_err(|error| {
+                    format!("Could not update permissions for user 
'{rust_user_id}': {error}")
+                })?;
+            Ok(())
+        })
+    }
+
+    pub fn change_password(
+        &self,
+        user_id: ffi::Identifier,
+        current_password: String,
+        new_password: String,
+    ) -> Result<(), String> {
+        let rust_user_id = RustIdentifier::try_from(user_id).map_err(|error| {
+            format!("Could not change password: invalid user identifier: 
{error}")
+        })?;
+
+        RUNTIME.block_on(async {
+            self.inner
+                .change_password(&rust_user_id, &current_password, 
&new_password)
+                .await
+                .map_err(|error| {
+                    format!("Could not change password for user 
'{rust_user_id}': {error}")
+                })?;
+            Ok(())
+        })
+    }
 }
 
 pub unsafe fn delete_connection(client: *mut Client) -> Result<(), String> {
diff --git a/foreign/cpp/src/lib.rs b/foreign/cpp/src/lib.rs
index febcd9db5..458e280b6 100644
--- a/foreign/cpp/src/lib.rs
+++ b/foreign/cpp/src/lib.rs
@@ -88,11 +88,40 @@ mod ffi {
         topics_count: u32,
     }
 
+    #[repr(u8)]
+    enum HeaderKind {
+        Raw = 1,
+        String = 2,
+        Bool = 3,
+        Int8 = 4,
+        Int16 = 5,
+        Int32 = 6,
+        Int64 = 7,
+        Int128 = 8,
+        Uint8 = 9,
+        Uint16 = 10,
+        Uint32 = 11,
+        Uint64 = 12,
+        Uint128 = 13,
+        Float32 = 14,
+        Float64 = 15,
+    }
+
+    struct HeaderField {
+        kind: u8,
+        value: Vec<u8>,
+    }
+
+    struct HeaderEntry {
+        key: HeaderField,
+        value: HeaderField,
+    }
+
     struct IggyMessageToSend {
         id_lo: u64,
         id_hi: u64,
         payload: Vec<u8>,
-        user_headers: Vec<u8>,
+        user_headers: Vec<HeaderEntry>,
     }
 
     struct IggyMessagePolled {
@@ -106,7 +135,7 @@ mod ffi {
         payload_length: u32,
         reserved: u64,
         payload: Vec<u8>,
-        user_headers: Vec<u8>,
+        user_headers: Vec<HeaderEntry>,
     }
 
     struct PolledMessages {
@@ -222,6 +251,71 @@ mod ffi {
         total_disk_space: u64,
     }
 
+    struct TransportEndpoints {
+        tcp: u16,
+        quic: u16,
+        http: u16,
+        websocket: u16,
+    }
+
+    struct ClusterNode {
+        name: String,
+        ip: String,
+        endpoints: TransportEndpoints,
+        role: String,
+        status: String,
+    }
+
+    struct ClusterMetadata {
+        name: String,
+        nodes: Vec<ClusterNode>,
+    }
+
+    struct GlobalPermissions {
+        manage_servers: bool,
+        read_servers: bool,
+        manage_users: bool,
+        read_users: bool,
+        manage_streams: bool,
+        read_streams: bool,
+        manage_topics: bool,
+        read_topics: bool,
+        poll_messages: bool,
+        send_messages: bool,
+    }
+
+    struct TopicPermissions {
+        manage_topic: bool,
+        read_topic: bool,
+        poll_messages: bool,
+        send_messages: bool,
+    }
+
+    struct TopicPermissionEntry {
+        topic_id: u32,
+        permissions: TopicPermissions,
+    }
+
+    struct StreamPermissions {
+        manage_stream: bool,
+        read_stream: bool,
+        manage_topics: bool,
+        read_topics: bool,
+        poll_messages: bool,
+        send_messages: bool,
+        topics: Vec<TopicPermissionEntry>,
+    }
+
+    struct StreamPermissionEntry {
+        stream_id: u32,
+        permissions: StreamPermissions,
+    }
+
+    struct Permissions {
+        global: GlobalPermissions,
+        streams: Vec<StreamPermissionEntry>,
+    }
+
     extern "Rust" {
         type Client;
         type Consumer;
@@ -357,7 +451,7 @@ mod ffi {
             auto_commit: bool,
         ) -> Result<PolledMessages>;
 
-        fn make_message(payload: Vec<u8>) -> IggyMessageToSend;
+        fn make_message(payload: Vec<u8>, user_headers: Vec<HeaderEntry>) -> 
IggyMessageToSend;
 
         #[allow(clippy::too_many_arguments)]
         fn send_messages(
@@ -388,22 +482,42 @@ mod ffi {
         ) -> Result<Vec<u8>>;
 
         // Future functions
-        // fn disconnect(self: &Client) -> Result<()>;
-        // fn shutdown(self: &Client) -> Result<()>;
+        fn disconnect(self: &Client) -> Result<()>;
+        fn shutdown(self: &Client) -> Result<()>;
         // fn subscribe_events(self: &Client) -> Result<()>;
-        // fn delete_segments(self: &Client, stream_id: Identifier, topic_id: 
Identifier, partition_id: u32, segments_count: u32) -> Result<()>;
+        fn delete_segments(
+            self: &Client,
+            stream_id: Identifier,
+            topic_id: Identifier,
+            partition_id: u32,
+            segments_count: u32,
+        ) -> Result<()>;
         // fn get_user(self: &Client, user_id: Identifier) -> Result<()>;
         // fn get_users(self: &Client) -> Result<()>;
         // fn create_user(self: &Client, username: String, password: String, 
status: u8) -> Result<()>;
         // fn delete_user(self: &Client, user_id: Identifier) -> Result<()>;
         // fn update_user(self: &Client, user_id: Identifier, username: 
String, status: u8) -> Result<()>;
-        // fn update_permissions(self: &Client, user_id: Identifier, 
permissions: Vec<u8>) -> Result<()>;
-        // fn change_password(self: &Client, user_id: Identifier, 
current_password: String, new_password: String) -> Result<()>;
-        // fn get_cluster_metadata(self: &Client) -> Result<()>;
-        // fn get_personal_access_tokens(self: &Client) -> Result<()>;
-        // fn create_personal_access_token(self: &Client, name: String, 
expiry: u64) -> Result<()>;
+        fn update_permissions(
+            self: &Client,
+            user_id: Identifier,
+            has_permissions: bool,
+            permissions: Permissions,
+        ) -> Result<()>;
+        fn change_password(
+            self: &Client,
+            user_id: Identifier,
+            current_password: String,
+            new_password: String,
+        ) -> Result<()>;
+        fn get_cluster_metadata(self: &Client) -> Result<ClusterMetadata>;
+        // fn get_personal_access_tokens(self: &Client) -> 
Result<Vec<PersonalAccessTokenInfo>>;
+        // fn create_personal_access_token(
+        //     self: &Client,
+        //     name: String,
+        //     expiry: u64,
+        // ) -> Result<RawPersonalAccessToken>;
         // fn delete_personal_access_token(self: &Client, name: String) -> 
Result<()>;
-        // fn login_with_personal_access_token(self: &Client, token: String) 
-> Result<()>;
+        // fn login_with_personal_access_token(self: &Client, token: String) 
-> Result<IdentityInfo>;
 
         unsafe fn delete_client(client: *mut Client) -> Result<()>;
 
diff --git a/foreign/cpp/src/messages.rs b/foreign/cpp/src/messages.rs
index f39cdc1d4..13a4150de 100644
--- a/foreign/cpp/src/messages.rs
+++ b/foreign/cpp/src/messages.rs
@@ -17,11 +17,14 @@
 
 use crate::ffi;
 
-pub fn make_message(payload: Vec<u8>) -> ffi::IggyMessageToSend {
+pub fn make_message(
+    payload: Vec<u8>,
+    user_headers: Vec<ffi::HeaderEntry>,
+) -> ffi::IggyMessageToSend {
     ffi::IggyMessageToSend {
         id_lo: 0,
         id_hi: 0,
         payload,
-        user_headers: Vec::new(),
+        user_headers,
     }
 }
diff --git a/foreign/cpp/src/type_conversion.rs 
b/foreign/cpp/src/type_conversion.rs
index dd38c1547..8a47f7c40 100644
--- a/foreign/cpp/src/type_conversion.rs
+++ b/foreign/cpp/src/type_conversion.rs
@@ -23,13 +23,19 @@ use iggy::prelude::{
     PolledMessages as RustPolledMessages, Stream as RustStream, StreamDetails 
as RustStreamDetails,
     Topic as RustTopic, TopicDetails as RustTopicDetails, Validatable,
 };
+use iggy_binary_protocol::WireUserHeaders;
 use iggy_common::{
     CacheMetrics as RustCacheMetrics, CacheMetricsKey as RustCacheMetricsKey,
     ClientInfo as RustClientInfo, ClientInfoDetails as RustClientInfoDetails,
+    ClusterMetadata as RustClusterMetadata, ClusterNode as RustClusterNode,
     ConsumerGroup as RustConsumerGroup, ConsumerGroupInfo as 
RustConsumerGroupInfo,
     ConsumerGroupMember as RustConsumerGroupMember, ConsumerOffsetInfo as 
RustConsumerOffsetInfo,
-    Stats as RustStats,
+    GlobalPermissions as RustGlobalPermissions, HeaderEntry as RustHeaderEntry,
+    HeaderField as RustHeaderField, HeaderKind as RustHeaderKind, Permissions 
as RustPermissions,
+    Stats as RustStats, StreamPermissions as RustStreamPermissions,
+    TopicPermissions as RustTopicPermissions, TransportEndpoints as 
RustTransportEndpoints,
 };
+use std::collections::BTreeMap;
 
 impl From<RustIdentifier> for ffi::Identifier {
     fn from(identifier: RustIdentifier) -> Self {
@@ -188,6 +194,119 @@ impl From<RustStats> for ffi::Stats {
     }
 }
 
+impl From<RustTransportEndpoints> for ffi::TransportEndpoints {
+    fn from(endpoints: RustTransportEndpoints) -> Self {
+        ffi::TransportEndpoints {
+            tcp: endpoints.tcp,
+            quic: endpoints.quic,
+            http: endpoints.http,
+            websocket: endpoints.websocket,
+        }
+    }
+}
+
+impl From<RustClusterNode> for ffi::ClusterNode {
+    fn from(node: RustClusterNode) -> Self {
+        ffi::ClusterNode {
+            name: node.name,
+            ip: node.ip,
+            endpoints: ffi::TransportEndpoints::from(node.endpoints),
+            role: node.role.to_string(),
+            status: node.status.to_string(),
+        }
+    }
+}
+
+impl From<RustClusterMetadata> for ffi::ClusterMetadata {
+    fn from(metadata: RustClusterMetadata) -> Self {
+        ffi::ClusterMetadata {
+            name: metadata.name,
+            nodes: metadata
+                .nodes
+                .into_iter()
+                .map(ffi::ClusterNode::from)
+                .collect(),
+        }
+    }
+}
+
+impl From<ffi::GlobalPermissions> for RustGlobalPermissions {
+    fn from(permissions: ffi::GlobalPermissions) -> Self {
+        RustGlobalPermissions {
+            manage_servers: permissions.manage_servers,
+            read_servers: permissions.read_servers,
+            manage_users: permissions.manage_users,
+            read_users: permissions.read_users,
+            manage_streams: permissions.manage_streams,
+            read_streams: permissions.read_streams,
+            manage_topics: permissions.manage_topics,
+            read_topics: permissions.read_topics,
+            poll_messages: permissions.poll_messages,
+            send_messages: permissions.send_messages,
+        }
+    }
+}
+
+impl From<ffi::TopicPermissions> for RustTopicPermissions {
+    fn from(permissions: ffi::TopicPermissions) -> Self {
+        RustTopicPermissions {
+            manage_topic: permissions.manage_topic,
+            read_topic: permissions.read_topic,
+            poll_messages: permissions.poll_messages,
+            send_messages: permissions.send_messages,
+        }
+    }
+}
+
+impl TryFrom<ffi::StreamPermissions> for RustStreamPermissions {
+    type Error = String;
+
+    fn try_from(permissions: ffi::StreamPermissions) -> Result<Self, 
Self::Error> {
+        let mut topics = BTreeMap::new();
+        for entry in permissions.topics {
+            let topic_id = entry.topic_id as usize;
+            if topics
+                .insert(topic_id, 
RustTopicPermissions::from(entry.permissions))
+                .is_some()
+            {
+                return Err(format!("duplicate topic permission ID: 
{topic_id}"));
+            }
+        }
+        let topics = (!topics.is_empty()).then_some(topics);
+
+        Ok(RustStreamPermissions {
+            manage_stream: permissions.manage_stream,
+            read_stream: permissions.read_stream,
+            manage_topics: permissions.manage_topics,
+            read_topics: permissions.read_topics,
+            poll_messages: permissions.poll_messages,
+            send_messages: permissions.send_messages,
+            topics,
+        })
+    }
+}
+
+impl TryFrom<ffi::Permissions> for RustPermissions {
+    type Error = String;
+
+    fn try_from(permissions: ffi::Permissions) -> Result<Self, Self::Error> {
+        let mut streams = BTreeMap::new();
+        for entry in permissions.streams {
+            let stream_id = entry.stream_id as usize;
+            let stream_permissions = 
RustStreamPermissions::try_from(entry.permissions)?;
+            if streams.insert(stream_id, stream_permissions).is_some() {
+                return Err(format!("duplicate stream permission ID: 
{stream_id}"));
+            }
+        }
+        let streams = (!streams.is_empty()).then_some(streams);
+
+        Ok(RustPermissions {
+            global: RustGlobalPermissions::from(permissions.global),
+            streams,
+        })
+    }
+}
+
 impl From<RustPartition> for ffi::Partition {
     fn from(partition: RustPartition) -> Self {
         ffi::Partition {
@@ -319,6 +438,32 @@ impl From<RustIggyMessage> for ffi::IggyMessagePolled {
         let id_bytes = message.header.id.to_le_bytes();
         let id_lo = u64::from_le_bytes(id_bytes[0..8].try_into().unwrap());
         let id_hi = u64::from_le_bytes(id_bytes[8..16].try_into().unwrap());
+        let user_headers = match message.user_headers {
+            Some(raw_headers) => {
+                // Keep polling forward-compatible with future header kinds. 
Unlike the Rust SDK's
+                // typed decoder, this structural decoder preserves unknown 
kinds and values whose
+                // lengths do not match their fixed-width kind.
+                match WireUserHeaders::from_bytes(raw_headers) {
+                    Ok(wire_headers) => wire_headers
+                        .iter()
+                        .map(|entry| ffi::HeaderEntry {
+                            key: ffi::HeaderField {
+                                kind: entry.key_kind.0,
+                                value: entry.key.to_vec(),
+                            },
+                            value: ffi::HeaderField {
+                                kind: entry.value_kind.0,
+                                value: entry.value.to_vec(),
+                            },
+                        })
+                        .collect(),
+                    // A malformed header must not make its message or poll 
batch unreadable.
+                    Err(_) => Vec::new(),
+                }
+            }
+            None => Vec::new(),
+        };
+
         ffi::IggyMessagePolled {
             checksum: message.header.checksum,
             id_lo,
@@ -330,29 +475,155 @@ impl From<RustIggyMessage> for ffi::IggyMessagePolled {
             payload_length: message.header.payload_length,
             reserved: message.header.reserved,
             payload: message.payload.to_vec(),
-            user_headers: message
-                .user_headers
-                .map(|headers| headers.to_vec())
-                .unwrap_or_default(),
+            user_headers,
         }
     }
 }
 
+impl TryFrom<ffi::HeaderEntry> for RustHeaderEntry {
+    type Error = String;
+
+    fn try_from(entry: ffi::HeaderEntry) -> Result<Self, Self::Error> {
+        Ok(RustHeaderEntry {
+            key: decode_field(entry.key.kind, entry.key.value)?,
+            value: decode_field(entry.value.kind, entry.value.value)?,
+        })
+    }
+}
+
+fn decode_field<T>(kind: u8, value: Vec<u8>) -> Result<RustHeaderField<T>, 
String> {
+    let kind = RustHeaderKind::from_code(kind)
+        .map_err(|error| format!("Could not convert header field: {error}"))?;
+
+    match kind {
+        RustHeaderKind::Raw => RustHeaderField::try_from(value)
+            .map_err(|error| format!("Could not convert header field: 
{error}")),
+        RustHeaderKind::String => {
+            let value = String::from_utf8(value)
+                .map_err(|_| "Could not convert header field: invalid UTF-8 
string".to_string())?;
+            RustHeaderField::try_from(value)
+                .map_err(|error| format!("Could not convert header field: 
{error}"))
+        }
+        RustHeaderKind::Bool => match value.as_slice() {
+            [0] => Ok(RustHeaderField::from(false)),
+            [1] => Ok(RustHeaderField::from(true)),
+            _ => {
+                Err("Could not convert header field: bool values must encode 
as 0 or 1".to_string())
+            }
+        },
+        RustHeaderKind::Int8 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(i8::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: int8 values require exactly 1 
bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Int16 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(i16::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: int16 values require exactly 
2 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Int32 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(i32::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: int32 values require exactly 
4 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Int64 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(i64::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: int64 values require exactly 
8 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Int128 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(i128::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: int128 values require exactly 
16 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Uint8 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(u8::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: uint8 values require exactly 
1 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Uint16 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(u16::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: uint16 values require exactly 
2 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Uint32 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(u32::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: uint32 values require exactly 
4 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Uint64 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(u64::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: uint64 values require exactly 
8 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Uint128 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(u128::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: uint128 values require 
exactly 16 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Float32 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(f32::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: float32 values require 
exactly 4 bytes, got {}",
+                value.len()
+            )),
+        },
+        RustHeaderKind::Float64 => match value.try_into() {
+            Ok(bytes) => Ok(RustHeaderField::from(f64::from_le_bytes(bytes))),
+            Err(value) => Err(format!(
+                "Could not convert header field: float64 values require 
exactly 8 bytes, got {}",
+                value.len()
+            )),
+        },
+    }
+}
+
 impl TryFrom<ffi::IggyMessageToSend> for RustIggyMessage {
     type Error = String;
 
     fn try_from(message: ffi::IggyMessageToSend) -> Result<Self, Self::Error> {
-        if !message.user_headers.is_empty() {
-            return Err(
-                "Could not convert message: user_headers are not yet supported 
in the C++ SDK"
-                    .to_string(),
-            );
+        // TODO: Document in the C++ SDK that user headers are unordered and 
keys must be unique.
+        // The BTreeMap sorts entries by kind and value, discards Vec 
insertion order on send and
+        // poll, and rejects duplicate keys.
+        let mut user_headers = BTreeMap::new();
+        for entry in message.user_headers {
+            let header_entry = RustHeaderEntry::try_from(entry)
+                .map_err(|error| format!("Could not convert message user 
headers: {error}"))?;
+            if user_headers
+                .insert(header_entry.key, header_entry.value)
+                .is_some()
+            {
+                return Err(
+                    "Could not convert message user headers: duplicate header 
key".to_string(),
+                );
+            }
         }
         let id = ((message.id_hi as u128) << 64) | (message.id_lo as u128);
         let payload = Bytes::from(message.payload);
         RustIggyMessage::builder()
             .id(id)
             .payload(payload)
+            
.maybe_user_headers((!user_headers.is_empty()).then_some(user_headers))
             .build()
             .map_err(|error| format!("Could not convert message: {error}"))
     }
diff --git a/foreign/cpp/tests/client/low_level_e2e.cpp 
b/foreign/cpp/tests/client/low_level_e2e.cpp
index 177fdc855..590c8f597 100644
--- a/foreign/cpp/tests/client/low_level_e2e.cpp
+++ b/foreign/cpp/tests/client/low_level_e2e.cpp
@@ -19,6 +19,8 @@
 
 // TODO(slbotbm): Add tests for store_consumer_offset, get_consumer_offset, 
and delete_consumer_offset functions
 // attached to client after implementing consumer group functions
+// TODO(slbotbm): Add tests for update_permissions after creating create_user, 
get_user, etc. functions
+#include <cstddef>
 #include <cstdint>
 #include <string>
 #include <unordered_set>
@@ -141,6 +143,87 @@ TEST_F(LowLevelE2E_Client, 
LogoutErrorsWhenCalledMoreThanOnce) {
     ASSERT_THROW(client->logout_user(), std::exception);
 }
 
+TEST_F(LowLevelE2E_Client, ChangePasswordBeforeLoginThrows) {
+    RecordProperty("description",
+                   "Rejects change_password before connect, after connect but 
before login, and after disconnect.");
+    iggy::ffi::Client *client      = GetLoggedOutClient();
+    const auto user_id             = make_string_identifier("iggy");
+    const std::string old_password = "iggy";
+    const std::string new_password = "iggy-updated-secret";
+
+    ASSERT_THROW(client->change_password(user_id, old_password, new_password), 
std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->change_password(user_id, old_password, new_password), 
std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->change_password(user_id, old_password, new_password), 
std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, ChangePasswordWithInvalidCurrentPasswordThrows) {
+    RecordProperty("description", "Rejects change_password when the provided 
current password is incorrect.");
+    iggy::ffi::Client *client        = GetLoggedInClient();
+    const auto user_id               = make_string_identifier("iggy");
+    const std::string wrong_password = "not-the-current-password";
+    const std::string new_password   = "iggy-updated-secret";
+
+    ASSERT_THROW(client->change_password(user_id, wrong_password, 
new_password), std::exception);
+    ASSERT_NO_THROW(client->logout_user());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+}
+
+TEST_F(LowLevelE2E_Client, ChangePasswordWithInvalidNewPasswordThrows) {
+    RecordProperty("description",
+                   "Rejects change_password when the replacement password 
violates client-side length bounds.");
+    iggy::ffi::Client *client      = GetLoggedInClient();
+    const auto user_id             = make_string_identifier("iggy");
+    const std::string old_password = "iggy";
+    const std::string too_short    = "";
+    const std::string too_long(256, 'a');
+
+    ASSERT_THROW(client->change_password(user_id, old_password, too_short), 
std::exception);
+    ASSERT_THROW(client->change_password(user_id, old_password, too_long), 
std::exception);
+    ASSERT_NO_THROW(client->logout_user());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+}
+
+TEST_F(LowLevelE2E_Client, ChangePasswordForWrongUserThrows) {
+    RecordProperty("description", "Rejects change_password when targeting a 
user that does not exist.");
+    iggy::ffi::Client *client            = GetLoggedInClient();
+    const auto wrong_user_id             = 
make_string_identifier(GetRandomName());
+    const std::string current_password   = "iggy";
+    const std::string replacement_secret = "iggy-updated-secret";
+
+    ASSERT_THROW(client->change_password(wrong_user_id, current_password, 
replacement_secret), std::exception);
+    ASSERT_NO_THROW(client->logout_user());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+}
+
+TEST_F(LowLevelE2E_Client, ChangePasswordUpdatesCredentialsAndCanBeRestored) {
+    RecordProperty("description",
+                   "Changes the password for the current user, updates login 
behavior, and restores the original "
+                   "password before the test exits.");
+    iggy::ffi::Client *client        = GetLoggedInClient();
+    iggy::ffi::Client *second_client = GetLoggedOutClient();
+    iggy::ffi::Client *third_client  = GetLoggedOutClient();
+    const auto user_id               = make_string_identifier("iggy");
+    const std::string old_password   = "iggy";
+    const std::string new_password   = "iggy-updated-secret";
+    bool password_changed            = false;
+
+    ASSERT_NO_THROW(client->change_password(user_id, old_password, 
new_password));
+    password_changed = true;
+
+    EXPECT_THROW(second_client->login_user("iggy", old_password), 
std::exception);
+    EXPECT_NO_THROW(second_client->login_user("iggy", new_password));
+
+    if (password_changed) {
+        EXPECT_NO_THROW(client->change_password(user_id, new_password, 
old_password));
+        password_changed = false;
+    }
+
+    EXPECT_NO_THROW(third_client->login_user("iggy", old_password));
+}
+
 TEST_F(LowLevelE2E_Client, DeleteWhileUnauthenticatedAfterFailedLogin) {
     RecordProperty("description", "Allows client cleanup after a failed login 
leaves the connection unauthenticated.");
     iggy::ffi::Client *client = nullptr;
@@ -153,6 +236,247 @@ TEST_F(LowLevelE2E_Client, 
DeleteWhileUnauthenticatedAfterFailedLogin) {
     client = nullptr;
 }
 
+TEST_F(LowLevelE2E_Client, ConnectLoginThenDisconnect) {
+    RecordProperty("description",
+                   "Connects, logs in, disconnects successfully, and rejects 
authenticated operations afterward.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_me(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DisconnectWithoutConnect) {
+    RecordProperty("description", "Allows disconnect to be called on a client 
that was never explicitly connected.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->disconnect());
+}
+
+TEST_F(LowLevelE2E_Client, DisconnectWithoutLogin) {
+    RecordProperty("description", "Allows disconnect after connect even when 
no user has authenticated.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_stats(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DisconnectThenReconnectWithoutRelogin) {
+    RecordProperty("description",
+                   "Requires logging in again after a disconnect and reconnect 
before authenticated operations work.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_me(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DisconnectAfterFailedLogin) {
+    RecordProperty("description", "Allows disconnect after a failed login 
attempt leaves the client unauthenticated.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->login_user("biggy", "biggy"), std::exception);
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_me(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, ConnectLoginThenShutdown) {
+    RecordProperty("description",
+                   "Connects, logs in, shuts down successfully, and rejects 
further operations afterward.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->ping());
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_THROW(client->get_stats(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, ShutdownWithoutConnect) {
+    RecordProperty("description", "Allows shutdown to be called on a client 
that was never explicitly connected.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->shutdown());
+}
+
+TEST_F(LowLevelE2E_Client, ShutdownWithoutLogin) {
+    RecordProperty("description", "Allows shutdown after connect even when no 
user has authenticated.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_THROW(client->get_stats(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, ShutdownAfterFailedLogin) {
+    RecordProperty("description", "Allows shutdown after a failed login 
attempt leaves the client unauthenticated.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->login_user("biggy", "biggy"), std::exception);
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_THROW(client->get_me(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, RepeatedShutdownCallsHaveStableBehavior) {
+    RecordProperty("description", "Keeps repeated shutdown calls stable across 
duplicate invocations.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_THROW(client->get_me(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, ShutdownThenConnectThrows) {
+    RecordProperty("description", "Rejects reconnecting a client after 
shutdown transitions it to a terminal state.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_THROW(client->connect(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, ShutdownThenLoginThrows) {
+    RecordProperty("description",
+                   "Rejects logging in again after shutdown, even when login 
would normally auto-connect.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->shutdown());
+    ASSERT_THROW(client->login_user("iggy", "iggy"), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, GetClientsReflectsSessionRemovalAfterShutdown) {
+    RecordProperty("description",
+                   "Removes a shut down authenticated session from subsequent 
get_clients and get_client results.");
+    iggy::ffi::Client *first_client  = GetLoggedInClient();
+    iggy::ffi::Client *second_client = GetLoggedInClient();
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after_shutdown;
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+
+    ASSERT_NO_THROW(first_client->shutdown());
+    ASSERT_NO_THROW({ clients_after_shutdown = second_client->get_clients(); 
});
+
+    bool found_first = false;
+    for (const auto &client : clients_after_shutdown) {
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            break;
+        }
+    }
+
+    EXPECT_FALSE(found_first);
+    ASSERT_THROW(second_client->get_client(first_me.client_id), 
std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, GetClientsReflectsSessionRemovalAfterDisconnect) {
+    RecordProperty("description",
+                   "Removes a disconnected authenticated session from 
subsequent get_clients and get_client results.");
+    iggy::ffi::Client *first_client  = GetLoggedInClient();
+    iggy::ffi::Client *second_client = GetLoggedInClient();
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after_disconnect;
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+
+    ASSERT_NO_THROW(first_client->disconnect());
+    ASSERT_NO_THROW({ clients_after_disconnect = second_client->get_clients(); 
});
+
+    bool found_first = false;
+    for (const auto &client : clients_after_disconnect) {
+        if (client.client_id == first_me.client_id) {
+            found_first = true;
+            break;
+        }
+    }
+
+    EXPECT_FALSE(found_first);
+    ASSERT_THROW(second_client->get_client(first_me.client_id), 
std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, 
GetClientsReflectsLoggedOutSessionAsUnauthenticated) {
+    RecordProperty("description",
+                   "Keeps a logged out session visible in get_clients and 
get_client, but marks it unauthenticated.");
+    iggy::ffi::Client *first_client  = GetLoggedInClient();
+    iggy::ffi::Client *second_client = GetLoggedInClient();
+
+    iggy::ffi::ClientInfoDetails first_me{};
+    iggy::ffi::ClientInfoDetails logged_out_client{};
+    rust::Vec<iggy::ffi::ClientInfo> clients_after_logout;
+    ASSERT_NO_THROW({ first_me = first_client->get_me(); });
+
+    ASSERT_NO_THROW(first_client->logout_user());
+    ASSERT_NO_THROW({
+        clients_after_logout = second_client->get_clients();
+        logged_out_client    = second_client->get_client(first_me.client_id);
+    });
+
+    bool found_first = false;
+    for (const auto &client : clients_after_logout) {
+        if (client.client_id != first_me.client_id) {
+            continue;
+        }
+
+        found_first = true;
+        EXPECT_FALSE(client.has_user_id);
+        EXPECT_EQ(static_cast<std::string>(client.address), 
static_cast<std::string>(first_me.address));
+        EXPECT_EQ(static_cast<std::string>(client.transport), 
static_cast<std::string>(first_me.transport));
+        break;
+    }
+
+    EXPECT_TRUE(found_first);
+    EXPECT_EQ(logged_out_client.client_id, first_me.client_id);
+    EXPECT_FALSE(logged_out_client.has_user_id);
+    EXPECT_EQ(static_cast<std::string>(logged_out_client.address), 
static_cast<std::string>(first_me.address));
+    EXPECT_EQ(static_cast<std::string>(logged_out_client.transport), 
static_cast<std::string>(first_me.transport));
+}
+
 TEST_F(LowLevelE2E_Client, LoginWithoutConnect) {
     RecordProperty("description", "Supports login without an explicit prior 
connect call.");
     iggy::ffi::Client *client = nullptr;
@@ -174,6 +498,18 @@ TEST_F(LowLevelE2E_Client, ConnectWithoutLoginThenDelete) {
     client = nullptr;
 }
 
+TEST_F(LowLevelE2E_Client, DeleteWithoutDisconnect) {
+    RecordProperty("description", "Allows deleting a connected and 
authenticated client without disconnecting first.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(iggy::ffi::delete_client(client));
+    client = nullptr;
+}
+
 TEST_F(LowLevelE2E_Client, RepeatedClientMethodCallsHaveStableBehavior) {
     RecordProperty("description",
                    "Keeps repeated connect, login, and delete calls stable 
across duplicate invocations.");
@@ -191,6 +527,20 @@ TEST_F(LowLevelE2E_Client, 
RepeatedClientMethodCallsHaveStableBehavior) {
     ASSERT_NO_THROW(iggy::ffi::delete_client(client));
 }
 
+TEST_F(LowLevelE2E_Client, RepeatedDisconnectCallsHaveStableBehavior) {
+    RecordProperty("description", "Keeps repeated disconnect calls stable 
across duplicate invocations.");
+    iggy::ffi::Client *client = nullptr;
+    ASSERT_NO_THROW({ client = iggy::ffi::new_connection(""); });
+    ASSERT_NE(client, nullptr);
+    TrackClient(client);
+
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_me(), std::exception);
+}
+
 TEST_F(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
     RecordProperty("description", "Treats deleting a null client pointer as a 
no-op.");
     iggy::ffi::Client *client = nullptr;
@@ -198,12 +548,16 @@ TEST_F(LowLevelE2E_Client, DeleteNullConnectionIsNoop) {
 }
 
 TEST_F(LowLevelE2E_Client, GetStatsBeforeLoginThrows) {
-    RecordProperty("description", "Rejects get_stats before connect, and after 
connect but before login.");
+    RecordProperty("description",
+                   "Rejects get_stats before connect, after connect but before 
login, and after disconnect.");
     iggy::ffi::Client *client = GetLoggedOutClient();
 
     ASSERT_THROW(client->get_stats(), std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->get_stats(), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_stats(), std::exception);
 }
 
 TEST_F(LowLevelE2E_Client, FlushUnsavedBufferSucceedsForExistingPartition) {
@@ -220,7 +574,7 @@ TEST_F(LowLevelE2E_Client, 
FlushUnsavedBufferSucceedsForExistingPartition) {
                                          0, "server_default"));
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    messages.push_back(iggy::ffi::make_message(to_payload("flush-me")));
+    messages.push_back(iggy::ffi::make_message(to_payload("flush-me"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
 
     ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0),
                                           "partition_id", 
partition_id_bytes(0), std::move(messages)));
@@ -247,7 +601,8 @@ TEST_F(LowLevelE2E_Client, 
FlushUnsavedBufferSucceedsForExistingEmptyPartition)
 
 TEST_F(LowLevelE2E_Client, FlushUnsavedBufferBeforeLoginThrows) {
     RecordProperty("description",
-                   "Throws when flush_unsaved_buffer is called before connect, 
and after connect but before login.");
+                   "Throws when flush_unsaved_buffer is called before connect, 
after connect but before login, and "
+                   "after disconnect.");
     iggy::ffi::Client *client = GetLoggedOutClient();
 
     ASSERT_THROW(client->flush_unsaved_buffer(make_numeric_identifier(1), 
make_numeric_identifier(1), 0, true),
@@ -255,6 +610,10 @@ TEST_F(LowLevelE2E_Client, 
FlushUnsavedBufferBeforeLoginThrows) {
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->flush_unsaved_buffer(make_numeric_identifier(1), 
make_numeric_identifier(1), 0, true),
                  std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->flush_unsaved_buffer(make_numeric_identifier(1), 
make_numeric_identifier(1), 0, true),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_Client, FlushUnsavedBufferOnNonExistentStreamThrows) {
@@ -341,7 +700,7 @@ TEST_F(LowLevelE2E_Client, FlushUnsavedBufferTwiceSucceeds) 
{
                                          0, "server_default"));
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    messages.push_back(iggy::ffi::make_message(to_payload("flush-twice")));
+    messages.push_back(iggy::ffi::make_message(to_payload("flush-twice"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
 
     ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0),
                                           "partition_id", 
partition_id_bytes(0), std::move(messages)));
@@ -372,6 +731,245 @@ TEST_F(LowLevelE2E_Client, 
FlushUnsavedBufferWithInvalidPartitionIdsThrows) {
     }
 }
 
+TEST_F(LowLevelE2E_Client, DeleteSegmentsBeforeLoginThrows) {
+    RecordProperty("description",
+                   "Rejects delete_segments before connect, after connect but 
before login, and after disconnect.");
+    const std::string stream_name   = GetRandomName();
+    const std::string topic_name    = GetRandomName();
+    iggy::ffi::Client *setup_client = GetLoggedInClient();
+
+    ASSERT_NO_THROW(setup_client->create_stream(stream_name));
+    TrackStream(stream_name);
+    
ASSERT_NO_THROW(setup_client->create_topic(make_string_identifier(stream_name), 
topic_name, 1, "none", 0,
+                                               "never_expire", 0, 
"server_default"));
+
+    iggy::ffi::Client *unauthenticated_client = GetLoggedOutClient();
+    
ASSERT_THROW(unauthenticated_client->delete_segments(make_string_identifier(stream_name),
+                                                         
make_string_identifier(topic_name), 0, 1),
+                 std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->connect());
+    
ASSERT_THROW(unauthenticated_client->delete_segments(make_string_identifier(stream_name),
+                                                         
make_string_identifier(topic_name), 0, 1),
+                 std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->delete_segments(make_string_identifier(stream_name),
+                                                         
make_string_identifier(topic_name), 0, 1),
+                 std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DeleteSegmentsOnNonExistentStreamThrows) {
+    RecordProperty("description", "Throws when deleting segments from a stream 
that does not exist.");
+    const std::string stream_name         = GetRandomName();
+    const std::string topic_name          = GetRandomName();
+    const std::string missing_stream_name = GetRandomName();
+    iggy::ffi::Client *client             = GetLoggedInClient();
+
+    ASSERT_NO_THROW(client->create_stream(stream_name));
+    TrackStream(stream_name);
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name, 1, "none", 0, "never_expire",
+                                         0, "server_default"));
+
+    ASSERT_THROW(
+        client->delete_segments(make_string_identifier(missing_stream_name), 
make_string_identifier(topic_name), 0, 1),
+        std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DeleteSegmentsOnNonExistentTopicThrows) {
+    RecordProperty("description", "Throws when deleting segments from a topic 
that does not exist.");
+    const std::string stream_name        = GetRandomName();
+    const std::string topic_name         = GetRandomName();
+    const std::string missing_topic_name = GetRandomName();
+    iggy::ffi::Client *client            = GetLoggedInClient();
+
+    ASSERT_NO_THROW(client->create_stream(stream_name));
+    TrackStream(stream_name);
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name, 1, "none", 0, "never_expire",
+                                         0, "server_default"));
+
+    ASSERT_THROW(
+        client->delete_segments(make_string_identifier(stream_name), 
make_string_identifier(missing_topic_name), 0, 1),
+        std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DeleteSegmentsOnNonExistentPartitionThrows) {
+    RecordProperty("description", "Throws when deleting segments from a 
partition that does not exist.");
+    const std::string stream_name = GetRandomName();
+    const std::string topic_name  = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    ASSERT_NO_THROW(client->create_stream(stream_name));
+    TrackStream(stream_name);
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name, 1, "none", 0, "never_expire",
+                                         0, "server_default"));
+
+    ASSERT_THROW(
+        client->delete_segments(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 999, 1),
+        std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, DeleteSegmentsWithZeroCountIsNoOp) {
+    RecordProperty("description", "Treats delete_segments with count 0 as a 
no-op.");
+    const std::string stream_name = GetRandomName();
+    const std::string topic_name  = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    ASSERT_NO_THROW(client->create_stream(stream_name));
+    TrackStream(stream_name);
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name, 1, "none", 0, "never_expire",
+                                         0, "server_default"));
+
+    std::uint32_t stream_id = 0;
+    std::uint32_t topic_id  = 0;
+    ASSERT_NO_THROW({
+        const auto stream_details = 
client->get_stream(make_string_identifier(stream_name));
+        ASSERT_EQ(stream_details.topics.size(), 1u);
+        stream_id = stream_details.id;
+        topic_id  = stream_details.topics.front().id;
+    });
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> messages;
+    for (std::uint32_t i = 0; i < 5; ++i) {
+        messages.push_back(iggy::ffi::make_message(to_payload("zero-count-" + 
std::to_string(i)),
+                                                   
rust::Vec<iggy::ffi::HeaderEntry>()));
+    }
+    ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id),
+                                          "partition_id", 
partition_id_bytes(0), std::move(messages)));
+
+    iggy::ffi::Partition partition_before_delete{};
+    ASSERT_NO_THROW({
+        const auto topic_details =
+            client->get_topic(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id));
+        for (const auto &partition : topic_details.partitions) {
+            if (partition.id == 0) {
+                partition_before_delete = partition;
+                break;
+            }
+        }
+    });
+
+    iggy::ffi::PolledMessages polled_before_delete{};
+    ASSERT_NO_THROW({
+        polled_before_delete =
+            client->poll_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id), 0, "consumer",
+                                  make_numeric_identifier(1005), "offset", 0, 
1000, false);
+    });
+
+    ASSERT_NO_THROW(
+        client->delete_segments(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 0, 0));
+
+    iggy::ffi::Partition partition_after_delete{};
+    ASSERT_NO_THROW({
+        const auto topic_details =
+            client->get_topic(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id));
+        for (const auto &partition : topic_details.partitions) {
+            if (partition.id == 0) {
+                partition_after_delete = partition;
+                break;
+            }
+        }
+    });
+
+    iggy::ffi::PolledMessages polled_after_delete{};
+    ASSERT_NO_THROW({
+        polled_after_delete =
+            client->poll_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id), 0, "consumer",
+                                  make_numeric_identifier(1006), "offset", 0, 
1000, false);
+    });
+
+    EXPECT_EQ(partition_after_delete.segments_count, 
partition_before_delete.segments_count);
+    EXPECT_EQ(partition_after_delete.current_offset, 
partition_before_delete.current_offset);
+    EXPECT_EQ(partition_after_delete.messages_count, 
partition_before_delete.messages_count);
+    EXPECT_EQ(partition_after_delete.size_bytes, 
partition_before_delete.size_bytes);
+    EXPECT_EQ(polled_after_delete.count, polled_before_delete.count);
+    ASSERT_EQ(polled_after_delete.messages.size(), 
polled_before_delete.messages.size());
+    for (std::size_t i = 0; i < polled_before_delete.messages.size(); ++i) {
+        EXPECT_EQ(polled_after_delete.messages[i].offset, 
polled_before_delete.messages[i].offset);
+    }
+}
+
+TEST_F(LowLevelE2E_Client, DeleteSegmentsWhenOnlyActiveSegmentRemainsIsNoOp) {
+    RecordProperty("description",
+                   "Keeps the partition unchanged when delete_segments is 
called with only the active segment.");
+    const std::string stream_name = GetRandomName();
+    const std::string topic_name  = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    ASSERT_NO_THROW(client->create_stream(stream_name));
+    TrackStream(stream_name);
+    ASSERT_NO_THROW(client->create_topic(make_string_identifier(stream_name), 
topic_name, 1, "none", 0, "never_expire",
+                                         0, "server_default"));
+
+    std::uint32_t stream_id = 0;
+    std::uint32_t topic_id  = 0;
+    ASSERT_NO_THROW({
+        const auto stream_details = 
client->get_stream(make_string_identifier(stream_name));
+        ASSERT_EQ(stream_details.topics.size(), 1u);
+        stream_id = stream_details.id;
+        topic_id  = stream_details.topics.front().id;
+    });
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> messages;
+    for (std::uint32_t i = 0; i < 5; ++i) {
+        messages.push_back(iggy::ffi::make_message(to_payload("active-only-" + 
std::to_string(i)),
+                                                   
rust::Vec<iggy::ffi::HeaderEntry>()));
+    }
+    ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id),
+                                          "partition_id", 
partition_id_bytes(0), std::move(messages)));
+
+    iggy::ffi::Partition partition_before_delete{};
+    ASSERT_NO_THROW({
+        const auto topic_details =
+            client->get_topic(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id));
+        for (const auto &partition : topic_details.partitions) {
+            if (partition.id == 0) {
+                partition_before_delete = partition;
+                break;
+            }
+        }
+    });
+    ASSERT_EQ(partition_before_delete.segments_count, 1u);
+
+    iggy::ffi::PolledMessages polled_before_delete{};
+    ASSERT_NO_THROW({
+        polled_before_delete =
+            client->poll_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id), 0, "consumer",
+                                  make_numeric_identifier(1007), "offset", 0, 
1000, false);
+    });
+
+    ASSERT_NO_THROW(
+        client->delete_segments(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 0, 1));
+
+    iggy::ffi::Partition partition_after_delete{};
+    ASSERT_NO_THROW({
+        const auto topic_details =
+            client->get_topic(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id));
+        for (const auto &partition : topic_details.partitions) {
+            if (partition.id == 0) {
+                partition_after_delete = partition;
+                break;
+            }
+        }
+    });
+
+    iggy::ffi::PolledMessages polled_after_delete{};
+    ASSERT_NO_THROW({
+        polled_after_delete =
+            client->poll_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(topic_id), 0, "consumer",
+                                  make_numeric_identifier(1008), "offset", 0, 
1000, false);
+    });
+
+    EXPECT_EQ(partition_after_delete.segments_count, 
partition_before_delete.segments_count);
+    EXPECT_EQ(partition_after_delete.current_offset, 
partition_before_delete.current_offset);
+    EXPECT_EQ(partition_after_delete.messages_count, 
partition_before_delete.messages_count);
+    EXPECT_EQ(partition_after_delete.size_bytes, 
partition_before_delete.size_bytes);
+    EXPECT_EQ(polled_after_delete.count, polled_before_delete.count);
+    ASSERT_EQ(polled_after_delete.messages.size(), 
polled_before_delete.messages.size());
+    for (std::size_t i = 0; i < polled_before_delete.messages.size(); ++i) {
+        EXPECT_EQ(polled_after_delete.messages[i].offset, 
polled_before_delete.messages[i].offset);
+    }
+}
+
 // TODO(slbotbm): add a test to create some streams, topics, partitions, and 
segments, send messages, and create
 // consumer groups and verify it.
 TEST_F(LowLevelE2E_Client, GetStatsReturnsServerStats) {
@@ -529,12 +1127,16 @@ TEST_F(LowLevelE2E_Client, 
GetStatsIsStableAcrossBackToBackCalls) {
 }
 
 TEST_F(LowLevelE2E_Client, GetMeBeforeLoginThrows) {
-    RecordProperty("description", "Rejects get_me before connect, and after 
connect but before login.");
+    RecordProperty("description",
+                   "Rejects get_me before connect, after connect but before 
login, and after disconnect.");
     iggy::ffi::Client *client = GetLoggedOutClient();
 
     ASSERT_THROW(client->get_me(), std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->get_me(), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_me(), std::exception);
 }
 
 TEST_F(LowLevelE2E_Client, GetMeReturnsCurrentClientDetails) {
@@ -699,12 +1301,16 @@ TEST_F(LowLevelE2E_Client, 
GetMeReturnsValidDetailsAfterReconnect) {
 }
 
 TEST_F(LowLevelE2E_Client, GetClientBeforeLoginThrows) {
-    RecordProperty("description", "Rejects get_client before connect, and 
after connect but before login.");
+    RecordProperty("description",
+                   "Rejects get_client before connect, after connect but 
before login, and after disconnect.");
     iggy::ffi::Client *client = GetLoggedOutClient();
 
     ASSERT_THROW(client->get_client(1), std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->get_client(1), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_client(1), std::exception);
 }
 
 TEST_F(LowLevelE2E_Client, GetClientWithWrongClientIdThrows) {
@@ -786,12 +1392,16 @@ TEST_F(LowLevelE2E_Client, 
GetClientIsStableAcrossBackToBackCalls) {
 }
 
 TEST_F(LowLevelE2E_Client, GetClientsBeforeLoginThrows) {
-    RecordProperty("description", "Rejects get_clients before connect, and 
after connect but before login.");
+    RecordProperty("description",
+                   "Rejects get_clients before connect, after connect but 
before login, and after disconnect.");
     iggy::ffi::Client *client = GetLoggedOutClient();
 
     ASSERT_THROW(client->get_clients(), std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->get_clients(), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_clients(), std::exception);
 }
 
 TEST_F(LowLevelE2E_Client, GetClientsReturnsActiveClientSessions) {
@@ -949,6 +1559,67 @@ TEST_F(LowLevelE2E_Client, 
GetClientsReflectsAdditionalSession) {
     EXPECT_TRUE(found_after);
 }
 
+TEST_F(LowLevelE2E_Client, GetClusterMetadataBeforeLoginThrows) {
+    RecordProperty(
+        "description",
+        "Rejects get_cluster_metadata before connect, after connect but before 
login, and after disconnect.");
+    iggy::ffi::Client *client = GetLoggedOutClient();
+
+    ASSERT_THROW(client->get_cluster_metadata(), std::exception);
+    ASSERT_NO_THROW(client->connect());
+    ASSERT_THROW(client->get_cluster_metadata(), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_cluster_metadata(), std::exception);
+}
+
+TEST_F(LowLevelE2E_Client, GetClusterMetadataReturnsSingleNodeMetadata) {
+    RecordProperty("description",
+                   "Returns the expected single-node cluster metadata shape 
from the default test server.");
+    iggy::ffi::Client *client = GetLoggedInClient();
+
+    iggy::ffi::ClusterMetadata metadata{};
+    ASSERT_NO_THROW({ metadata = client->get_cluster_metadata(); });
+
+    EXPECT_EQ(static_cast<std::string>(metadata.name), "single-node");
+    ASSERT_EQ(metadata.nodes.size(), 1u);
+
+    const auto &node = metadata.nodes[0];
+    EXPECT_FALSE(static_cast<std::string>(node.name).empty());
+    EXPECT_FALSE(static_cast<std::string>(node.ip).empty());
+    EXPECT_EQ(static_cast<std::string>(node.role), "leader");
+    EXPECT_EQ(static_cast<std::string>(node.status), "healthy");
+    EXPECT_NE(node.endpoints.tcp, 0u);
+    EXPECT_NE(node.endpoints.http, 0u);
+}
+
+TEST_F(LowLevelE2E_Client, GetClusterMetadataIsStableAcrossBackToBackCalls) {
+    RecordProperty("description", "Returns stable single-node cluster metadata 
across back-to-back calls.");
+    iggy::ffi::Client *client = GetLoggedInClient();
+
+    iggy::ffi::ClusterMetadata first_metadata{};
+    iggy::ffi::ClusterMetadata second_metadata{};
+    ASSERT_NO_THROW({
+        first_metadata  = client->get_cluster_metadata();
+        second_metadata = client->get_cluster_metadata();
+    });
+
+    EXPECT_EQ(static_cast<std::string>(first_metadata.name), 
static_cast<std::string>(second_metadata.name));
+    ASSERT_EQ(first_metadata.nodes.size(), 1u);
+    ASSERT_EQ(second_metadata.nodes.size(), 1u);
+
+    const auto &first_node  = first_metadata.nodes[0];
+    const auto &second_node = second_metadata.nodes[0];
+    EXPECT_EQ(static_cast<std::string>(first_node.name), 
static_cast<std::string>(second_node.name));
+    EXPECT_EQ(static_cast<std::string>(first_node.ip), 
static_cast<std::string>(second_node.ip));
+    EXPECT_EQ(static_cast<std::string>(first_node.role), 
static_cast<std::string>(second_node.role));
+    EXPECT_EQ(static_cast<std::string>(first_node.status), 
static_cast<std::string>(second_node.status));
+    EXPECT_EQ(first_node.endpoints.tcp, second_node.endpoints.tcp);
+    EXPECT_EQ(first_node.endpoints.quic, second_node.endpoints.quic);
+    EXPECT_EQ(first_node.endpoints.http, second_node.endpoints.http);
+    EXPECT_EQ(first_node.endpoints.websocket, second_node.endpoints.websocket);
+}
+
 TEST_F(LowLevelE2E_Client, PingSucceedsForNewConnection) {
     RecordProperty("description", "Successfully pings the server from a fresh 
unauthenticated client session.");
     iggy::ffi::Client *client = GetLoggedOutClient();
@@ -980,13 +1651,17 @@ TEST_F(LowLevelE2E_Client, 
HeartbeatIntervalReturnsConfiguredValueFromConnection
 }
 
 TEST_F(LowLevelE2E_Client, SnapshotBeforeLoginThrows) {
-    RecordProperty("description", "Rejects snapshot before connect, and after 
connect but before login.");
+    RecordProperty("description",
+                   "Rejects snapshot before connect, after connect but before 
login, and after disconnect.");
     iggy::ffi::Client *client = GetLoggedOutClient();
 
     ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"test"})), 
std::exception);
 
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"test"})), 
std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->snapshot("deflated", make_snapshot_types({"test"})), 
std::exception);
 }
 
 TEST_F(LowLevelE2E_Client, SnapshotAllCombinedWithOtherTypeThrows) {
diff --git a/foreign/cpp/tests/common/test_helpers.hpp 
b/foreign/cpp/tests/common/test_helpers.hpp
index a6f240998..de81c9c7b 100644
--- a/foreign/cpp/tests/common/test_helpers.hpp
+++ b/foreign/cpp/tests/common/test_helpers.hpp
@@ -24,6 +24,7 @@
 #include <initializer_list>
 #include <random>
 #include <string>
+#include <utility>
 
 #include <gtest/gtest.h>
 
@@ -66,6 +67,38 @@ inline rust::Vec<rust::String> 
make_snapshot_types(std::initializer_list<const c
     return snapshot_types;
 }
 
+inline iggy::ffi::HeaderField make_header_field(const iggy::ffi::HeaderKind 
kind, rust::Vec<std::uint8_t> value) {
+    iggy::ffi::HeaderField field;
+    field.kind  = static_cast<std::uint8_t>(kind);
+    field.value = std::move(value);
+    return field;
+}
+
+inline iggy::ffi::HeaderEntry make_header_entry(iggy::ffi::HeaderField key, 
iggy::ffi::HeaderField value) {
+    iggy::ffi::HeaderEntry entry;
+    entry.key   = std::move(key);
+    entry.value = std::move(value);
+    return entry;
+}
+
+inline bool has_header(const rust::Vec<iggy::ffi::HeaderEntry> &headers,
+                       const std::uint8_t key_kind,
+                       const rust::Vec<std::uint8_t> &key_value,
+                       const std::uint8_t value_kind,
+                       const rust::Vec<std::uint8_t> &value_value) {
+    for (const auto &header : headers) {
+        if (header.key.kind == key_kind && header.value.kind == value_kind &&
+            header.key.value.size() == key_value.size() &&
+            std::equal(header.key.value.begin(), header.key.value.end(), 
key_value.begin()) &&
+            header.value.value.size() == value_value.size() &&
+            std::equal(header.value.value.begin(), header.value.value.end(), 
value_value.begin())) {
+            return true;
+        }
+    }
+
+    return false;
+}
+
 struct TrackedConsumerGroup {
     std::string stream_name;
     std::string topic_name;
diff --git a/foreign/cpp/tests/consumer_group/low_level_e2e.cpp 
b/foreign/cpp/tests/consumer_group/low_level_e2e.cpp
index f48acbd19..e729d42ff 100644
--- a/foreign/cpp/tests/consumer_group/low_level_e2e.cpp
+++ b/foreign/cpp/tests/consumer_group/low_level_e2e.cpp
@@ -150,6 +150,11 @@ TEST_F(LowLevelE2E_ConsumerGroup, 
CreateConsumerGroupBeforeLoginThrows) {
     
ASSERT_THROW(unauthenticated_client->create_consumer_group(make_string_identifier(stream_name),
                                                                
make_string_identifier(topic_name), group_name),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->create_consumer_group(make_string_identifier(stream_name),
+                                                               
make_string_identifier(topic_name), group_name),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_ConsumerGroup, 
GetConsumerGroupReturnsSameInfoAsCreateConsumerGroup) {
@@ -227,6 +232,11 @@ TEST_F(LowLevelE2E_ConsumerGroup, 
GetConsumerGroupsBeforeLoginThrows) {
     
ASSERT_THROW(unauthenticated_client->get_consumer_groups(make_string_identifier(stream_name),
                                                              
make_string_identifier(topic_name)),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->get_consumer_groups(make_string_identifier(stream_name),
+                                                             
make_string_identifier(topic_name)),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_ConsumerGroup, JoinConsumerGroupSucceeds) {
@@ -274,6 +284,12 @@ TEST_F(LowLevelE2E_ConsumerGroup, 
JoinConsumerGroupBeforeLoginThrows) {
                                                              
make_string_identifier(topic_name),
                                                              
make_string_identifier(group_name)),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->join_consumer_group(make_string_identifier(stream_name),
+                                                             
make_string_identifier(topic_name),
+                                                             
make_string_identifier(group_name)),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_ConsumerGroup, 
JoinConsumerGroupOnNonExistentResourcesThrows) {
@@ -529,6 +545,12 @@ TEST_F(LowLevelE2E_ConsumerGroup, 
LeaveConsumerGroupBeforeLoginThrows) {
                                                               
make_string_identifier(topic_name),
                                                               
make_string_identifier(group_name)),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->leave_consumer_group(make_string_identifier(stream_name),
+                                                              
make_string_identifier(topic_name),
+                                                              
make_string_identifier(group_name)),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_ConsumerGroup, 
LeaveConsumerGroupOnNonExistentResourcesThrows) {
@@ -896,6 +918,12 @@ TEST_F(LowLevelE2E_ConsumerGroup, 
GetConsumerGroupBeforeLoginThrows) {
                                                             
make_string_identifier(topic_name),
                                                             
make_string_identifier(group_name)),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->get_consumer_group(make_string_identifier(stream_name),
+                                                            
make_string_identifier(topic_name),
+                                                            
make_string_identifier(group_name)),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_ConsumerGroup, 
GetConsumerGroupOnNonExistentResourcesThrows) {
@@ -1000,6 +1028,12 @@ TEST_F(LowLevelE2E_ConsumerGroup, 
DeleteConsumerGroupBeforeLoginThrows) {
                                                                
make_string_identifier(topic_name),
                                                                
make_string_identifier(group_name)),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->delete_consumer_group(make_string_identifier(stream_name),
+                                                               
make_string_identifier(topic_name),
+                                                               
make_string_identifier(group_name)),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_ConsumerGroup, 
DeleteConsumerGroupOnNonExistentResourcesThrows) {
diff --git a/foreign/cpp/tests/message/low_level_e2e.cpp 
b/foreign/cpp/tests/message/low_level_e2e.cpp
index 76c29b7a4..5fca7dc29 100644
--- a/foreign/cpp/tests/message/low_level_e2e.cpp
+++ b/foreign/cpp/tests/message/low_level_e2e.cpp
@@ -42,7 +42,8 @@ TEST_F(LowLevelE2E_Message, SendAndPollMessagesRoundTrip) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 10; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("test message " + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("test message " + 
std::to_string(i)),
+                                           
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -76,7 +77,7 @@ TEST_F(LowLevelE2E_Message, PollMessagesVerifyMessageIds) {
                          "server_default");
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg  = iggy::ffi::make_message(to_payload("id-test-message"));
+    auto msg  = iggy::ffi::make_message(to_payload("id-test-message"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     msg.id_lo = 42;
     msg.id_hi = 0;
     messages.push_back(std::move(msg));
@@ -117,12 +118,22 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesBeforeLoginThrows) {
     ASSERT_NO_THROW(client->connect());
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("should-fail"));
+    auto msg = iggy::ffi::make_message(to_payload("should-fail"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     ASSERT_THROW(client->send_messages(make_numeric_identifier(1), 
make_numeric_identifier(1), "partition_id",
                                        partition_id_bytes(0), 
std::move(messages)),
                  std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> disconnected_messages;
+    auto disconnected_msg =
+        iggy::ffi::make_message(to_payload("should-still-fail"), 
rust::Vec<iggy::ffi::HeaderEntry>());
+    disconnected_messages.push_back(std::move(disconnected_msg));
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(1), 
make_numeric_identifier(1), "partition_id",
+                                       partition_id_bytes(0), 
std::move(disconnected_messages)),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_Message, SendMessagesWithInvalidStreamId) {
@@ -130,7 +141,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesWithInvalidStreamId) {
     iggy::ffi::Client *client = GetLoggedInClient();
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("test"));
+    auto msg = iggy::ffi::make_message(to_payload("test"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     iggy::ffi::Identifier invalid_id;
@@ -147,7 +158,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesToNonExistentStream) {
     iggy::ffi::Client *client = GetLoggedInClient();
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("test"));
+    auto msg = iggy::ffi::make_message(to_payload("test"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     
ASSERT_THROW(client->send_messages(make_string_identifier("nonexistent-stream-12345"),
 make_numeric_identifier(0),
@@ -168,7 +179,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesWithInvalidPartitioningKind) {
                          "server_default");
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("test"));
+    auto msg = iggy::ffi::make_message(to_payload("test"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "invalid_kind",
@@ -189,7 +200,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesWithInvalidPartitioningValue) {
                          "server_default");
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("test"));
+    auto msg = iggy::ffi::make_message(to_payload("test"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     rust::Vec<std::uint8_t> short_bytes;
@@ -216,7 +227,8 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesToSpecificPartitionVerified) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("partition-test-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("partition-test-" + 
std::to_string(i)),
+                                           
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -267,7 +279,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessageWithEmptyPayloadThrows) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     rust::Vec<std::uint8_t> empty_payload;
-    auto msg = iggy::ffi::make_message(std::move(empty_payload));
+    auto msg = iggy::ffi::make_message(std::move(empty_payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -296,7 +308,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessageWithOversizedPayloadThrows) {
     }
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(std::move(oversized_payload));
+    auto msg = iggy::ffi::make_message(std::move(oversized_payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -318,7 +330,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesPreservesOrder) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 50; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("order-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("order-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -351,7 +364,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesWithDuplicateIds) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 3; i++) {
-        auto msg  = iggy::ffi::make_message(to_payload("dup-id-msg-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("dup-id-msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         msg.id_lo = 99;
         msg.id_hi = 0;
         messages.push_back(std::move(msg));
@@ -396,16 +410,16 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesWithVariousPayloads) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
 
-    auto msg0 = iggy::ffi::make_message(to_payload("simple ascii"));
+    auto msg0 = iggy::ffi::make_message(to_payload("simple ascii"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg0));
 
-    auto msg1 = iggy::ffi::make_message(std::move(payload_null));
+    auto msg1 = iggy::ffi::make_message(std::move(payload_null), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg1));
 
-    auto msg2 = iggy::ffi::make_message(to_payload("héllo wörld"));
+    auto msg2 = iggy::ffi::make_message(to_payload("héllo wörld"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg2));
 
-    auto msg3 = iggy::ffi::make_message(std::move(payload_binary));
+    auto msg3 = iggy::ffi::make_message(std::move(payload_binary), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg3));
 
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -435,11 +449,365 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesWithVariousPayloads) {
     EXPECT_EQ(polled.messages[3].payload[3], 0xEF);
 }
 
+TEST_F(LowLevelE2E_Message, SendAndPollMessageWithTypedHeadersRoundTrip) {
+    RecordProperty(
+        "description",
+        "Sends one message per typed header kind and verifies payload, IDs, 
header contents, and encoded size.");
+    const std::string stream_name = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    client->create_stream(stream_name);
+    auto stream = client->get_stream(make_string_identifier(stream_name));
+    TrackStream(stream.id);
+    const std::string topic_name = GetRandomName();
+    client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, 
"none", 0, "never_expire", 0,
+                         "server_default");
+
+    struct ExpectedHeaderMessage {
+        const char *key;
+        iggy::ffi::HeaderKind value_kind;
+        const rust::Vec<std::uint8_t> *value;
+    };
+
+    const rust::Vec<std::uint8_t> raw_value{0xDE, 0xAD, 0xBE, 0xEF};           
                 // raw bytes 0xDEADBEEF
+    const rust::Vec<std::uint8_t> string_value = to_payload("hello");          
                 // UTF-8 string "hello"
+    const rust::Vec<std::uint8_t> bool_value{0x01};                            
                 // bool true
+    const rust::Vec<std::uint8_t> int8_value{0xFB};                            
                 // int8 -5
+    const rust::Vec<std::uint8_t> int16_value{0x2E, 0xFB};                     
                 // int16 -1234
+    const rust::Vec<std::uint8_t> int32_value{0xEB, 0x32, 0xA4, 0xF8};         
                 // int32 -123456789
+    const rust::Vec<std::uint8_t> int64_value{0x79, 0x29, 0xED, 0xFF, 0xFF, 
0xFF, 0xFF, 0xFF};  // int64 -1234567
+    const rust::Vec<std::uint8_t> int128_value{
+        0x00, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99,
+        0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11};                 // 
int128 0x112233445566778899AABBCCDDEEFF00
+    const rust::Vec<std::uint8_t> uint8_value{0xFA};                     // 
uint8 250
+    const rust::Vec<std::uint8_t> uint16_value{0xD2, 0x04};              // 
uint16 1234
+    const rust::Vec<std::uint8_t> uint32_value{0x78, 0x56, 0x34, 0x12};  // 
uint32 0x12345678
+    const rust::Vec<std::uint8_t> uint64_value{0x88, 0x77, 0x66, 0x55,
+                                               0x44, 0x33, 0x22, 0x11};  // 
uint64 0x1122334455667788
+    const rust::Vec<std::uint8_t> uint128_value{
+        0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE,
+        0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01};                  // 
uint128 0x0123456789ABCDEFFEDCBA9876543210
+    const rust::Vec<std::uint8_t> float32_value{0x00, 0x00, 0x80, 0x3F};  // 
float32 1.0
+    const rust::Vec<std::uint8_t> float64_value{0x00, 0x00, 0x00, 0x00, 0x00, 
0x00, 0xF0, 0x3F};  // float64 1.0
+    const ExpectedHeaderMessage expected_messages[] = {
+        {"raw", iggy::ffi::HeaderKind::Raw, &raw_value},
+        {"string", iggy::ffi::HeaderKind::String, &string_value},
+        {"bool", iggy::ffi::HeaderKind::Bool, &bool_value},
+        {"int8", iggy::ffi::HeaderKind::Int8, &int8_value},
+        {"int16", iggy::ffi::HeaderKind::Int16, &int16_value},
+        {"int32", iggy::ffi::HeaderKind::Int32, &int32_value},
+        {"int64", iggy::ffi::HeaderKind::Int64, &int64_value},
+        {"int128", iggy::ffi::HeaderKind::Int128, &int128_value},
+        {"uint8", iggy::ffi::HeaderKind::Uint8, &uint8_value},
+        {"uint16", iggy::ffi::HeaderKind::Uint16, &uint16_value},
+        {"uint32", iggy::ffi::HeaderKind::Uint32, &uint32_value},
+        {"uint64", iggy::ffi::HeaderKind::Uint64, &uint64_value},
+        {"uint128", iggy::ffi::HeaderKind::Uint128, &uint128_value},
+        {"float32", iggy::ffi::HeaderKind::Float32, &float32_value},
+        {"float64", iggy::ffi::HeaderKind::Float64, &float64_value},
+    };
+    rust::Vec<iggy::ffi::IggyMessageToSend> messages;
+    for (const auto &expected : expected_messages) {
+        rust::Vec<iggy::ffi::HeaderEntry> headers;
+        
headers.push_back(make_header_entry(make_header_field(iggy::ffi::HeaderKind::String,
 to_payload(expected.key)),
+                                            
make_header_field(expected.value_kind, *expected.value)));
+
+        messages.push_back(
+            iggy::ffi::make_message(to_payload(std::string("payload-") + 
expected.key), std::move(headers)));
+    }
+
+    ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0),
+                                          "partition_id", 
partition_id_bytes(0), std::move(messages)));
+
+    auto polled = client->poll_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), 0, "consumer",
+                                        make_numeric_identifier(1), "offset", 
0, 100, false);
+
+    constexpr std::size_t expected_message_count = sizeof(expected_messages) / 
sizeof(expected_messages[0]);
+    ASSERT_EQ(polled.count, expected_message_count);
+    ASSERT_EQ(polled.messages.size(), expected_message_count);
+    for (std::size_t i = 0; i < expected_message_count; ++i) {
+        const auto &expected       = expected_messages[i];
+        const auto &polled_message = polled.messages[i];
+
+        EXPECT_EQ(std::string(polled_message.payload.begin(), 
polled_message.payload.end()),
+                  std::string("payload-") + expected.key);
+        ASSERT_EQ(polled_message.user_headers.size(), 1u);
+        EXPECT_EQ(polled_message.user_headers_length,
+                  static_cast<std::uint32_t>(10 + 
std::string(expected.key).size() + expected.value->size()));
+        EXPECT_TRUE(has_header(polled_message.user_headers, 
static_cast<std::uint8_t>(iggy::ffi::HeaderKind::String),
+                               to_payload(expected.key), 
static_cast<std::uint8_t>(expected.value_kind),
+                               *expected.value));
+    }
+}
+
+TEST_F(LowLevelE2E_Message, SendMessageWithDuplicateTypedHeaderKeysThrows) {
+    RecordProperty("description", "Throws when a single message contains 
duplicate typed header keys.");
+    const std::string stream_name = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    client->create_stream(stream_name);
+    auto stream = client->get_stream(make_string_identifier(stream_name));
+    TrackStream(stream.id);
+    const std::string topic_name = GetRandomName();
+    client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, 
"none", 0, "never_expire", 0,
+                         "server_default");
+
+    rust::Vec<iggy::ffi::HeaderEntry> headers;
+    
headers.push_back(make_header_entry(make_header_field(iggy::ffi::HeaderKind::String,
 to_payload("dup-key")),
+                                        
make_header_field(iggy::ffi::HeaderKind::String, to_payload("first"))));
+    
headers.push_back(make_header_entry(make_header_field(iggy::ffi::HeaderKind::String,
 to_payload("dup-key")),
+                                        
make_header_field(iggy::ffi::HeaderKind::String, to_payload("second"))));
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> messages;
+    messages.push_back(iggy::ffi::make_message(to_payload("payload"), 
std::move(headers)));
+
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(messages)),
+                 std::exception);
+}
+
+TEST_F(LowLevelE2E_Message, SendMessageWithWrongFixedWidthHeaderBytesThrows) {
+    RecordProperty("description", "Throws when a typed header uses a 
fixed-width kind with the wrong byte count.");
+    const std::string stream_name = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    client->create_stream(stream_name);
+    auto stream = client->get_stream(make_string_identifier(stream_name));
+    TrackStream(stream.id);
+    const std::string topic_name = GetRandomName();
+    client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, 
"none", 0, "never_expire", 0,
+                         "server_default");
+
+    struct FixedWidthHeaderCase {
+        const char *key;
+        iggy::ffi::HeaderKind kind;
+    };
+    const FixedWidthHeaderCase fixed_width_header_cases[] = {
+        {"broken-bool", iggy::ffi::HeaderKind::Bool},       {"broken-int8", 
iggy::ffi::HeaderKind::Int8},
+        {"broken-int16", iggy::ffi::HeaderKind::Int16},     {"broken-int32", 
iggy::ffi::HeaderKind::Int32},
+        {"broken-int64", iggy::ffi::HeaderKind::Int64},     {"broken-int128", 
iggy::ffi::HeaderKind::Int128},
+        {"broken-uint8", iggy::ffi::HeaderKind::Uint8},     {"broken-uint16", 
iggy::ffi::HeaderKind::Uint16},
+        {"broken-uint32", iggy::ffi::HeaderKind::Uint32},   {"broken-uint64", 
iggy::ffi::HeaderKind::Uint64},
+        {"broken-uint128", iggy::ffi::HeaderKind::Uint128}, {"broken-float32", 
iggy::ffi::HeaderKind::Float32},
+        {"broken-float64", iggy::ffi::HeaderKind::Float64},
+    };
+
+    for (const auto &test_case : fixed_width_header_cases) {
+        SCOPED_TRACE(test_case.key);
+
+        rust::Vec<iggy::ffi::HeaderEntry> headers;
+        rust::Vec<std::uint8_t> broken_width_value;
+        broken_width_value.push_back(0x01);
+        broken_width_value.push_back(0x02);
+        broken_width_value.push_back(0x03);
+        
headers.push_back(make_header_entry(make_header_field(iggy::ffi::HeaderKind::String,
 to_payload(test_case.key)),
+                                            make_header_field(test_case.kind, 
std::move(broken_width_value))));
+
+        rust::Vec<iggy::ffi::IggyMessageToSend> messages;
+        messages.push_back(iggy::ffi::make_message(to_payload("payload"), 
std::move(headers)));
+
+        ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0),
+                                           "partition_id", 
partition_id_bytes(0), std::move(messages)),
+                     std::exception);
+    }
+}
+
+TEST_F(LowLevelE2E_Message, SendMessageWithInvalidTypedHeaderKindThrows) {
+    RecordProperty("description", "Throws when a typed header uses an 
unsupported header kind code.");
+    const std::string stream_name = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    client->create_stream(stream_name);
+    auto stream = client->get_stream(make_string_identifier(stream_name));
+    TrackStream(stream.id);
+    const std::string topic_name = GetRandomName();
+    client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, 
"none", 0, "never_expire", 0,
+                         "server_default");
+
+    iggy::ffi::HeaderField invalid_key;
+    invalid_key.kind  = 255;
+    invalid_key.value = to_payload("bad-kind");
+
+    rust::Vec<iggy::ffi::HeaderEntry> headers;
+    headers.push_back(make_header_entry(std::move(invalid_key),
+                                        
make_header_field(iggy::ffi::HeaderKind::String, to_payload("value"))));
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> messages;
+    messages.push_back(iggy::ffi::make_message(to_payload("payload"), 
std::move(headers)));
+
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(messages)),
+                 std::exception);
+}
+
+TEST_F(LowLevelE2E_Message, SendMessageWithInvalidTypedHeaderSizesThrows) {
+    RecordProperty("description", "Throws when typed header key or value sizes 
violate Rust header size constraints.");
+    const std::string stream_name = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    client->create_stream(stream_name);
+    auto stream = client->get_stream(make_string_identifier(stream_name));
+    TrackStream(stream.id);
+    const std::string topic_name = GetRandomName();
+    client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, 
"none", 0, "never_expire", 0,
+                         "server_default");
+
+    rust::Vec<iggy::ffi::HeaderEntry> empty_key_headers;
+    empty_key_headers.push_back(
+        make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, 
rust::Vec<std::uint8_t>()),
+                          make_header_field(iggy::ffi::HeaderKind::String, 
to_payload("value"))));
+    rust::Vec<iggy::ffi::IggyMessageToSend> empty_key_messages;
+    
empty_key_messages.push_back(iggy::ffi::make_message(to_payload("payload"), 
std::move(empty_key_headers)));
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(empty_key_messages)),
+                 std::exception);
+
+    rust::Vec<iggy::ffi::HeaderEntry> empty_raw_value_headers;
+    empty_raw_value_headers.push_back(
+        make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, 
to_payload("key")),
+                          make_header_field(iggy::ffi::HeaderKind::Raw, 
rust::Vec<std::uint8_t>())));
+    rust::Vec<iggy::ffi::IggyMessageToSend> empty_raw_value_messages;
+    empty_raw_value_messages.push_back(
+        iggy::ffi::make_message(to_payload("payload"), 
std::move(empty_raw_value_headers)));
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(empty_raw_value_messages)),
+                 std::exception);
+
+    rust::Vec<std::uint8_t> oversized_value_bytes;
+    for (std::size_t index = 0; index < 256; ++index) {
+        oversized_value_bytes.push_back(static_cast<std::uint8_t>(index));
+    }
+    rust::Vec<iggy::ffi::HeaderEntry> oversized_value_headers;
+    oversized_value_headers.push_back(
+        make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, 
to_payload("key")),
+                          make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(oversized_value_bytes))));
+    rust::Vec<iggy::ffi::IggyMessageToSend> oversized_value_messages;
+    oversized_value_messages.push_back(
+        iggy::ffi::make_message(to_payload("payload"), 
std::move(oversized_value_headers)));
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(oversized_value_messages)),
+                 std::exception);
+
+    rust::Vec<std::uint8_t> oversized_string_bytes;
+    for (std::size_t index = 0; index < 256; ++index) {
+        oversized_string_bytes.push_back('a');
+    }
+    rust::Vec<iggy::ffi::HeaderEntry> oversized_string_headers;
+    oversized_string_headers.push_back(
+        make_header_entry(make_header_field(iggy::ffi::HeaderKind::String, 
to_payload("key")),
+                          make_header_field(iggy::ffi::HeaderKind::String, 
std::move(oversized_string_bytes))));
+    rust::Vec<iggy::ffi::IggyMessageToSend> oversized_string_messages;
+    oversized_string_messages.push_back(
+        iggy::ffi::make_message(to_payload("payload"), 
std::move(oversized_string_headers)));
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(oversized_string_messages)),
+                 std::exception);
+}
+
+TEST_F(LowLevelE2E_Message, SendMessageAtUserHeadersSizeBoundary) {
+    RecordProperty("description",
+                   "Throws when encoded user headers exceed 100_000 bytes and 
succeeds at exactly 100_000 bytes.");
+    const std::string stream_name = GetRandomName();
+    iggy::ffi::Client *client     = GetLoggedInClient();
+
+    client->create_stream(stream_name);
+    auto stream = client->get_stream(make_string_identifier(stream_name));
+    TrackStream(stream.id);
+    const std::string topic_name = GetRandomName();
+    client->create_topic(make_numeric_identifier(stream.id), topic_name, 1, 
"none", 0, "never_expire", 0,
+                         "server_default");
+
+    constexpr std::uint32_t kMaxUserHeadersBytes     = 100'000u;
+    constexpr std::uint32_t kFullHeaderEncodedBytes  = 267u;  // 10 bytes 
framing + 2-byte key + 255-byte value
+    constexpr std::uint32_t kFullHeaderCount         = 374u;
+    constexpr std::uint32_t kTailExactValueBytes     = 130u;
+    constexpr std::uint32_t kTailOversizedValueBytes = 131u;
+
+    rust::Vec<iggy::ffi::HeaderEntry> oversized_headers;
+    for (std::uint32_t index = 0; index < kFullHeaderCount; ++index) {
+        rust::Vec<std::uint8_t> key_bytes;
+        key_bytes.push_back(static_cast<std::uint8_t>(index & 0xFF));
+        key_bytes.push_back(static_cast<std::uint8_t>((index >> 8) & 0xFF));
+
+        rust::Vec<std::uint8_t> value_bytes;
+        for (std::uint32_t value_index = 0; value_index < 255u; ++value_index) 
{
+            value_bytes.push_back(static_cast<std::uint8_t>(value_index));
+        }
+
+        oversized_headers.push_back(
+            make_header_entry(make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(key_bytes)),
+                              make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(value_bytes))));
+    }
+    rust::Vec<std::uint8_t> oversized_tail_key_bytes;
+    
oversized_tail_key_bytes.push_back(static_cast<std::uint8_t>(kFullHeaderCount & 
0xFF));
+    
oversized_tail_key_bytes.push_back(static_cast<std::uint8_t>((kFullHeaderCount 
>> 8) & 0xFF));
+    rust::Vec<std::uint8_t> oversized_tail_value_bytes;
+    for (std::uint32_t value_index = 0; value_index < 
kTailOversizedValueBytes; ++value_index) {
+        
oversized_tail_value_bytes.push_back(static_cast<std::uint8_t>(value_index));
+    }
+    oversized_headers.push_back(
+        make_header_entry(make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(oversized_tail_key_bytes)),
+                          make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(oversized_tail_value_bytes))));
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> oversized_messages;
+    oversized_messages.push_back(
+        iggy::ffi::make_message(to_payload("oversized-user-headers"), 
std::move(oversized_headers)));
+    ASSERT_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
+                                       partition_id_bytes(0), 
std::move(oversized_messages)),
+                 std::exception);
+
+    rust::Vec<iggy::ffi::HeaderEntry> exact_headers;
+    for (std::uint32_t index = 0; index < kFullHeaderCount; ++index) {
+        rust::Vec<std::uint8_t> key_bytes;
+        key_bytes.push_back(static_cast<std::uint8_t>(index & 0xFF));
+        key_bytes.push_back(static_cast<std::uint8_t>((index >> 8) & 0xFF));
+
+        rust::Vec<std::uint8_t> value_bytes;
+        for (std::uint32_t value_index = 0; value_index < 255u; ++value_index) 
{
+            value_bytes.push_back(static_cast<std::uint8_t>(value_index));
+        }
+
+        exact_headers.push_back(
+            make_header_entry(make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(key_bytes)),
+                              make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(value_bytes))));
+    }
+    rust::Vec<std::uint8_t> exact_tail_key_bytes;
+    exact_tail_key_bytes.push_back(static_cast<std::uint8_t>(kFullHeaderCount 
& 0xFF));
+    exact_tail_key_bytes.push_back(static_cast<std::uint8_t>((kFullHeaderCount 
>> 8) & 0xFF));
+    rust::Vec<std::uint8_t> exact_tail_value_bytes;
+    for (std::uint32_t value_index = 0; value_index < kTailExactValueBytes; 
++value_index) {
+        
exact_tail_value_bytes.push_back(static_cast<std::uint8_t>(value_index));
+    }
+    exact_headers.push_back(
+        make_header_entry(make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(exact_tail_key_bytes)),
+                          make_header_field(iggy::ffi::HeaderKind::Raw, 
std::move(exact_tail_value_bytes))));
+
+    rust::Vec<iggy::ffi::IggyMessageToSend> exact_messages;
+    
exact_messages.push_back(iggy::ffi::make_message(to_payload("exact-user-headers"),
 std::move(exact_headers)));
+    ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0),
+                                          "partition_id", 
partition_id_bytes(0), std::move(exact_messages)));
+
+    auto polled = client->poll_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), 0, "consumer",
+                                        make_numeric_identifier(1), "offset", 
0, 10, false);
+
+    ASSERT_EQ(polled.count, 1u);
+    ASSERT_EQ(polled.messages.size(), 1u);
+    EXPECT_EQ(std::string(polled.messages[0].payload.begin(), 
polled.messages[0].payload.end()), "exact-user-headers");
+    EXPECT_EQ(polled.messages[0].user_headers_length, kMaxUserHeadersBytes);
+    EXPECT_EQ(polled.messages[0].user_headers.size(), kFullHeaderCount + 1u);
+    EXPECT_EQ(kFullHeaderCount * kFullHeaderEncodedBytes + 10u + 2u + 
kTailExactValueBytes, kMaxUserHeadersBytes);
+}
+
 TEST_F(LowLevelE2E_Message, PollMessagesBeforeLoginThrows) {
     RecordProperty("description", "Throws when polling messages before 
authentication.");
     iggy::ffi::Client *client = GetLoggedOutClient();
     ASSERT_NO_THROW(client->connect());
 
+    ASSERT_THROW(client->poll_messages(make_numeric_identifier(1), 
make_numeric_identifier(0), 0, "consumer",
+                                       make_numeric_identifier(1), "offset", 
0, 10, false),
+                 std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
     ASSERT_THROW(client->poll_messages(make_numeric_identifier(1), 
make_numeric_identifier(0), 0, "consumer",
                                        make_numeric_identifier(1), "offset", 
0, 10, false),
                  std::exception);
@@ -515,7 +883,7 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesCountLessThanAvailable) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 10; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -543,7 +911,7 @@ TEST_F(LowLevelE2E_Message, PollMessagesWithLargeOffset) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -571,7 +939,7 @@ TEST_F(LowLevelE2E_Message, PollMessagesFirstStrategy) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 10; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -606,7 +974,7 @@ TEST_F(LowLevelE2E_Message, PollMessagesLastStrategy) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 10; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -642,7 +1010,7 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesNextStrategyNoAutoCommit) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -684,7 +1052,7 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesNextStrategyAutoCommit) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 10; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -732,7 +1100,7 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesConsumerIdIndependence) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -766,7 +1134,8 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesMultipleSendsThenPollOrder) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> batch1;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("batch1-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("batch1-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         batch1.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -774,7 +1143,8 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesMultipleSendsThenPollOrder) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> batch2;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("batch2-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("batch2-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         batch2.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -814,7 +1184,7 @@ TEST_F(LowLevelE2E_Message, PollMessagesMultipleCustomIds) 
{
     const std::uint64_t id_values[] = {100, 200, 300, 400, 500};
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg  = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         msg.id_lo = id_values[i];
         msg.id_hi = 0;
         messages.push_back(std::move(msg));
@@ -846,7 +1216,7 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesAfterStreamDeletedThrows) {
                          "server_default");
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("test"));
+    auto msg = iggy::ffi::make_message(to_payload("test"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -910,7 +1280,7 @@ TEST_F(LowLevelE2E_Message, 
PollMessagesWithoutSpecifyingPartition) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -946,7 +1316,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesTimestampStrategy) 
{
 
     rust::Vec<iggy::ffi::IggyMessageToSend> batch1;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("batch1-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("batch1-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         batch1.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -956,7 +1327,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesTimestampStrategy) 
{
 
     rust::Vec<iggy::ffi::IggyMessageToSend> batch2;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("batch2-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("batch2-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         batch2.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -1007,7 +1379,8 @@ TEST_F(LowLevelE2E_Message, PollMessagesMonotonicOffsets) 
{
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 20; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("mono-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("mono-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -1045,7 +1418,8 @@ TEST_F(LowLevelE2E_Message, SendMessagesLargeBatch) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 1000; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("batch-msg-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("batch-msg-" + 
std::to_string(i)), rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
 
@@ -1066,7 +1440,7 @@ TEST_F(LowLevelE2E_Message, 
SendMessagesWithInvalidTopicIdThrows) {
     iggy::ffi::Client *client = GetLoggedInClient();
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    auto msg = iggy::ffi::make_message(to_payload("test"));
+    auto msg = iggy::ffi::make_message(to_payload("test"), 
rust::Vec<iggy::ffi::HeaderEntry>());
     messages.push_back(std::move(msg));
 
     iggy::ffi::Identifier invalid_id;
@@ -1139,7 +1513,8 @@ TEST_F(LowLevelE2E_Message, 
ConsumerGroupCreateJoinAndPollMessages) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 10; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("cg-msg-" + 
std::to_string(i)));
+        auto msg =
+            iggy::ffi::make_message(to_payload("cg-msg-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
diff --git a/foreign/cpp/tests/message/unit_tests.cpp 
b/foreign/cpp/tests/message/unit_tests.cpp
index 50dd87790..f441c55e0 100644
--- a/foreign/cpp/tests/message/unit_tests.cpp
+++ b/foreign/cpp/tests/message/unit_tests.cpp
@@ -32,7 +32,7 @@ TEST(MessageTest, MakeMessageSetsPayload) {
         payload.push_back(static_cast<std::uint8_t>(c));
     }
 
-    auto msg = iggy::ffi::make_message(std::move(payload));
+    auto msg = iggy::ffi::make_message(std::move(payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     ASSERT_EQ(msg.payload.size(), text.size());
     for (std::size_t i = 0; i < text.size(); i++) {
@@ -45,7 +45,7 @@ TEST(MessageTest, MakeMessageZerosIdAndHeaders) {
     rust::Vec<std::uint8_t> payload;
     payload.push_back(0x42);
 
-    auto msg = iggy::ffi::make_message(std::move(payload));
+    auto msg = iggy::ffi::make_message(std::move(payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     EXPECT_EQ(msg.id_lo, 0u);
     EXPECT_EQ(msg.id_hi, 0u);
@@ -56,7 +56,7 @@ TEST(MessageTest, MakeMessageWithEmptyPayload) {
     RecordProperty("description", "Verifies make_message accepts an empty 
payload.");
     rust::Vec<std::uint8_t> empty_payload;
 
-    auto msg = iggy::ffi::make_message(std::move(empty_payload));
+    auto msg = iggy::ffi::make_message(std::move(empty_payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     ASSERT_EQ(msg.payload.size(), 0u);
 }
@@ -66,7 +66,7 @@ TEST(MessageTest, MakeMessageWithSingleByte) {
     rust::Vec<std::uint8_t> payload;
     payload.push_back(0xFF);
 
-    auto msg = iggy::ffi::make_message(std::move(payload));
+    auto msg = iggy::ffi::make_message(std::move(payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     ASSERT_EQ(msg.payload.size(), 1u);
     EXPECT_EQ(msg.payload[0], 0xFF);
@@ -79,7 +79,7 @@ TEST(MessageTest, MakeMessageWithNullBytes) {
     payload.push_back(0x01);
     payload.push_back(0x00);
 
-    auto msg = iggy::ffi::make_message(std::move(payload));
+    auto msg = iggy::ffi::make_message(std::move(payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     ASSERT_EQ(msg.payload.size(), 3u);
     EXPECT_EQ(msg.payload[0], 0x00);
@@ -91,7 +91,7 @@ TEST(MessageTest, MakeMessageThenSetCustomId) {
     RecordProperty("description", "Verifies custom ID can be set after 
make_message without affecting payload.");
     rust::Vec<std::uint8_t> payload;
     payload.push_back(0x42);
-    auto msg = iggy::ffi::make_message(std::move(payload));
+    auto msg = iggy::ffi::make_message(std::move(payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     msg.id_lo = 100;
     msg.id_hi = 200;
@@ -109,7 +109,7 @@ TEST(MessageTest, MakeMessageWithLargePayload) {
         payload.push_back(static_cast<std::uint8_t>(i % 256));
     }
 
-    auto msg = iggy::ffi::make_message(std::move(payload));
+    auto msg = iggy::ffi::make_message(std::move(payload), 
rust::Vec<iggy::ffi::HeaderEntry>());
 
     ASSERT_EQ(msg.payload.size(), 10000u);
     EXPECT_EQ(msg.payload[0], 0u);
diff --git a/foreign/cpp/tests/partition/low_level_e2e.cpp 
b/foreign/cpp/tests/partition/low_level_e2e.cpp
index 517c2c679..8b444eb69 100644
--- a/foreign/cpp/tests/partition/low_level_e2e.cpp
+++ b/foreign/cpp/tests/partition/low_level_e2e.cpp
@@ -62,6 +62,10 @@ TEST_F(LowLevelE2E_Partition, 
CreatePartitionsBeforeLoginThrows) {
     ASSERT_NO_THROW(client->connect());
     
ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 1),
                  std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    
ASSERT_THROW(client->create_partitions(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 1),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_Partition, CreatePartitionsOnNonExistentResourcesThrows) {
@@ -356,6 +360,10 @@ TEST_F(LowLevelE2E_Partition, 
DeletePartitionsBeforeLoginThrows) {
     ASSERT_NO_THROW(client->connect());
     
ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 1),
                  std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    
ASSERT_THROW(client->delete_partitions(make_string_identifier(stream_name), 
make_string_identifier(topic_name), 1),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_Partition, DeletePartitionsOnNonExistentResourcesThrows) {
diff --git a/foreign/cpp/tests/stream/low_level_e2e.cpp 
b/foreign/cpp/tests/stream/low_level_e2e.cpp
index 2cdd4fc52..2c3fad218 100644
--- a/foreign/cpp/tests/stream/low_level_e2e.cpp
+++ b/foreign/cpp/tests/stream/low_level_e2e.cpp
@@ -52,6 +52,9 @@ TEST_F(LowLevelE2E_Stream, CreateStreamBeforeLoginThrows) {
     ASSERT_THROW(client->create_stream(stream_name), std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->create_stream(stream_name), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->create_stream(stream_name), std::exception);
 }
 
 TEST_F(LowLevelE2E_Stream, CreateStreamValidatesNameConstraintsAndUniqueness) {
@@ -160,6 +163,10 @@ TEST_F(LowLevelE2E_Stream, UpdateStreamBeforeLoginThrows) {
     ASSERT_NO_THROW(unauthenticated_client->connect());
     
ASSERT_THROW(unauthenticated_client->update_stream(make_string_identifier(stream_name),
 updated_stream_name),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->update_stream(make_string_identifier(stream_name),
 updated_stream_name),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_Stream, UpdateStreamWithVariousUtf8Characters) {
@@ -287,7 +294,8 @@ TEST_F(LowLevelE2E_Stream, UpdateStreamOnlyChangesName) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 3; ++i) {
-        auto message = 
iggy::ffi::make_message(to_payload("stream-update-preserve-" + 
std::to_string(i)));
+        auto message = 
iggy::ffi::make_message(to_payload("stream-update-preserve-" + 
std::to_string(i)),
+                                               
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(message));
     }
     ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_id), 
make_numeric_identifier(0),
@@ -396,6 +404,9 @@ TEST_F(LowLevelE2E_Stream, DeleteStreamBeforeLoginThrows) {
     ASSERT_NO_THROW(client->connect());
 
     ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), 
std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->delete_stream(make_string_identifier(stream_name)), 
std::exception);
 }
 
 TEST_F(LowLevelE2E_Stream, DeleteStreamTwiceThrows) {
@@ -476,6 +487,9 @@ TEST_F(LowLevelE2E_Stream, 
GetStreamDetailsBeforeLoginThrows) {
     ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), 
std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), 
std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_stream(make_string_identifier(stream_name)), 
std::exception);
 }
 
 TEST_F(LowLevelE2E_Stream, GetDeletedStreamDetailsThrows) {
@@ -564,7 +578,8 @@ TEST_F(LowLevelE2E_Stream, GetStreamsFieldsVerification) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 5; i++) {
-        auto msg = iggy::ffi::make_message(to_payload("field-verify-message-" 
+ std::to_string(i)));
+        auto msg = iggy::ffi::make_message(to_payload("field-verify-message-" 
+ std::to_string(i)),
+                                           
rust::Vec<iggy::ffi::HeaderEntry>());
         messages.push_back(std::move(msg));
     }
     client->send_messages(make_numeric_identifier(stream.id), 
make_numeric_identifier(0), "partition_id",
@@ -592,6 +607,9 @@ TEST_F(LowLevelE2E_Stream, GetStreamsBeforeLoginThrows) {
     ASSERT_THROW(client->get_streams(), std::exception);
     ASSERT_NO_THROW(client->connect());
     ASSERT_THROW(client->get_streams(), std::exception);
+    ASSERT_NO_THROW(client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(client->disconnect());
+    ASSERT_THROW(client->get_streams(), std::exception);
 }
 
 TEST_F(LowLevelE2E_Stream, GetStreamsConsistentWithGetStream) {
@@ -716,7 +734,8 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamPreservesStreamMetadata) {
     ASSERT_EQ(stream_before_purge.topics.size(), 2u);
 
     rust::Vec<iggy::ffi::IggyMessageToSend> first_topic_messages;
-    
first_topic_messages.push_back(iggy::ffi::make_message(to_payload("preserve-stream-metadata")));
+    first_topic_messages.push_back(
+        iggy::ffi::make_message(to_payload("preserve-stream-metadata"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_before_purge.id),
                                           
make_string_identifier(first_topic_name), "partition_id",
                                           partition_id_bytes(0), 
std::move(first_topic_messages)));
@@ -809,7 +828,8 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamRemovesMessagesAndPreservesTopics) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> first_topic_messages;
     for (std::uint32_t i = 0; i < 3; ++i) {
-        
first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-first-"
 + std::to_string(i))));
+        
first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-first-"
 + std::to_string(i)),
+                                                               
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                           
make_numeric_identifier(first_topic_id), "partition_id",
@@ -817,8 +837,8 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamRemovesMessagesAndPreservesTopics) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> second_topic_messages;
     for (std::uint32_t i = 0; i < 2; ++i) {
-        second_topic_messages.push_back(
-            iggy::ffi::make_message(to_payload("purge-stream-second-" + 
std::to_string(i))));
+        
second_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-second-"
 + std::to_string(i)),
+                                                                
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                           
make_numeric_identifier(second_topic_id), "partition_id",
@@ -913,7 +933,8 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamAcrossMultipleTopicsAndPartitionsClearsEve
         rust::Vec<iggy::ffi::IggyMessageToSend> messages;
         for (std::uint32_t i = 0; i < 2; ++i) {
             messages.push_back(iggy::ffi::make_message(
-                to_payload("purge-stream-topic-a-" + 
std::to_string(partition_id) + "-" + std::to_string(i))));
+                to_payload("purge-stream-topic-a-" + 
std::to_string(partition_id) + "-" + std::to_string(i)),
+                rust::Vec<iggy::ffi::HeaderEntry>()));
         }
         
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                               
make_numeric_identifier(first_topic_id), "partition_id",
@@ -923,7 +944,8 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamAcrossMultipleTopicsAndPartitionsClearsEve
         rust::Vec<iggy::ffi::IggyMessageToSend> messages;
         for (std::uint32_t i = 0; i < 2; ++i) {
             messages.push_back(iggy::ffi::make_message(
-                to_payload("purge-stream-topic-b-" + 
std::to_string(partition_id) + "-" + std::to_string(i))));
+                to_payload("purge-stream-topic-b-" + 
std::to_string(partition_id) + "-" + std::to_string(i)),
+                rust::Vec<iggy::ffi::HeaderEntry>()));
         }
         
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                               
make_numeric_identifier(second_topic_id), "partition_id",
@@ -961,15 +983,15 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamThenSendMessagesAgainSucceeds) {
     const std::uint32_t topic_id = created_stream.topics.front().id;
 
     rust::Vec<iggy::ffi::IggyMessageToSend> first_batch;
-    first_batch.push_back(iggy::ffi::make_message(to_payload("before-purge")));
+    first_batch.push_back(iggy::ffi::make_message(to_payload("before-purge"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
 make_numeric_identifier(topic_id),
                                           "partition_id", 
partition_id_bytes(0), std::move(first_batch)));
 
     ASSERT_NO_THROW(client->purge_stream(make_string_identifier(stream_name)));
 
     rust::Vec<iggy::ffi::IggyMessageToSend> second_batch;
-    
second_batch.push_back(iggy::ffi::make_message(to_payload("after-purge-0")));
-    
second_batch.push_back(iggy::ffi::make_message(to_payload("after-purge-1")));
+    
second_batch.push_back(iggy::ffi::make_message(to_payload("after-purge-0"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
+    
second_batch.push_back(iggy::ffi::make_message(to_payload("after-purge-1"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
 make_numeric_identifier(topic_id),
                                           "partition_id", 
partition_id_bytes(0), std::move(second_batch)));
 
@@ -998,7 +1020,8 @@ TEST_F(LowLevelE2E_Stream, 
PurgeStreamTwiceKeepsStreamEmptyAndTopicsIntact) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
     for (std::uint32_t i = 0; i < 3; ++i) {
-        
messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-twice-" + 
std::to_string(i))));
+        
messages.push_back(iggy::ffi::make_message(to_payload("purge-stream-twice-" + 
std::to_string(i)),
+                                                   
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
 make_numeric_identifier(topic_id),
                                           "partition_id", 
partition_id_bytes(0), std::move(messages)));
@@ -1035,4 +1058,7 @@ TEST_F(LowLevelE2E_Stream, PurgeStreamBeforeLoginThrows) {
     
ASSERT_THROW(unauthenticated_client->purge_stream(make_string_identifier(stream_name)),
 std::exception);
     ASSERT_NO_THROW(unauthenticated_client->connect());
     
ASSERT_THROW(unauthenticated_client->purge_stream(make_string_identifier(stream_name)),
 std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->purge_stream(make_string_identifier(stream_name)),
 std::exception);
 }
diff --git a/foreign/cpp/tests/topic/low_level_e2e.cpp 
b/foreign/cpp/tests/topic/low_level_e2e.cpp
index 8a58e065c..82f442031 100644
--- a/foreign/cpp/tests/topic/low_level_e2e.cpp
+++ b/foreign/cpp/tests/topic/low_level_e2e.cpp
@@ -295,6 +295,11 @@ TEST_F(LowLevelE2E_Topic, CreateTopicBeforeLoginThrows) {
     
ASSERT_THROW(unauthenticated_client->create_topic(make_string_identifier(stream_name),
 topic_name, 1, "none", 0,
                                                       "server_default", 0, 
"server_default"),
                  std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->create_topic(make_string_identifier(stream_name),
 topic_name, 1, "none", 0,
+                                                      "server_default", 0, 
"server_default"),
+                 std::exception);
 }
 
 TEST_F(LowLevelE2E_Topic, DeleteTopicAfterCreate) {
@@ -398,6 +403,11 @@ TEST_F(LowLevelE2E_Topic, DeleteTopicBeforeLoginThrows) {
     ASSERT_THROW(
         
unauthenticated_client->delete_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)),
         std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    ASSERT_THROW(
+        
unauthenticated_client->delete_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)),
+        std::exception);
 }
 
 TEST_F(LowLevelE2E_Topic, DeleteTopicWithInvalidStreamIdentifierThrows) {
@@ -497,6 +507,11 @@ TEST_F(LowLevelE2E_Topic, GetTopicBeforeLoginThrows) {
     ASSERT_THROW(
         unauthenticated_client->get_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)),
         std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    ASSERT_THROW(
+        unauthenticated_client->get_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)),
+        std::exception);
 }
 
 TEST_F(LowLevelE2E_Topic, GetTopicWithWrongStreamIdThrows) {
@@ -730,6 +745,9 @@ TEST_F(LowLevelE2E_Topic, GetTopicsBeforeLoginThrows) {
     
ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)),
 std::exception);
     ASSERT_NO_THROW(unauthenticated_client->connect());
     
ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)),
 std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    
ASSERT_THROW(unauthenticated_client->get_topics(make_string_identifier(stream_name)),
 std::exception);
 }
 
 TEST_F(LowLevelE2E_Topic, GetTopicsReturnsEmptyForStreamWithoutTopics) {
@@ -875,7 +893,8 @@ TEST_F(LowLevelE2E_Topic, UpdateTopicDoesNotChangeMessages) 
{
     const auto topic_id = created_stream.topics.front().id;
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    
messages.push_back(iggy::ffi::make_message(to_payload("message-before-topic-update")));
+    messages.push_back(
+        iggy::ffi::make_message(to_payload("message-before-topic-update"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
 make_numeric_identifier(topic_id),
                                           "partition_id", 
partition_id_bytes(0), std::move(messages)));
 
@@ -1080,6 +1099,12 @@ TEST_F(LowLevelE2E_Topic, UpdateTopicBeforeLoginThrows) {
         
unauthenticated_client->update_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name),
                                              updated_topic_name, "gzip", 1, 
"duration", 1000, "1GiB"),
         std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    ASSERT_THROW(
+        
unauthenticated_client->update_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name),
+                                             updated_topic_name, "gzip", 1, 
"duration", 1000, "1GiB"),
+        std::exception);
 }
 
 TEST_F(LowLevelE2E_Topic, UpdateTopicOnNonExistentStreamThrows) {
@@ -1243,7 +1268,8 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicPreservesTopicMetadata) {
     const auto topic_before_messages = stream_before_purge.topics.front();
 
     rust::Vec<iggy::ffi::IggyMessageToSend> messages;
-    
messages.push_back(iggy::ffi::make_message(to_payload("preserve-topic-metadata")));
+    messages.push_back(
+        iggy::ffi::make_message(to_payload("preserve-topic-metadata"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(stream_before_purge.id),
                                           
make_numeric_identifier(topic_before_messages.id), "partition_id",
                                           partition_id_bytes(1), 
std::move(messages)));
@@ -1307,7 +1333,8 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicRemovesOnlyTargetTopicMessages) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> first_topic_messages;
     for (std::uint32_t i = 0; i < 3; ++i) {
-        
first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-first-"
 + std::to_string(i))));
+        
first_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-first-"
 + std::to_string(i)),
+                                                               
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                           
make_numeric_identifier(first_topic_id), "partition_id",
@@ -1315,7 +1342,8 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicRemovesOnlyTargetTopicMessages) {
 
     rust::Vec<iggy::ffi::IggyMessageToSend> second_topic_messages;
     for (std::uint32_t i = 0; i < 2; ++i) {
-        
second_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-second-"
 + std::to_string(i))));
+        
second_topic_messages.push_back(iggy::ffi::make_message(to_payload("purge-topic-second-"
 + std::to_string(i)),
+                                                                
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                           
make_numeric_identifier(second_topic_id), "partition_id",
@@ -1373,7 +1401,8 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicAcrossMultiplePartitionsClearsAllPartitions)
         rust::Vec<iggy::ffi::IggyMessageToSend> messages;
         for (std::uint32_t i = 0; i < 2; ++i) {
             messages.push_back(iggy::ffi::make_message(
-                to_payload("purge-topic-partition-" + 
std::to_string(partition_id) + "-" + std::to_string(i))));
+                to_payload("purge-topic-partition-" + 
std::to_string(partition_id) + "-" + std::to_string(i)),
+                rust::Vec<iggy::ffi::HeaderEntry>()));
         }
         
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                               
make_numeric_identifier(topic_id), "partition_id",
@@ -1413,15 +1442,18 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicThenSendMessagesAgainSucceeds) {
     const std::uint32_t topic_id = created_stream.topics.front().id;
 
     rust::Vec<iggy::ffi::IggyMessageToSend> first_batch;
-    
first_batch.push_back(iggy::ffi::make_message(to_payload("before-topic-purge")));
+    first_batch.push_back(
+        iggy::ffi::make_message(to_payload("before-topic-purge"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
 make_numeric_identifier(topic_id),
                                           "partition_id", 
partition_id_bytes(0), std::move(first_batch)));
 
     ASSERT_NO_THROW(client->purge_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)));
 
     rust::Vec<iggy::ffi::IggyMessageToSend> second_batch;
-    
second_batch.push_back(iggy::ffi::make_message(to_payload("after-topic-purge-0")));
-    
second_batch.push_back(iggy::ffi::make_message(to_payload("after-topic-purge-1")));
+    second_batch.push_back(
+        iggy::ffi::make_message(to_payload("after-topic-purge-0"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
+    second_batch.push_back(
+        iggy::ffi::make_message(to_payload("after-topic-purge-1"), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
 make_numeric_identifier(topic_id),
                                           "partition_id", 
partition_id_bytes(0), std::move(second_batch)));
 
@@ -1470,8 +1502,8 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicTwiceKeepsTargetTopicEmptyAndOtherTopicsUnto
 
     rust::Vec<iggy::ffi::IggyMessageToSend> first_topic_messages;
     for (std::uint32_t i = 0; i < 3; ++i) {
-        first_topic_messages.push_back(
-            iggy::ffi::make_message(to_payload("purge-topic-twice-first-" + 
std::to_string(i))));
+        first_topic_messages.push_back(iggy::ffi::make_message(
+            to_payload("purge-topic-twice-first-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                           
make_numeric_identifier(first_topic_id), "partition_id",
@@ -1479,8 +1511,8 @@ TEST_F(LowLevelE2E_Topic, 
PurgeTopicTwiceKeepsTargetTopicEmptyAndOtherTopicsUnto
 
     rust::Vec<iggy::ffi::IggyMessageToSend> second_topic_messages;
     for (std::uint32_t i = 0; i < 2; ++i) {
-        second_topic_messages.push_back(
-            iggy::ffi::make_message(to_payload("purge-topic-twice-second-" + 
std::to_string(i))));
+        second_topic_messages.push_back(iggy::ffi::make_message(
+            to_payload("purge-topic-twice-second-" + std::to_string(i)), 
rust::Vec<iggy::ffi::HeaderEntry>()));
     }
     
ASSERT_NO_THROW(client->send_messages(make_numeric_identifier(created_stream.id),
                                           
make_numeric_identifier(second_topic_id), "partition_id",
@@ -1542,4 +1574,9 @@ TEST_F(LowLevelE2E_Topic, PurgeTopicBeforeLoginThrows) {
     ASSERT_THROW(
         
unauthenticated_client->purge_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)),
         std::exception);
+    ASSERT_NO_THROW(unauthenticated_client->login_user("iggy", "iggy"));
+    ASSERT_NO_THROW(unauthenticated_client->disconnect());
+    ASSERT_THROW(
+        
unauthenticated_client->purge_topic(make_string_identifier(stream_name), 
make_string_identifier(topic_name)),
+        std::exception);
 }


Reply via email to