bartoszkobylinski opened a new issue, #3735:
URL: https://github.com/apache/iggy/issues/3735

   
   ### Bug description
   
   When a `send_messages` call is rejected by the server, the error that 
reaches the client
   carries no information about the request. Every field of `PartitionNotFound` 
arrives zeroed,
   and two genuinely different requests produce byte-identical errors:
   
   ```
   PartitionNotFound(0, Identifier { kind: Numeric, length: 4, value: [0, 0, 0, 
0] },
                        Identifier { kind: Numeric, length: 4, value: [0, 0, 0, 
0] })
   ```
   
   The variant is `PartitionNotFound(partition_id, topic_id, stream_id)` — per 
the format
   string in `core/common/src/error/iggy_error.rs`: *"Partition with ID: {0} 
for topic with ID:
   {1} for stream with ID: {2} was not found."* So the message asserts 
partition 0 of topic 0,
   stream 0, and **none of those three values was requested**. The actual 
request named an
   existing stream and topic (both had just resolved successfully via 
`get_stream` /
   `get_topic`) and an out-of-range partition id.
   
   **Expected:** the error names the partition that was rejected and the 
stream/topic the
   request addressed — or, failing that, says nothing about them rather than 
reporting values
   that were never sent.
   
   This is not specific to `PartitionNotFound`. The mechanism below applies to 
**every
   field-carrying `IggyError` variant** over the binary protocol — 
`StreamIdNotFound`,
   `TopicIdNotFound`, `InvalidOffset`, `ClientNotFound` and the rest all arrive 
with zeroed or
   empty payloads.
   
   ### Root cause
   
   The server builds the error correctly; the wire format discards the payload 
and the client
   manufactures a replacement out of `Default`. Line references are given at tag
   `server-0.8.0`, with the `master` path where it moved:
   
   1. **The server builds the error with the real ids.**
      `core/server/src/shard/communication.rs:95` —
      `IggyError::PartitionNotFound(ns.partition_id(), topic_id, stream_id)`.
   
   2. **The server serializes the discriminant and an empty body.**
      `core/common/src/sender/mod.rs:200-208` (on `master`:
      `core/server/src/sender/mod.rs:199-207`):
   
      ```rust
      pub(crate) async fn send_error_response<T>(stream: &mut T, error: 
IggyError) -> Result<(), IggyError> {
          send_response(stream, &error.as_code().to_le_bytes(), &[]).await
      }
      ```
   
      `error` is consumed for its 4-byte code only. The payload is dropped here 
and never
      reaches the wire.
   
   3. **The client returns before reading a body.**
      `core/sdk/src/tcp/tcp_client.rs:214-236` returns `Err` as soon as `status 
!= 0`. QUIC
      behaves identically: `core/sdk/src/quic/quic_client.rs:240-248`.
   
   4. **The client rebuilds the variant from the bare code.**
      `IggyError::from_code(status)` → 
`core/common/src/error/iggy_error.rs:521-523` →
      `IggyError::from_repr(code)`. strum's `FromRepr` documents: *"For 
variants with
      additional data, the returned variant will use the `Default` trait to 
fill the data."*
   
   5. **`Identifier::default()` is exactly the observed payload.**
      `core/common/src/types/identifier/mod.rs:60-68` →
      `{ kind: Numeric, length: 4, value: [0, 0, 0, 0] }`.
   
   The byte-identical string-vs-numeric errors in the log below are the 
empirical fingerprint
   of step 4: two different requests cannot produce an identical error unless 
the payload was
   never carried.
   
   **The HTTP transport is unaffected**, which is the clearest evidence that 
the information
   exists and is merely not transported: `core/server/src/http/error.rs:86-87` 
serializes
   `reason: error.to_string()` — the fully formatted message, real ids included 
— plus a
   `field` hint (`"partition_id"` for this variant, line 91). So the same 
server-side error is
   informative over HTTP and misleading over TCP/QUIC.
   
   ### Suggested direction
   
   Either would resolve it; both touch the wire protocol, so this is filed as 
an issue rather
   than a PR, per `CONTRIBUTING.md`:
   
   - **Carry the payload.** The error response body is already empty; put the 
formatted message
     (or the encoded fields) in it, and have the client attach it when `status 
!= 0`. This is
     what the HTTP path already returns.
   - **If the wire format must not change, stop fabricating.** Have the 
client-side
     reconstruction return something carrying only what was actually received — 
e.g. a
     `ServerError { code: u32, name: &'static str }` — instead of a fielded 
variant filled with
     `Default`. `from_code_as_string` already produces the name, so the 
material is there.
   
   An error saying "partition not found (3007)" is strictly better than one 
naming a partition,
   topic and stream nobody asked about. The fabricated zeros are the harmful 
part; an admitted
   unknown would have cost us nothing.
   
   ### Affected area / component
   
   Wire protocol / API · Iggy server · Rust SDK · Python SDK
   
   ### Deployment
   
   Pre-built binary taken from the `apache/iggy:0.8.0` DockerHub image, run 
under systemd (not
   in a container).
   
   ### Versions
   
   Server `0.8.0` (DockerHub `apache/iggy:0.8.0`) · Python SDK `apache-iggy` 
0.8.0 (PyPI wheel,
   cp312, manylinux). Source references above read at tag `server-0.8.0` and at 
`master`
   (`6eb5805`, 2026-07-22).
   
   **Reproduced on 0.8.0. Not run against `master` or an edge build** — the 
statement that all
   five steps are unchanged on `master` comes from reading the source at that 
commit, not from
   executing it. Happy to re-run the script against `apache/iggy:edge` if that 
would help.
   
   ### Hardware / environment
   
   Small self-hosted x86_64 Linux VPS, single server instance, TCP transport on 
loopback
   `127.0.0.1:8092`, Python 3.12.13.
   
   ### Sample code
   
   ```python
   import asyncio
   from apache_iggy import IggyClient, SendMessage
   
   STREAM, TOPIC = "probe_stream", "p3"
   
   async def main():
       c = IggyClient("127.0.0.1:8092")
       await c.connect()
       await c.login_user("iggy", "<password>")
   
       if await c.get_stream(STREAM) is None:
           await c.create_stream(STREAM)
       if await c.get_topic(STREAM, TOPIC) is None:
           await c.create_topic(STREAM, TOPIC, 3)   # 3 partitions
   
       t = await c.get_topic(STREAM, TOPIC)
       print("topic id:", t.id, "partitions:", t.partitions_count)
   
       # Out of range: the topic has partitions 0..2
       try:
           await c.send_messages(STREAM, TOPIC, 3, [SendMessage(b"x")])
       except Exception as e:
           print("string ids ->", e)
   
       # The same call with numeric identifiers
       s = await c.get_stream(STREAM)
       try:
           await c.send_messages(s.id, t.id, 3, [SendMessage(b"x")])
       except Exception as e:
           print("numeric ids ->", e)
   
   asyncio.run(main())
   ```
   
   ### Logs
   
   Client-side output. The two errors are byte-identical, and neither mentions 
partition `3`:
   
   ```
   topic id: 1 partitions: 3
   string ids  -> PartitionNotFound(0, Identifier { kind: Numeric, length: 4, 
value: [0, 0, 0, 0] }, Identifier { kind: Numeric, length: 4, value: [0, 0, 0, 
0] })
   numeric ids -> PartitionNotFound(0, Identifier { kind: Numeric, length: 4, 
value: [0, 0, 0, 0] }, Identifier { kind: Numeric, length: 4, value: [0, 0, 0, 
0] })
   ```
   
   The corresponding server-side line was not captured. It should contain the 
real ids — the
   dispatch failure path formats the same error value
   (`core/server/src/tcp/connection_handler.rs:134-135`), which is the 
information that never
   reaches the client.
   
   ### Iggy server config
   
   Not relevant to this path and therefore not captured: `send_error_response` 
drops the
   payload unconditionally, with no configuration branch. Happy to supply the 
resolved config
   if a maintainer wants it ruled out.
   
   ### Reproduction
   
   1. Start `iggy-server` 0.8.0 with the TCP transport on `127.0.0.1:8092`.
   2. Run the script above with `apache-iggy` 0.8.0. It creates a stream and a 
topic with
      `partitions_count=3`.
   3. It then sends to partition id `3` — one past the end — twice: once 
addressing the stream
      and topic by string name, once by numeric id.
   4. Observe that both raise the same error, that all three fields are zero, 
and that neither
      mentions the partition id that was actually rejected.
   
   ### Why this matters in practice
   
   The zeroed stream/topic identifiers point at the wrong cause. In our case
   `get_stream("...")` and `get_topic("...")` had both just succeeded with 
string identifiers,
   so an error claiming stream 0 / topic 0 not found read as *"the SDK is 
mangling my
   identifiers on this code path"* — and sent the investigation after 
identifier handling for
   most of a day. The actual cause was a partition id one higher than the topic 
had, which the
   error never mentions.
   
   Partition indexing itself is fine and correctly validated: a 1-partition 
topic accepts only
   `0`, a 3-partition topic accepts `0,1,2` and rejects `3` and `4`. The only 
problem is what
   the rejection *says*.
   
   ### Adjacent, and much cheaper to fix
   
   The Python binding types this argument as a bare `int` named `partitioning`
   (`foreign/python/src/client.rs:258,275` — `partitioning: u32` →
   `Partitioning::partition_id(partitioning)`), and the generated stub 
documents nothing about
   it. A reader cannot tell from the signature that it is a 0-based partition 
id rather than,
   say, a `PartitioningKind` discriminator — we guessed the latter first, 
precisely because the
   error payload gave nothing to correct the guess. A one-line docstring on the 
stub would have
   prevented the whole detour, independently of the payload fix. Mentioning it 
here as context
   rather than as a second request; happy to split it into its own issue if 
preferred.
   


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

To unsubscribe, e-mail: [email protected]

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

Reply via email to