Skip to content

Refactor: preserve EofError in io::Error via type-safe downcast#454

Merged
leynos merged 4 commits intomainfrom
refactor-eof-error-downcasting-us6wgc
Feb 16, 2026
Merged

Refactor: preserve EofError in io::Error via type-safe downcast#454
leynos merged 4 commits intomainfrom
refactor-eof-error-downcasting-us6wgc

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Feb 5, 2026

Summary

  • Replaces string matching-based EOF classification with type-safe downcasting of EofError inside io::Error
  • Preserves precise EofError variants for downstream handling and testing

Changes

Core Functionality

  • Remove the special-case mapping of CodecError::Eof(EofError::CleanClose) to a generic UnexpectedEof with a string message in From for io::Error
  • io::Error now preserves the inner EofError as the source, enabling robust downcasting by callers
  • Update EOF classification logic to downcast the inner error when possible; fall back to buffer-length based inference when the inner error is not present

Tests

  • Add test: io_error_from_clean_close_preserves_eof_error in src/codec/error_tests.rs to verify that an EofError::CleanClose is preserved inside the io::Error source
  • Update tests/fixtures/codec_error/decoder_ops.rs: replace string-based EOF classification with downcasting to EofError and add a fallback inference path based on current buffer state

Notes

  • The new approach uses type-safe downcasting of the error source to recover the precise EofError variant (e.g., CleanClose). If the source is absent, the code infers EOF state from the current buffer length (mid-header vs mid-frame).
  • This change improves robustness against upstream changes to error message formatting.

Test plan

  • cargo test
  • Run error tests to ensure inner EofError variants are preserved in io::Error sources
  • Run decoder EOF classification tests to verify downcasting-based behavior (and fallback inference)

◳ Generated by DevBoxer


ℹ️ Tag @devboxerhub to ask questions and address PR feedback

📎 Task: https://www.devboxer.com/task/b9b4b861-a655-4fd1-9ebf-b15f6a002be4

📝 Closes #434

Summary by Sourcery

Preserve precise EOF error variants when converting CodecError into io::Error and update EOF classification in tests to use type-safe downcasting instead of string matching.

Enhancements:

  • Remove special-casing of CodecError::Eof(EofError::CleanClose) so io::Error consistently wraps EofError as the inner source for UnexpectedEof.
  • Refine test-only EOF classification helper to recover EofError via downcasting from io::Error and fall back to buffer-based inference when no inner error is present.

Tests:

  • Add a regression test ensuring CodecError::Eof(EofError::CleanClose) is preserved inside the io::Error source.
  • Adjust decoder EOF classification fixture tests to exercise the new downcasting-based EOF detection and buffer-length fallback.

…classification

- Remove special case mapping of `EofError::CleanClose` to a generic IO error, preserving the original EOF error.
- Add test to ensure `EofError::CleanClose` is preserved when converting to `io::Error`.
- Update EOF error classification in decoder to downcast the inner error instead of relying on fragile string matching.
- Improve robustness in EOF error inference logic by inspecting inner errors and buffer content.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Feb 5, 2026

Reviewer's Guide

Refactors EOF handling so that CodecError::Eof is wrapped in io::Error while preserving the concrete EofError as the inner source, and updates decoder test utilities and tests to classify EOF via type-safe downcasting instead of brittle string inspection.

Sequence diagram for EOF error propagation and classification

sequenceDiagram
    actor Client
    participant Codec
    participant FromCodecErrorForIoError
    participant IoLayer
    participant DecoderClassifier

    Client->>Codec: read_frame()
    Codec-->>Client: error CodecError::Eof(EofError::CleanClose)

    Client->>FromCodecErrorForIoError: from(CodecError::Eof(CleanClose))
    FromCodecErrorForIoError->>IoLayer: create io_Error(kind=UnexpectedEof, source=EofError::CleanClose)
    IoLayer-->>Client: io_Error

    Client->>DecoderClassifier: classify_eof(io_Error, buffer_state)
    DecoderClassifier->>IoLayer: source()
    IoLayer-->>DecoderClassifier: EofError::CleanClose
    DecoderClassifier-->>Client: EofClassification(clean_close)
Loading

Class diagram for CodecError to IoError EOF mapping refactor

classDiagram
    class CodecError {
        <<enum>>
        Io(io_Error)
        Framing(FramingError)
        Protocol(ProtocolError)
        Eof(EofError)
    }

    class EofError {
        <<enum>>
        CleanClose
        MidHeader
        MidFrame
        Other
    }

    class io_Error {
        +kind: io_ErrorKind
        +source: Error
        +new(kind: io_ErrorKind, source: Error)
    }

    class io_ErrorKind {
        <<enum>>
        UnexpectedEof
        InvalidData
        Other
    }

    class FromCodecErrorForIoError {
        +from(error: CodecError) io_Error
    }

    CodecError --> EofError : contains
    CodecError --> io_Error : converted_via_From
    FromCodecErrorForIoError --> CodecError : input
    FromCodecErrorForIoError --> io_Error : output

    io_Error ..> EofError : source_preserves_EofError
    io_Error --> io_ErrorKind : has_kind
Loading

File-Level Changes

Change Details Files
Preserve EofError variants inside io::Error instead of mapping CleanClose to a generic UnexpectedEof message.
  • Remove the special-case conversion of CodecError::Eof(EofError::CleanClose) into an UnexpectedEof io::Error with a string message.
  • Ensure all CodecError::Eof variants are wrapped as UnexpectedEof io::Error values carrying the EofError as their inner source.
src/codec/error.rs
Add a regression test verifying EofError::CleanClose survives the CodecError -> io::Error conversion.
  • Introduce a new test that converts CodecError::Eof(EofError::CleanClose) into io::Error and asserts the inner source downcasts back to EofError::CleanClose.
src/codec/error_tests.rs
Replace string-based EOF classification in decoder fixtures with type-safe downcasting and a buffer-based fallback.
  • Change EOF classification logic to downcast the inner error from io::Error to EofError when available.
  • Introduce a helper to infer EOF state from the current buffer when no inner EofError is present, distinguishing mid-header from mid-frame based on buffer length and header size.
  • Update documentation comments to describe the new downcasting-based classification and fallback behavior.
tests/fixtures/codec_error/decoder_ops.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#434 Replace brittle string-based EOF error classification in the codec-error world tests with type-safe downcasting of the inner EofError from io::Error.
#434 Ensure CodecError::Eof is converted into an io::Error that preserves the inner EofError as the source, enabling type-safe downcasting instead of string matching.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 5, 2026

No actionable comments were generated in the recent review. 🎉


Summary by CodeRabbit

  • Bug Fixes

    • Unified EOF handling so clean closes now use the same EOF formatting, preserving original EOF details and improving message consistency.
    • More robust EOF classification that prefers precise EOF variants and falls back to buffer-based inference when necessary.
  • Tests

    • Added tests covering EOF conversion and classification for clean-close, mid-frame and mid-header scenarios.

Walkthrough

Remove special-case handling for EofError::CleanClose in From<CodecError>io::Error. Replace brittle string-based EOF classification with type-safe downcasting via io::Error::get_ref(), add a buffer-inspection fallback, and add unit tests validating preservation and classification of EofError variants.

Changes

Cohort / File(s) Summary
Error conversion
src/codec/error.rs
Drop explicit mapping of EofError::CleanClose to a bespoke io::Error message; allow all Eof variants to use the unified Eof formatting when converted to io::Error.
Error tests
src/codec/error_tests.rs
Add parameterised test io_error_from_eof_variants_preserves_eof_error to assert CodecError::Eof(...) converts to io::Error with UnexpectedEof and preserves the original EofError accessible via get_ref/downcast.
EOF classification & fixtures
tests/fixtures/codec_error/decoder_ops.rs
Replace fragile string-based EOF classification with io::Error downcasting to EofError; add find_eof_error and infer_eof_from_buffer helpers; set self.detected_eof from inner error or buffer inference; add tests for inner-error paths, non-UnexpectedEof ignoring, and buffer-based inference for MidHeader/MidFrame.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

Cast brittle strings into the breeze,
Downcast types with graceful ease,
EOFs now named with clearer sight,
Buffers whisper left or right,
Tests applaud the tidy frame ✨

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (116 files):

⚔️ Cargo.lock (content)
⚔️ Cargo.toml (content)
⚔️ docs/adr-004-pluggable-protocol-codecs.md (content)
⚔️ docs/generic-message-fragmentation-and-re-assembly-design.md (content)
⚔️ docs/hardening-wireframe-a-guide-to-production-resilience.md (content)
⚔️ docs/multi-packet-and-streaming-responses-design.md (content)
⚔️ docs/roadmap.md (content)
⚔️ docs/rstest-bdd-users-guide.md (content)
⚔️ docs/rust-testing-with-rstest-fixtures.md (content)
⚔️ docs/the-road-to-wireframe-1-0-feature-set-philosophy-and-capability-maturity.md (content)
⚔️ docs/users-guide.md (content)
⚔️ docs/v0-1-0-to-v0-2-0-migration-guide.md (content)
⚔️ docs/wireframe-client-design.md (content)
⚔️ docs/wireframe-testing-crate.md (content)
⚔️ src/app/builder_defaults.rs (content)
⚔️ src/app/fragmentation_state.rs (content)
⚔️ src/app/mod.rs (content)
⚔️ src/client/error.rs (content)
⚔️ src/client/messaging.rs (content)
⚔️ src/client/mod.rs (content)
⚔️ src/client/runtime.rs (content)
⚔️ src/client/tests/error_handling.rs (content)
⚔️ src/client/tests/messaging.rs (content)
⚔️ src/codec/error.rs (content)
⚔️ src/codec/error_tests.rs (content)
⚔️ src/fragment/error.rs (content)
⚔️ src/fragment/mod.rs (content)
⚔️ src/fragment/reassembler.rs (content)
⚔️ src/fragment/series.rs (content)
⚔️ src/fragment/tests.rs (content)
⚔️ src/hooks.rs (content)
⚔️ src/lib.rs (content)
⚔️ src/message.rs (content)
⚔️ src/response.rs (content)
⚔️ src/server/config/binding.rs (content)
⚔️ src/server/config/mod.rs (content)
⚔️ src/server/config/preamble.rs (content)
⚔️ src/server/connection.rs (content)
⚔️ src/server/mod.rs (content)
⚔️ src/server/runtime.rs (content)
⚔️ src/test_helpers/frame_codec.rs (content)
⚔️ tests/app_data.rs (content)
⚔️ tests/async_stream.rs (content)
⚔️ tests/bdd/mod.rs (content)
⚔️ tests/client_lifecycle.rs (content)
⚔️ tests/client_preamble.rs (content)
⚔️ tests/client_runtime.rs (content)
⚔️ tests/common/fragment_helpers.rs (content)
⚔️ tests/connection.rs (content)
⚔️ tests/connection_actor_errors.rs (content)
⚔️ tests/connection_actor_fairness.rs (content)
⚔️ tests/connection_actor_shutdown.rs (content)
⚔️ tests/connection_fragmentation.rs (content)
⚔️ tests/correlation_id.rs (content)
⚔️ tests/error_display.rs (content)
⚔️ tests/example_codecs.rs (content)
⚔️ tests/extractor.rs (content)
⚔️ tests/features/client_lifecycle.feature (content)
⚔️ tests/features/client_runtime.feature (content)
⚔️ tests/features/fragment.feature (content)
⚔️ tests/fixtures/client_lifecycle.rs (content)
⚔️ tests/fixtures/client_messaging.rs (content)
⚔️ tests/fixtures/client_preamble.rs (content)
⚔️ tests/fixtures/client_runtime.rs (content)
⚔️ tests/fixtures/codec_error/decoder_ops.rs (content)
⚔️ tests/fixtures/codec_error/mod.rs (content)
⚔️ tests/fixtures/codec_stateful.rs (content)
⚔️ tests/fixtures/correlation.rs (content)
⚔️ tests/fixtures/fragment/mod.rs (content)
⚔️ tests/fixtures/message_assembler.rs (content)
⚔️ tests/fixtures/message_assembly.rs (content)
⚔️ tests/fixtures/multi_packet.rs (content)
⚔️ tests/fixtures/panic.rs (content)
⚔️ tests/fixtures/request_parts.rs (content)
⚔️ tests/fixtures/stream_end.rs (content)
⚔️ tests/fragment_transport.rs (content)
⚔️ tests/fragment_transport/eviction.rs (content)
⚔️ tests/fragment_transport/mod.rs (content)
⚔️ tests/fragment_transport/rejection.rs (content)
⚔️ tests/frame_codec.rs (content)
⚔️ tests/lifecycle.rs (content)
⚔️ tests/metadata.rs (content)
⚔️ tests/metrics.rs (content)
⚔️ tests/middleware.rs (content)
⚔️ tests/middleware_order.rs (content)
⚔️ tests/multi_packet.rs (content)
⚔️ tests/multi_packet_streaming.rs (content)
⚔️ tests/packet_parts.rs (content)
⚔️ tests/preamble.rs (content)
⚔️ tests/preamble/basic.rs (content)
⚔️ tests/preamble/callbacks.rs (content)
⚔️ tests/preamble/responses.rs (content)
⚔️ tests/preamble/support.rs (content)
⚔️ tests/preamble/timeouts.rs (content)
⚔️ tests/push.rs (content)
⚔️ tests/push_policies.rs (content)
⚔️ tests/rate_limiter_regression.rs (content)
⚔️ tests/request_parts.rs (content)
⚔️ tests/response.rs (content)
⚔️ tests/routes.rs (content)
⚔️ tests/scenarios/client_runtime_scenarios.rs (content)
⚔️ tests/scenarios/fragment_scenarios.rs (content)
⚔️ tests/server.rs (content)
⚔️ tests/session_registry.rs (content)
⚔️ tests/steps/client_lifecycle_steps.rs (content)
⚔️ tests/steps/client_runtime_steps.rs (content)
⚔️ tests/steps/fragment_steps.rs (content)
⚔️ tests/stream_end.rs (content)
⚔️ tests/streaming_request.rs (content)
⚔️ tests/support.rs (content)
⚔️ tests/wireframe_protocol.rs (content)
⚔️ wireframe_testing/Cargo.lock (content)
⚔️ wireframe_testing/Cargo.toml (content)
⚔️ wireframe_testing/README.md (content)
⚔️ wireframe_testing/src/helpers.rs (content)
⚔️ wireframe_testing/src/lib.rs (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarises the main change: replacing string-based EOF classification with type-safe downcasting of EofError, which is reflected across all modified files.
Description check ✅ Passed The description clearly relates to the changeset, explaining the motivation, core changes, test additions, and notes on the new approach—all of which align with the provided file summaries.
Linked Issues check ✅ Passed The PR fully satisfies issue #434 requirements: removes special-case CleanClose handling, implements downcasting via error source chain traversal, adds fallback buffer-based inference, and includes comprehensive tests for all paths.
Out of Scope Changes check ✅ Passed All changes directly address issue #434: removal of string-based EOF classification in error.rs, addition of downcasting logic in decoder_ops.rs, and regression/table-driven tests in error_tests.rs remain within scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-eof-error-downcasting-us6wgc
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch refactor-eof-error-downcasting-us6wgc
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@leynos leynos marked this pull request as ready for review February 16, 2026 08:54
sourcery-ai[bot]

This comment was marked as resolved.

- Add a test verifying EOF variants preserve UnexpectedEof kind on conversion to io::Error
- Add tests for classify_eof_error method covering inner error detection, buffer fallback, and ignoring unrelated errors
- Add tests for infer_eof_from_buffer covering MidHeader and MidFrame EOF error cases
- Refactor decoder_ops to improve EOF error detection by searching error chain
- Verify io_error_from_clean_close_preserves_eof_error with assert_eq on error kind

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Feb 16, 2026

@coderabbitai have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- In `classify_eof_error`, consider walking the `Error::source()` chain instead of relying solely on `get_ref()` so that EOF downcasting remains robust if additional `io::Error` wrapping is introduced in the future.

## Individual Comments

### Comment 1
<location> `tests/fixtures/codec_error/decoder_ops.rs:109-118` </location>
<code_context>
+    /// `UnexpectedEof`. We downcast to recover the precise variant. If the
+    /// inner error is missing, fall back to inspecting the buffer length to
+    /// infer whether we stopped mid-header or mid-frame.
     fn classify_eof_error(&mut self, e: &std::io::Error) {
         if e.kind() != std::io::ErrorKind::UnexpectedEof {
             return;
         }
-        let msg = e.to_string();
-        self.detected_eof = Some(if msg.contains("header") {
+        let detected = e
+            .get_ref()
+            .and_then(|inner| inner.downcast_ref::<EofError>())
+            .copied()
+            .unwrap_or_else(|| self.infer_eof_from_buffer());
+        self.detected_eof = Some(detected);
+    }
+
</code_context>

<issue_to_address>
**suggestion (testing):** Add tests that exercise both the downcast path and the buffer-based fallback in `classify_eof_error`.

The logic looks good, but it isn’t covered by targeted tests. Please add tests that:

- Use an `io::Error` with `kind() == UnexpectedEof` and an inner `EofError` (e.g., `CleanClose`, `MidHeader`, `MidFrame`) and assert `classify_eof_error` sets `detected_eof` to that exact variant.
- Use an `io::Error` with `kind() == UnexpectedEof` whose inner error is not an `EofError` (or has no inner error) and assert it falls back to `infer_eof_from_buffer`.
- Optionally, cover the `kind() != UnexpectedEof` case to ensure `detected_eof` remains unchanged.

Suggested implementation:

```rust
    /// The decoder wraps `EofError` inside `io::Error` for
    /// `UnexpectedEof`. We downcast to recover the precise variant. If the
    /// inner error is missing, fall back to inspecting the buffer length to
    /// infer whether we stopped mid-header or mid-frame.

    /// Classify the EOF error type from the inner error.
    ///
    /// # Implementation Note
    ///
    #[cfg(test)]
    mod classify_eof_error_tests {
        use super::*;
        use std::io;

        /// Helper to construct a decoder with a specific buffer length so that
        /// `infer_eof_from_buffer` has a deterministic result.
        ///
        /// Adjust the construction of `DecoderOps` here to match the real
        /// struct in this file (fields, constructors, etc.).
        fn make_decoder_with_buf_len(len: usize) -> DecoderOps {
            // This assumes `DecoderOps` has a `buf` field (or similar) and
            // implements `Default`. If not, adjust accordingly.
            let mut decoder = DecoderOps::default();
            decoder.buf.resize(len, 0);
            decoder.detected_eof = None;
            decoder
        }

        #[test]
        fn classify_eof_error_uses_inner_eof_error_when_present() {
            // Exercise the downcast path: inner error is an `EofError`.
            for eof_variant in [
                EofError::CleanClose,
                EofError::MidHeader,
                EofError::MidFrame,
            ] {
                let inner: EofError = eof_variant;
                let io_err = io::Error::new(io::ErrorKind::UnexpectedEof, inner);

                let mut decoder = make_decoder_with_buf_len(0);
                // Sanity: we start with no detected EOF
                assert!(decoder.detected_eof.is_none());

                decoder.classify_eof_error(&io_err);

                assert_eq!(
                    decoder.detected_eof,
                    Some(eof_variant),
                    "Expected classify_eof_error to use inner EofError variant"
                );
            }
        }

        #[test]
        fn classify_eof_error_falls_back_to_infer_eof_from_buffer_when_inner_not_eoferror() {
            // Inner error is not `EofError` (string), so we should hit the
            // buffer-based fallback.
            let io_err =
                io::Error::new(io::ErrorKind::UnexpectedEof, "not an EofError inner type");

            let mut decoder = make_decoder_with_buf_len(8);

            // Capture the expected classification from the buffer-based logic.
            let expected = decoder.infer_eof_from_buffer();

            decoder.classify_eof_error(&io_err);

            assert_eq!(
                decoder.detected_eof,
                Some(expected),
                "Expected classify_eof_error to fall back to infer_eof_from_buffer"
            );
        }

        #[test]
        fn classify_eof_error_ignores_non_unexpected_eof_kinds() {
            let io_err = io::Error::new(io::ErrorKind::Other, "some other kind");

            let mut decoder = make_decoder_with_buf_len(4);
            // Set a sentinel value to ensure it is not touched.
            decoder.detected_eof = Some(EofError::CleanClose);

            decoder.classify_eof_error(&io_err);

            assert_eq!(
                decoder.detected_eof,
                Some(EofError::CleanClose),
                "Non-UnexpectedEof errors should leave detected_eof unchanged"
            );
        }
    }

```

The tests above assume several details about the surrounding code that you may need to align with your actual implementation:

1. **Decoder type and imports**
   - Replace `DecoderOps` with the actual type that defines `classify_eof_error`, `infer_eof_from_buffer`, `detected_eof`, and the buffer.
   - Ensure `EofError` is imported correctly in this file (e.g., `use crate::codec::EofError;` or similar) if it is not already in scope.

2. **Decoder construction**
   - If `DecoderOps::default()` does not exist, either:
     - Implement `Default` for `DecoderOps`, or
     - Replace `make_decoder_with_buf_len` with construction that matches your real struct (e.g., `DecoderOps { buf: BytesMut::with_capacity(len), detected_eof: None, ... }`).

3. **Buffer field name and type**
   - The helper `make_decoder_with_buf_len` assumes a `buf` field that supports `resize(len, 0)`. If your buffer is named differently or uses a different type (e.g., `BytesMut`, `Vec<u8>`), adjust the helper accordingly to set the intended buffer length.

4. **EofError variants**
   - Ensure the variants `CleanClose`, `MidHeader`, and `MidFrame` exist on `EofError`. If naming differs (e.g., `Midframe` or `MidFrameHeader`), update the tests to use the actual variants.

5. **Visibility**
   - The tests are inside the same file/module so they can access private methods like `infer_eof_from_buffer` and fields like `detected_eof` and `buf`. If the visibility layout differs (e.g., this file declares a module and the implementation lives in a submodule), move the tests into the module where `classify_eof_error` is defined so they can access private items.

Once adjusted to your actual types and fields, these tests will directly exercise:
- The **downcast path** (inner `EofError`),
- The **buffer-based fallback** when the inner error is not an `EofError`,
- And the **non-`UnexpectedEof`** case where `detected_eof` must remain unchanged.
</issue_to_address>

### Comment 2
<location> `tests/fixtures/codec_error/decoder_ops.rs:121-99` </location>
<code_context>
+        self.detected_eof = Some(detected);
+    }
+
+    fn infer_eof_from_buffer(&self) -> EofError {
+        if self.buffer.len() < LENGTH_HEADER_SIZE {
             EofError::MidHeader {
</code_context>

<issue_to_address>
**suggestion (testing):** Add focused tests for `infer_eof_from_buffer` to verify both `MidHeader` and `MidFrame` inference.

Since EOF classification now falls back to `infer_eof_from_buffer`, it would benefit from direct tests (unit tests for the helper or via a small `CodecErrorWorld` harness) that:

- When `buffer.len() < LENGTH_HEADER_SIZE`, verify `EofError::MidHeader` with the expected `bytes_received` and `header_size`.
- When `buffer.len() >= LENGTH_HEADER_SIZE`, verify `EofError::MidFrame` with `bytes_received == len - LENGTH_HEADER_SIZE` and the correct `expected` from the encoded length header.

This keeps the non-trivial logic well-covered beyond the current indirect higher-level tests.

Suggested implementation:

```rust
    /// Call `decode_eof` when buffer has incomplete data.
}

#[cfg(test)]
mod infer_eof_from_buffer_tests {
    use super::*;
    use bytes::{BufMut, BytesMut};

    /// Helper to build a decoder with a pre-populated internal buffer.
    ///
    /// This assumes the type that implements `infer_eof_from_buffer` is called
    /// `DecoderOps` and that it implements `Default` and exposes a `buffer`
    /// field. Adjust as needed to match the actual type and construction
    /// patterns in this file.
    fn make_decoder_with_buffer<F>(fill: F) -> DecoderOps
    where
        F: FnOnce(&mut BytesMut),
    {
        let mut buffer = BytesMut::new();
        fill(&mut buffer);

        let mut decoder = DecoderOps::default();
        decoder.buffer = buffer;
        decoder
    }

    #[test]
    fn infer_eof_returns_mid_header_when_buffer_shorter_than_length_header() {
        // buffer.len() < LENGTH_HEADER_SIZE should classify as MidHeader
        let decoder = make_decoder_with_buffer(|buf| {
            // Put fewer bytes than LENGTH_HEADER_SIZE into the buffer.
            // We don't care about the content, only the length.
            let short_len = LENGTH_HEADER_SIZE.saturating_sub(1);
            for _ in 0..short_len {
                buf.put_u8(0xAA);
            }
        });

        let eof = decoder.infer_eof_from_buffer();

        match eof {
            EofError::MidHeader {
                bytes_received,
                header_size,
            } => {
                assert_eq!(
                    bytes_received,
                    decoder.buffer.len(),
                    "MidHeader.bytes_received should equal buffer.len()"
                );
                assert_eq!(
                    header_size, LENGTH_HEADER_SIZE,
                    "MidHeader.header_size should equal LENGTH_HEADER_SIZE"
                );
            }
            other => panic!("expected EofError::MidHeader, got {other:?}"),
        }
    }

    #[test]
    fn infer_eof_returns_mid_frame_when_header_complete_but_body_incomplete() {
        // When buffer contains a full length header but an incomplete frame body,
        // infer_eof_from_buffer should classify as MidFrame and compute the
        // correct bytes_received and expected values.
        let expected_len: u32 = 42;

        let decoder = make_decoder_with_buffer(|buf| {
            // Encode the length header. Adjust endianness if the codec expects
            // little-endian instead of big-endian.
            buf.put_u32(expected_len);
            // Add fewer bytes than `expected_len` to simulate an incomplete frame.
            buf.put_slice(&[0x11, 0x22]);
        });

        let eof = decoder.infer_eof_from_buffer();

        match eof {
            EofError::MidFrame {
                bytes_received,
                expected,
            } => {
                assert_eq!(
                    bytes_received,
                    decoder.buffer.len() - LENGTH_HEADER_SIZE,
                    "MidFrame.bytes_received should equal buffer.len() - LENGTH_HEADER_SIZE"
                );
                assert_eq!(
                    expected,
                    expected_len as usize,
                    "MidFrame.expected should come from the encoded length header"
                );
            }
            other => panic!("expected EofError::MidFrame, got {other:?}"),
        }
    }
}

```

1. Replace `DecoderOps` in the helper and tests with the actual type that defines `infer_eof_from_buffer` if it has a different name (e.g. `CodecErrorWorld`).
2. If the type does not implement `Default` or the `buffer` field is not public, add a suitable constructor or helper within this file (or expose a test-only constructor) that allows creating an instance with a pre-filled buffer, and update `make_decoder_with_buffer` accordingly.
3. Ensure the `bytes` crate (and `BufMut`, `BytesMut`) is in scope for this test module; if `decoder_ops.rs` already imports these at the top-level, you can remove the redundant `use bytes::{BufMut, BytesMut};` inside the test module.
4. Adjust the length-header encoding (`put_u32`) to match your actual wire format (endianness and integer width). If your codec uses little-endian or a different integer type/size for lengths, switch to the appropriate `BufMut` method and type.
5. Once these tests are wired up, you may need to fix the implementation of `infer_eof_from_buffer` itself so that it actually returns `EofError::MidFrame` in the non-header-short path and does not duplicate struct fields in the `MidHeader` arm; these tests will help drive that correction.
</issue_to_address>

### Comment 3
<location> `src/codec/error_tests.rs:117-127` </location>
<code_context>
     assert_eq!(io_err.kind(), expected_kind);
 }

+#[test]
+fn io_error_from_clean_close_preserves_eof_error() {
+    let io_err: io::Error = CodecError::Eof(EofError::CleanClose).into();
+    let inner = io_err
+        .get_ref()
+        .expect("expected inner error for clean close");
+    let eof = inner
+        .downcast_ref::<EofError>()
+        .expect("expected EofError to be preserved");
+    assert_eq!(*eof, EofError::CleanClose);
+}
+
</code_context>

<issue_to_address>
**suggestion (testing):** Extend the new regression test to assert the `io::ErrorKind` and consider covering other `EofError` variants.

The new `io_error_from_clean_close_preserves_eof_error` test does a good job verifying that `EofError::CleanClose` is preserved as the inner error. To strengthen it, you could also assert that `io_err.kind()` is `io::ErrorKind::UnexpectedEof`, and optionally add a small table-driven test over multiple `EofError` variants (e.g., `CleanClose`, `MidHeader`, `MidFrame`) to ensure each is preserved as the inner source when converted via `CodecError::Eof`.

```suggestion
#[test]
fn io_error_from_clean_close_preserves_eof_error() {
    let io_err: io::Error = CodecError::Eof(EofError::CleanClose).into();

    // Ensure the io::Error is classified correctly
    assert_eq!(io_err.kind(), io::ErrorKind::UnexpectedEof);

    // Ensure the inner EofError is preserved
    let inner = io_err
        .get_ref()
        .expect("expected inner error for clean close");
    let eof = inner
        .downcast_ref::<EofError>()
        .expect("expected EofError to be preserved");
    assert_eq!(*eof, EofError::CleanClose);
}

#[test]
fn io_error_from_eof_variants_preserves_inner_eof_error() {
    use crate::codec::EofError::*;

    let variants = [CleanClose, MidHeader, MidFrame];

    for variant in variants {
        let io_err: io::Error = CodecError::Eof(variant).into();

        // All EOF variants should translate to UnexpectedEof
        assert_eq!(
            io_err.kind(),
            io::ErrorKind::UnexpectedEof,
            "unexpected io::ErrorKind for variant {variant:?}"
        );

        let inner = io_err
            .get_ref()
            .unwrap_or_else(|| panic!("expected inner error for variant {variant:?}"));

        let eof = inner
            .downcast_ref::<EofError>()
            .unwrap_or_else(|| panic!("expected EofError to be preserved for variant {variant:?}"));

        assert_eq!(
            *eof, variant,
            "EofError variant was not preserved for {variant:?}"
        );
    }
}
```
</issue_to_address>

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me check the current state of the codebase to see if these review comments have been addressed.


🏁 Script executed:

#!/bin/bash

# Check the decoder_ops.rs file for the classify_eof_error implementation and tests
echo "=== Checking tests/fixtures/codec_error/decoder_ops.rs ==="
cat tests/fixtures/codec_error/decoder_ops.rs

echo -e "\n\n=== Checking src/codec/error_tests.rs ==="
cat src/codec/error_tests.rs

Length of output: 17519


@leynos: Yes, all the review comments have been fully resolved! Here's what I found:

✅ Overall Comment - Resolved

The code now walks the Error::source() chain via the new find_eof_error() helper function (lines 119-129 in decoder_ops.rs):

  • Iterates through the error source chain using a while loop
  • Attempts to downcast each error in the chain to EofError
  • Returns the first match found

✅ Comment 1 - Resolved

Three comprehensive tests for classify_eof_error have been added (lines 233-273 in decoder_ops.rs):

  1. classify_eof_error_uses_inner_eof_error_variant - exercises the downcast path with all three EofError variants
  2. classify_eof_error_falls_back_to_buffer_classification - verifies fallback when inner error is not an EofError
  3. classify_eof_error_ignores_non_unexpected_eof - confirms non-UnexpectedEof errors leave detected_eof unchanged

✅ Comment 2 - Resolved

Two focused tests for infer_eof_from_buffer have been added (lines 275-313 in decoder_ops.rs):

  1. infer_eof_from_buffer_reports_mid_header - verifies MidHeader classification when buffer length < LENGTH_HEADER_SIZE
  2. infer_eof_from_buffer_reports_mid_frame - verifies MidFrame classification with correct byte counts when header is complete but body is incomplete

✅ Comment 3 - Resolved

The test coverage has been extended in error_tests.rs:

  1. io_error_from_clean_close_preserves_eof_error (lines 115-124) now asserts the io::ErrorKind
  2. New table-driven test io_error_from_eof_variants_preserves_eof_error (lines 126-147) covers all three EofError variants (CleanClose, MidFrame, MidHeader)

All suggestions have been implemented with appropriate test coverage. The refactoring is complete and well-tested! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/codec/error_tests.rs`:
- Around line 117-157: Merge the two tests
io_error_from_clean_close_preserves_eof_error and
io_error_from_eof_variants_preserves_eof_error into a single
rstest-parameterised test that iterates over all EofError variants (include
EofError::CleanClose plus the MidFrame and MidHeader variants) and for each
converts CodecError::Eof(variant) into an io::Error and asserts io_err.kind() ==
io::ErrorKind::UnexpectedEof; when extracting the inner error use
io_err.get_ref().expect(...) and inner.downcast_ref::<EofError>().expect(...)
(replace the current unwrap_or_else(|| panic!(...)) calls), and assert_eq!(*eof,
variant); ensure the new test is annotated with #[rstest] and uses a parameter
name like variant to match the pattern used elsewhere.

In `@tests/fixtures/codec_error/decoder_ops.rs`:
- Around line 234-342: The tests module currently uses a hand-rolled helper
make_world_with_buffer and a manual loop in
classify_eof_error_uses_inner_eof_error_variant; refactor to use rstest fixtures
and parameterised cases: replace make_world_with_buffer with a #[fixture] fn
returning CodecErrorWorld (populate world.buffer in the fixture or via helper
called in the fixture), convert classify_eof_error_uses_inner_eof_error_variant
into an #[rstest] test with #[case] for each EofError variant, and inline simple
buffer setup directly in tests (or add another #[fixture] for prefilled buffers)
while keeping calls to world.classify_eof_error and assertions against
world.detected_eof and world.infer_eof_from_buffer unchanged; reference symbols:
make_world_with_buffer, CodecErrorWorld,
classify_eof_error_uses_inner_eof_error_variant, infer_eof_from_buffer, and
EofError.

Comment thread src/codec/error_tests.rs Outdated
Comment thread tests/fixtures/codec_error/decoder_ops.rs
…ts for EOF variants

Refactor multiple manual loops and separate test functions into concise parameterized tests using the rstest crate. This simplifies and clarifies tests around EOF error variant conversions and EOF classification logic in codec error handling.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
Before applying any fix, first verify the finding against the current code and
decide whether a code change is actually needed. If the finding is not valid or
no change is required, do not modify code for that item and briefly explain why
it was skipped.

In `@tests/fixtures/codec_error/decoder_ops.rs`:
- Around line 234-239: Add a module-level doc comment to the test module by
inserting a `//!` comment as the first item inside `mod tests` so it explains
the purpose of the tests in this module (e.g., what behavior `decoder_ops`
fixtures validate); ensure the `//!` comment appears before any `use` statements
or items within `mod tests` and is concise and descriptive to satisfy the
module-level documentation guideline.

Comment thread tests/fixtures/codec_error/decoder_ops.rs
…coder ops tests

Add documentation comment describing tests for decoder ops EOF classification and buffer inference behaviour.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos merged commit 2a76e76 into main Feb 16, 2026
6 checks passed
@leynos leynos deleted the refactor-eof-error-downcasting-us6wgc branch February 16, 2026 21:02
devboxerhub Bot added a commit that referenced this pull request Feb 18, 2026
* refactor(codec,error): Refactor EOF error handling and improve error classification

- Remove special case mapping of `EofError::CleanClose` to a generic IO error, preserving the original EOF error.
- Add test to ensure `EofError::CleanClose` is preserved when converting to `io::Error`.
- Update EOF error classification in decoder to downcast the inner error instead of relying on fragile string matching.
- Improve robustness in EOF error inference logic by inspecting inner errors and buffer content.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>

* test(codec error): add tests for EOF error classification and conversion

- Add a test verifying EOF variants preserve UnexpectedEof kind on conversion to io::Error
- Add tests for classify_eof_error method covering inner error detection, buffer fallback, and ignoring unrelated errors
- Add tests for infer_eof_from_buffer covering MidHeader and MidFrame EOF error cases
- Refactor decoder_ops to improve EOF error detection by searching error chain
- Verify io_error_from_clean_close_preserves_eof_error with assert_eq on error kind

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>

* test(codec_error): replace manual loops with rstest parameterized tests for EOF variants

Refactor multiple manual loops and separate test functions into concise parameterized tests using the rstest crate. This simplifies and clarifies tests around EOF error variant conversions and EOF classification logic in codec error handling.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>

* docs(tests/fixtures/codec_error): add module-level doc comment for decoder ops tests

Add documentation comment describing tests for decoder ops EOF classification and buffer inference behaviour.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>

---------

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replace brittle EOF string matching with type-safe downcasting

1 participant