Skip to content

feat(server,preamble): PreambleHooks, clamp timeout, async failure callback#398

Merged
leynos merged 4 commits intomainfrom
feature/preamble-timeout
Nov 26, 2025
Merged

feat(server,preamble): PreambleHooks, clamp timeout, async failure callback#398
leynos merged 4 commits intomainfrom
feature/preamble-timeout

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Nov 25, 2025

Summary

  • Adds a configurable preamble_timeout with minimum clamp to 1 ms to bound handshake duration and surface timeout errors through the preamble failure path.
  • Introduces an asynchronous preamble failure callback that can write a response before the connection is closed.
  • Refactors server runtime to bundle preamble hooks and timeout into a reusable PreambleHooks, passed through accept_loop and per-connection handling.

Changes

Core Functionality

  • Added preamble_timeout builder option with minimum clamp to 1 ms.
  • Enforced timeout around read_preamble using tokio's timeout in connection spawning flow.
  • Reworked preamble failure handling: replaced PreambleFailureHandler trait with a shared PreambleFailure callback type, which is asynchronous and receives a mutable TcpStream for replying before disconnection.
  • Introduced PreambleHooks to bundle on_success, on_failure, and timeout, threaded through accept_loop and connection handling.
  • Updated on_preamble_decode_failure API to reflect the new signature and usage.

API/Runtime Changes

  • Replaced trait-based failure handler with PreambleFailure type alias (Arc< dyn Fn(&DecodeError, &mut TcpStream) -> BoxFuture<...> + Send + Sync + 'static>).
  • Wireframe server state updated to carry and propagate the new PreambleFailure callback.
  • spawn_connection_task and process_stream updated to work with the new PreambleFailure API and the PreambleHooks container.

Documentation

  • Documented the new preamble_timeout configuration and the asynchronous preamble failure callback in server configuration guides, user guides, and preamble validator docs, including examples and hardening guidance.

Tests

  • Added tests verifying preamble timeout enforcement, timeout-triggered failure handling and connection closure, and the ability for failure callbacks to write responses before disconnection.

📎 Task: https://www.terragonlabs.com/task/dc3fcaf4-3dd0-4e73-8631-40364d875efd

…ilure callback

This update introduces a new `preamble_timeout` configuration for `WireframeServer` to bound the maximum duration allowed for reading a connection preamble. If the preamble read exceeds this timeout, a failure handler (if registered) asynchronously executes, allowing custom protocol errors to be sent before the connection is closed. This prevents resources from being tied up indefinitely by clients that fail to send a valid preamble.

Key changes include:
- Added `preamble_timeout` builder method clamping minimum timeout to 1 ms.
- Modified `spawn_connection_task` to enforce timeout around `read_preamble` with tokio's `timeout`.
- Enhanced failure handler signature to accept mutable `TcpStream` for replying before disconnection.
- Extended documentation with usage examples and detailed guides.
- Added tests verifying timeout enforcement, failure handler invocation, and response writing.

This enhances server resilience and DoS protection by bounding handshake durations.

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

sourcery-ai Bot commented Nov 25, 2025

Reviewer's Guide

Adds a configurable preamble read timeout to WireframeServer, refactors preamble hook wiring to carry this timeout, and upgrades the preamble decode failure callback to an async handler with access to the TcpStream so it can respond before the connection is closed. Tests and documentation are updated to cover the new behavior.

Sequence diagram for preamble read with timeout and async failure handler

sequenceDiagram
    actor Client
    participant WireframeServer
    participant AcceptLoop
    participant ConnectionTask
    participant ProcessStream
    participant ReadPreamble
    participant PreambleFailureHandler
    participant WireframeApp

    Client->>WireframeServer: connect
    WireframeServer->>AcceptLoop: start accept_loop
    AcceptLoop->>AcceptLoop: listener.accept()
    AcceptLoop->>ConnectionTask: spawn_connection_task(stream, factory, hooks.on_success, hooks.on_failure, hooks.timeout)

    ConnectionTask->>ProcessStream: process_stream(stream, peer_addr, factory, on_success, on_failure, preamble_timeout)

    alt preamble_timeout is Some
        ProcessStream->>ProcessStream: timeout(preamble_timeout, read_preamble)
        ProcessStream->>ReadPreamble: read_preamble(stream)
        alt preamble read completes in time
            ReadPreamble-->>ProcessStream: Ok(preamble, leftover)
        else preamble read times out
            ProcessStream-->>ProcessStream: Err(timeout_error)
        end
    else preamble_timeout is None
        ProcessStream->>ReadPreamble: read_preamble(stream)
        ReadPreamble-->>ProcessStream: Result(preamble or error)
    end

    alt preamble_result is Ok
        ProcessStream->>ProcessStream: invoke on_success if Some
        opt on_success is Some
            ProcessStream->>WireframeApp: on_preamble_success(preamble, stream)
            WireframeApp-->>ProcessStream: Result
        end
        ProcessStream->>WireframeApp: hand off stream and preamble
    else preamble_result is Err (decode failure or timeout)
        alt on_failure is Some
            ProcessStream->>PreambleFailureHandler: on_preamble_failure(error, stream)
            PreambleFailureHandler-->>ProcessStream: io::Result
            ProcessStream->>Client: optional protocol error reply via stream
            ProcessStream->>Client: close connection
        else on_failure is None
            ProcessStream->>ProcessStream: log error with peer_addr
            ProcessStream->>Client: close connection
        end
    end
Loading

Updated class diagram for preamble hooks, handlers, and WireframeServer

classDiagram
    class WireframeServer~F,T,S~ {
        +F factory
        +usize workers
        +Option~PreambleHandler~T~~ on_preamble_success
        +Option~PreambleFailure~T~~ on_preamble_failure
        +Option~oneshot_Sender~unit~~ ready_tx
        +BackoffConfig backoff_config
        +Option~Duration~ preamble_timeout
        +S state
        +preamble_timeout(timeout Duration) WireframeServer~F,T,S~
        +on_preamble_decode_success(handler PreambleHandler~T~) WireframeServer~F,T,S~
        +on_preamble_decode_failure(handler PreambleFailure~T~) WireframeServer~F,T,S~
    }

    class PreambleHooks~T~ {
        +Option~PreambleHandler~T~~ on_success
        +Option~PreambleFailure~T~~ on_failure
        +Option~Duration~ timeout
        +new() PreambleHooks~T~
        +clone() PreambleHooks~T~
        +default() PreambleHooks~T~
    }

    class PreambleSuccessHandler~T~ {
        <<interface>>
        +call(preamble T, stream TcpStream) Future
    }

    class PreambleFailureHandler~T~ {
        <<interface>>
        +call(error DecodeError, stream TcpStream) BoxFuture~io_Result_unit~~
    }

    class PreambleHandler~T~ {
        <<typealias>>
        +Arc~PreambleSuccessHandler~T~~
    }

    class PreambleFailure~T~ {
        <<typealias>>
        +Arc~PreambleFailureHandler~T~~
    }

    class BackoffConfig {
        +Duration initial_delay
        +Duration max_delay
        +f64 multiplier
    }

    class AcceptLoop {
        +accept_loop(listener AcceptListener, factory F, preamble PreambleHooks~T~, shutdown CancellationToken, tracker TaskTracker, backoff_config BackoffConfig) Future
    }

    class ConnectionTask {
        +spawn_connection_task(stream TcpStream, factory F, on_success Option~PreambleHandler~T~~, on_failure Option~PreambleFailure~T~~, preamble_timeout Option~Duration~, tracker TaskTracker)
        +process_stream(stream TcpStream, peer_addr Option~SocketAddr~, factory F, on_success Option~PreambleHandler~T~~, on_failure Option~PreambleFailure~T~~, preamble_timeout Option~Duration~) Future
        +timeout_error() DecodeError
    }

    WireframeServer~F,T,S~ --> PreambleHandler~T~ : uses success
    WireframeServer~F,T,S~ --> PreambleFailure~T~ : uses failure
    WireframeServer~F,T,S~ --> BackoffConfig : has
    WireframeServer~F,T,S~ --> PreambleHooks~T~ : configures

    PreambleHandler~T~ ..> PreambleSuccessHandler~T~ : wraps
    PreambleFailure~T~ ..> PreambleFailureHandler~T~ : wraps

    AcceptLoop ..> PreambleHooks~T~ : takes
    AcceptLoop ..> ConnectionTask : spawns

    ConnectionTask ..> PreambleHandler~T~ : optional
    ConnectionTask ..> PreambleFailure~T~ : optional
    ConnectionTask ..> PreambleFailureHandler~T~ : invokes
    ConnectionTask ..> PreambleSuccessHandler~T~ : invokes
    ConnectionTask ..> BackoffConfig : uses

    PreambleHooks~T~ --> PreambleHandler~T~ : on_success
    PreambleHooks~T~ --> PreambleFailure~T~ : on_failure
Loading

File-Level Changes

Change Details Files
Introduce configurable preamble read timeout and plumb it through server configuration and runtime wiring.
  • Add preamble_timeout field to WireframeServer state and initialize it to None by default
  • Implement preamble_timeout(Duration) builder that clamps sub-millisecond values to 1 ms and stores the normalized timeout
  • Include preamble_timeout when transitioning between unbound and bound server config states so it is preserved across binding
  • Create PreambleHooks struct to bundle preamble success/failure handlers with the timeout and pass it through accept_loop in place of separate handler params
  • Update accept_loop and its tests to use PreambleHooks, cloning hooks per accepted connection and forwarding timeout into spawn_connection_task
src/server/mod.rs
src/server/config/mod.rs
src/server/config/preamble.rs
src/server/config/binding.rs
src/server/runtime.rs
Enforce timeout around preamble reading and route timeouts through the failure handler, which is now asynchronous and stream-aware.
  • Extend spawn_connection_task and process_stream to accept an optional preamble_timeout and pass it down to preamble reading
  • Wrap read_preamble in tokio::time::timeout when a preamble_timeout is set, converting timeout expiry into a synthetic DecodeError::Io with ErrorKind::TimedOut
  • Change the preamble failure handler type to PreambleFailure, an async trait receiving &DecodeError and &mut TcpStream and returning BoxFuture<io::Result<()>>
  • Invoke the async failure handler with the decode error and stream, logging and closing on handler errors; keep existing logging for missing handlers
  • Update internal tests that call spawn_connection_task or preamble handlers directly to provide the new timeout argument and await async handlers
src/server/connection.rs
src/server/mod.rs
src/server/runtime.rs
src/server/config/tests.rs
tests/preamble.rs
Update public API, tests, and docs to reflect async preamble failure callbacks and the new timeout behavior.
  • Adjust on_preamble_decode_failure builder to accept PreambleFailureHandler and update its example to show writing a response asynchronously
  • Update multiple tests to construct async failure handlers that box futures and optionally write to the stream or signal channels
  • Add tests verifying that failure callbacks can write responses, that preamble_timeout triggers a timed-out DecodeError and connection close, and that success handlers work without a failure handler
  • Document preamble handling and preamble_timeout usage in server configuration docs, users guide, hardening guide, and preamble validator docs, including how timeouts flow through the failure path and that sub-millisecond values are clamped
src/server/config/preamble.rs
src/server/config/tests.rs
tests/preamble.rs
docs/server/configuration.md
docs/users-guide.md
docs/hardening-wireframe-a-guide-to-production-resilience.md
docs/preamble-validator.md

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 Nov 25, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added optional preamble timeout configuration to cap connection handshake read duration; timeouts are automatically normalised to a minimum of 1 millisecond.
    • Enhanced preamble failure callbacks: now asynchronous and receive mutable stream access, enabling protocol-level error responses before connection closure.
  • Documentation

    • Expanded documentation with preamble timeout handling, asynchronous failure callback patterns, and detailed processing flow diagrams.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Introduce an optional preamble_timeout that bounds read_preamble (normalised to >= 1 ms) and route timeouts through an asynchronous on_preamble_decode_failure handler that receives the DecodeError and a mutable TcpStream so the server can write a protocol response before closing the connection.

Changes

Cohort / File(s) Summary
Documentation
docs/hardening-wireframe-a-guide-to-production-resilience.md, docs/preamble-validator.md, docs/server/configuration.md, docs/users-guide.md
Add sections describing preamble_timeout, its 1 ms normalisation, and the async on_preamble_decode_failure handler that receives (&DecodeError, &mut TcpStream) and may write responses before socket close. Update examples and citations.
Configuration builders
src/server/config/preamble.rs, src/server/config/mod.rs, src/server/config/binding.rs
Add preamble_timeout field propagation; add .preamble_timeout(Duration) builder that clamps to 1 ms; change on_preamble_decode_failure builder to accept an async handler returning BoxFuture and receiving the error plus a mutable TcpStream.
Server API & types
src/server/mod.rs
Replace synchronous PreambleErrorHandler alias with PreambleFailure alias: an Arc<dyn for<'a> Fn(&'a DecodeError, &'a mut TcpStream) -> BoxFuture<'a, io::Result<()>>> + Send + Sync + 'static. Add preamble_timeout: Option<Duration> to WireframeServer.
Runtime wiring
src/server/runtime.rs
Introduce PreambleHooks<T> (on_success, on_failure, timeout). Change accept_loop/run_with_shutdown to propagate a single hooks struct to worker tasks and clone/forward hooks to connection tasks.
Connection handling
src/server/connection.rs
Thread preamble_timeout: Option<Duration> through spawn_connection_taskprocess_stream. Apply timeout to read_preamble when provided; add timeout_error() helper. Invoke async failure handler with (error, &mut stream) on timeout or decode error, log handler failures, then close stream.
Tests
tests/preamble.rs, src/server/config/tests.rs
Update test harness and callbacks to the new async signature for<'a> Fn(&'a DecodeError, &'a mut TcpStream) -> BoxFuture<'a, io::Result<()>>. Add test_preamble_timeout_configuration and tests for async failure behaviours interacting with a real TcpStream.

Sequence Diagram(s)

sequenceDiagram
    participant Accept as Acceptor
    participant Conn as spawn_connection_task
    participant Proc as process_stream
    participant Handler as on_preamble_decode_failure
    participant Stream as TcpStream

    Note over Accept,Conn: hand off TcpStream + PreambleHooks{timeout,...}
    Accept->>Conn: hand off TcpStream + PreambleHooks
    Conn->>Proc: start processing (passes timeout)
    Proc->>Proc: attempt read_preamble (with optional timeout)

    alt read_preamble succeeds
        Proc->>Proc: invoke on_success handler (async)
        Proc->>Stream: proceed to handshake
    else read_preamble times out or decodes to error
        Proc->>Handler: call Handler(error, &mut Stream) [async]
        Handler->>Stream: optionally write error response
        Handler-->>Proc: future resolves (ok / err)
        Proc->>Stream: close connection
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

  • Inspect higher-ranked trait bounds and BoxFuture lifetimes across type aliases and builder APIs.
  • Verify preamble_timeout normalisation (clamp to 1 ms) and correct propagation through config → runtime → connection.
  • Confirm safe mutable TcpStream borrowing across async handler invocation and absence of lifetime/race issues.
  • Validate timeout_error() construction and that handler errors are logged but do not leak resources.
  • Review updated tests that exercise real TcpStream writes in failure handlers.

Possibly related PRs

Suggested reviewers

  • codescene-delta-analysis

Poem

✨ Guard the handshake, bound the wait,
Send a last byte ere you close the gate,
Timeout whispers, handlers choreograph,
Stream in hand, deliver the final paragraph,
Close with grace, then log the aftermath.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title accurately summarises the main changes: introduction of PreambleHooks, timeout clamping, and async failure callback functionality.
Description check ✅ Passed The description comprehensively outlines the core functionality, API changes, documentation updates, and test additions, all directly related to the changeset.
✨ 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 feature/preamble-timeout

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

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • The PreambleHooks<T> struct only contains clonable/optional fields and could derive Clone and Default instead of hand-written impls, which would simplify the code and keep it in sync if fields change.
  • The generic type parameter T on PreambleFailureHandler<T> is never used in the trait bounds or methods, so consider removing it (and using a non-generic trait/alias) to reduce API surface and avoid confusion.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `PreambleHooks<T>` struct only contains clonable/optional fields and could derive `Clone` and `Default` instead of hand-written impls, which would simplify the code and keep it in sync if fields change.
- The generic type parameter `T` on `PreambleFailureHandler<T>` is never used in the trait bounds or methods, so consider removing it (and using a non-generic trait/alias) to reduce API surface and avoid confusion.

## Individual Comments

### Comment 1
<location> `tests/preamble.rs:270` </location>
<code_context>
+
+#[rstest]
+#[tokio::test]
+async fn preamble_timeout_invokes_failure_handler_and_closes_connection(
+    factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static,
+) {
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test to cover the case where a preamble timeout is configured but a valid preamble arrives in time

Currently we only cover the timeout/failure path. Please add a test where `preamble_timeout` is set (e.g., 50–100 ms) and a valid preamble arrives well within that window, asserting that the success handler runs and the connection remains open. This will help catch regressions where the timeout might incorrectly fire on timely preambles.

Suggested implementation:

```rust
        timeout(Duration::from_millis(200), failure_rx)
            .await
            .expect("timeout waiting for failure callback")
            .expect("failure callback send");
    })
    .await;
}

#[rstest]
#[tokio::test]
async fn preamble_timeout_allows_timely_preamble_and_keeps_connection_open(
    factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static,
) {
    // Arrange
    let (success_holder, success_rx) = channel_holder();
    let (failure_holder, failure_rx) = channel_holder();

    let server = WireframeServer::new(factory)
        .with_preamble::<HotlinePreamble>()
        // Configure a relatively short timeout; the client will send a valid preamble well
        // within this window.
        .with_preamble_timeout(Duration::from_millis(100))
        .on_preamble(move |preamble: HotlinePreamble, mut stream| {
            let success_holder = success_holder.clone();
            Box::pin(async move {
                // In a real test you might assert on fields from `preamble` here.
                // For now we just signal that the success handler ran and keep the
                // connection open by writing back a simple response.
                if let Some(tx) = success_holder.lock().expect("lock").take() {
                    let _ = tx.send(());
                }

                stream
                    .write_all(b"OK")
                    .await
                    .expect("write in success handler failed");
                stream.flush().await.expect("flush in success handler failed");
            })
        })
        .on_preamble_decode_failure(move |_, _stream| {
            let failure_holder = failure_holder.clone();
            Box::pin(async move {
                // We should NOT hit this in this test – if we do, we'll observe it below
                // via `failure_rx`.
                if let Some(tx) = failure_holder.lock().expect("lock").take() {
                    let _ = tx.send(());
                }
            })
        });

    // Start the server with a connected client using whatever helper is already
    // used in the other preamble tests.
    let (client, _guard) = start_server_and_connect_client(server).await;

    // Act: send a valid preamble immediately, well before the timeout expires.
    send_valid_hotline_preamble(&client).await;

    // Assert: the preamble success handler ran before the timeout elapsed.
    timeout(Duration::from_millis(200), success_rx)
        .await
        .expect("timeout waiting for preamble success callback")
        .expect("preamble success callback send");

    // Assert: the failure handler did NOT run (we expect this to time out).
    assert!(
        timeout(Duration::from_millis(150), failure_rx).await.is_err(),
        "preamble failure callback unexpectedly fired for timely, valid preamble"
    );

    // Assert: the connection remains open and usable after the preamble.
    // We expect to be able to read the "OK" response sent in the success handler.
    let mut buf = [0u8; 2];
    client
        .read_exact(&mut buf)
        .await
        .expect("connection should remain open and readable after preamble");
    assert_eq!(&buf, b"OK", "expected OK response from preamble success handler");
}

```

The snippet above assumes the existence of several helpers and APIs that should be aligned with your existing tests:

1) Server/client bootstrap:
   - Replace `start_server_and_connect_client(server).await` with whatever helper you are already using in this file to:
     - Spawn the `WireframeServer`
     - Return a connected client `TcpStream` (or equivalent) and any guard/handle used to keep the server alive for the test.
   - Use the same pattern as in `preamble_timeout_invokes_failure_handler_and_closes_connection` for consistency.

2) Sending a valid preamble:
   - Implement `send_valid_hotline_preamble(&client).await` or replace it with the actual helper you already use to send a valid `HotlinePreamble` from the client side.
   - Make sure it sends exactly what `with_preamble::<HotlinePreamble>()` expects so that the success handler runs.

3) Success handler wiring:
   - If your codebase uses a different method name than `.on_preamble(...)` for the success path (e.g., `.on_preamble_success(...)` or a generic `.on_preamble_decoded(...)`), adjust the call accordingly.
   - Mirror the pattern used elsewhere in `tests/preamble.rs` for the "happy path" preamble test (e.g., the test that asserts successful preamble handling without a timeout).

4) Channel helpers:
   - This test assumes `channel_holder()` returns something like `(Arc<Mutex<Option<oneshot::Sender<()>>>>, oneshot::Receiver<()>)` and that cloning the holder is the intended pattern (as in your failure test).
   - If your actual type or pattern is slightly different, adjust the `success_holder`/`failure_holder` usage to match the existing tests.

5) Connection-open assertion:
   - If your existing tests use a different way to assert that the connection remains open (for example, attempting a second application-level message or using a helper like `assert_connection_open(&client).await`), replace the manual `read_exact`/`assert_eq!` portion with that existing pattern.

Once you align these placeholders with your current helpers and APIs, this test will exercise the scenario where:
- A preamble timeout is configured,
- A valid preamble arrives well within that timeout,
- The success handler runs,
- The failure handler does not run,
- And the connection remains open and usable after preamble processing.
</issue_to_address>

### Comment 2
<location> `tests/preamble.rs:233` </location>
<code_context>

+#[rstest]
+#[tokio::test]
+async fn failure_callback_can_write_response(
+    factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static,
+) {
</code_context>

<issue_to_address>
**suggestion (testing):** Consider adding a timeout-based failure test that also writes a response before closing

The new test covers the decode-error path well. Since the timeout path now uses the same failure handler with a `TimedOut` error and a live `TcpStream`, please add a similar test for the preamble-timeout case that writes a protocol error, verifies the client receives the response bytes, and then confirms the connection closes. This will validate the same "respond then close" behavior for timeouts as for decode failures.

Suggested implementation:

```rust
#[rstest]
#[tokio::test]
async fn failure_callback_can_write_response(
    factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static,
) {
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::TcpStream;

    let (failure_holder, failure_rx) = channel_holder();
    let server = WireframeServer::new(factory)
        .with_preamble::<HotlinePreamble>()
        .on_preamble_decode_failure(move |_, mut stream| {
            let failure_holder = failure_holder.clone();
            Box::pin(async move {
                stream.write_all(b"ERR").await.expect("write failed");
                stream.flush().await.expect("flush failed");

                if let Some(tx) = failure_holder.lock().expect("lock").take() {
                    let _ = tx.send(());
                }

                // Ensure we always close the connection after responding
                stream.shutdown().await.expect("shutdown failed");
                Ok::<(), io::Error>(())
            })
        });

    with_running_server(server, |addr| async move {
        let mut stream = TcpStream::connect(addr).await.expect("connect failed");

        // Send invalid data to trigger a decode failure
        stream
            .write_all(b"not-a-valid-preamble")
            .await
            .expect("write failed");

        // Read the protocol error response
        let mut buf = [0u8; 3];
        stream.read_exact(&mut buf).await.expect("read_exact failed");
        assert_eq!(&buf, b"ERR");

        // After the response is written, the server should close the connection
        let mut eof_buf = [0u8; 1];
        let read_res = stream.read(&mut eof_buf).await.expect("read failed");
        assert_eq!(read_res, 0);

        // Also ensure the failure callback actually ran
        failure_rx.await.expect("failure callback not called");
    })
    .await;
}

#[rstest]
#[tokio::test]
async fn preamble_timeout_failure_writes_response_and_closes(
    factory: impl Fn() -> WireframeApp + Send + Sync + Clone + 'static,
) {
    use std::time::Duration;
    use tokio::io::{AsyncReadExt, AsyncWriteExt};
    use tokio::net::TcpStream;
    use tokio::time::timeout;

    let (failure_holder, failure_rx) = channel_holder();

    let server = WireframeServer::new(factory)
        .with_preamble::<HotlinePreamble>()
        // Configure a short preamble timeout so the test completes quickly
        .with_preamble_timeout(Duration::from_millis(50))
        // The timeout path should use the same failure handler and still
        // allow us to write a response before closing the connection.
        .on_preamble_decode_failure(move |_, mut stream| {
            let failure_holder = failure_holder.clone();
            Box::pin(async move {
                stream.write_all(b"ERR").await.expect("write failed");
                stream.flush().await.expect("flush failed");

                if let Some(tx) = failure_holder.lock().expect("lock").take() {
                    let _ = tx.send(());
                }

                // After responding, close the connection from the server side
                stream.shutdown().await.expect("shutdown failed");
                Ok::<(), io::Error>(())
            })
        });

    with_running_server(server, |addr| async move {
        let mut stream = TcpStream::connect(addr).await.expect("connect failed");

        // Do NOT send a preamble; just wait for the server-side timeout to fire.
        // We expect the failure handler to write "ERR" and then close.
        let mut buf = [0u8; 3];

        // Use a client-side timeout so the test doesn't hang if something breaks.
        let () = timeout(Duration::from_secs(2), async {
            stream.read_exact(&mut buf).await.expect("read_exact failed");
        })
        .await
        .expect("did not receive timeout error response in time");

        assert_eq!(&buf, b"ERR");

        // After the error response, the connection should be closed.
        let mut eof_buf = [0u8; 1];
        let read_len = stream.read(&mut eof_buf).await.expect("read failed");
        assert_eq!(read_len, 0, "connection should be closed after timeout error response");

        // Ensure the timeout failure callback actually executed.
        failure_rx.await.expect("timeout failure callback not called");
    })
    .await;
}

```

The above changes assume the following, which you may need to adjust to your actual API and imports:

1) There is a `with_preamble_timeout(Duration)` builder on `WireframeServer` that configures the preamble timeout. If the real method has a different name or signature (for example, `with_timeout`, `with_preamble_read_timeout`, or configuration via `WireframeConfig`), update the call accordingly.
2) The timeout path reuses the same `.on_preamble_decode_failure` callback with a `TimedOut` error, as referenced in your comment. If the timeout uses a different hook (e.g., `.on_preamble_timeout_failure` or a generic `.on_preamble_failure` that includes the error), wire the closure up to that method instead, keeping the “write ERR, flush, then close” behavior intact.
3) If `AsyncWriteExt::shutdown` is not in scope through your existing imports, ensure `tokio::io::AsyncWriteExt` is imported once at the top of the file instead of inside each test (or remove the explicit `shutdown` call if your server already closes the connection after the handler returns).
4) If you already have shared helpers for:
   - connecting a client,
   - asserting EOF after a response,
   - or waiting for the failure callback,
   you can replace the inline logic in the tests with those helpers to align with the rest of the test suite style.
</issue_to_address>

### Comment 3
<location> `src/server/config/tests.rs:142` </location>
<code_context>
-        PreambleHandlerKind::Failure => server.on_preamble_decode_failure(move |_err: &DecodeError| {
-            c.fetch_add(1, Ordering::SeqCst);
-        }),
+        PreambleHandlerKind::Failure => server.on_preamble_decode_failure(
+            move |_err: &DecodeError, _stream| {
+                let c = c.clone();
</code_context>

<issue_to_address>
**suggestion (testing):** Add a test case for a failure handler that returns an error to exercise the error-logging/cleanup path

The current tests only cover the case where the async failure handler returns Ok(()). Since process_stream now logs and closes on handler errors, please add a variant where the handler immediately returns Err(io::Error::new(io::ErrorKind::Other, "boom")), trigger a preamble failure, and assert that the connection is closed and the test completes without panic. This will exercise the new error path and validate the error-handling behavior.

Suggested implementation:

```rust
        PreambleHandlerKind::Failure => server.on_preamble_decode_failure(
            move |_err: &DecodeError, _stream| {
                let c = c.clone();
                Box::pin(async move {
                    c.fetch_add(1, Ordering::SeqCst);
                    Ok::<(), io::Error>(())
                })
            },
        ),
    };

    assert_eq!(counter.load(Ordering::SeqCst), 0);
                .on_preamble_failure
}

#[rstest]
#[tokio::test]
async fn test_preamble_failure_handler_error() {
    use std::io;
    use std::sync::{
        atomic::{AtomicUsize, Ordering},
        Arc,
    };

    // Arrange
    let counter = Arc::new(AtomicUsize::new(0));
    let c = counter.clone();

    // Build a server with a failure handler that returns an error.
    //
    // This should mirror the setup used in `test_bind_success`, except that:
    // - we always install the `PreambleHandlerKind::Failure` variant
    // - the failure handler returns `Err(io::Error)` instead of `Ok(())`
    let server = {
        let counter = c.clone();

        // NOTE: This assumes the same server/builder initialization pattern as in `test_bind_success`.
        // The outer setup (e.g., creating the server, binding sockets, etc.) should be identical,
        // differing only in this handler configuration.
        let server = make_test_server(); // see <additional_changes> for details

        let _server = match PreambleHandlerKind::Failure {
            PreambleHandlerKind::Failure => server.on_preamble_decode_failure(
                move |_err: &DecodeError, _stream| {
                    let c = counter.clone();
                    Box::pin(async move {
                        c.fetch_add(1, Ordering::SeqCst);
                        Err::<(), io::Error>(io::Error::new(io::ErrorKind::Other, "boom"))
                    })
                },
            ),
            // If other variants are required by the builder in this scope, mirror them from
            // the existing `test_bind_success` implementation.
        };

        server
    };

    // Act: trigger a preamble failure on this server/connection.
    //
    // This should follow the same pattern used by the existing tests to induce a preamble
    // decode failure (e.g., connecting a client and sending an invalid/partial preamble).
    let result = trigger_preamble_failure(server.clone()).await;

    // Assert:
    //
    // 1. The failure handler was invoked exactly once.
    assert_eq!(counter.load(Ordering::SeqCst), 1);

    // 2. The processing completed without panicking. Depending on the helpers, this may
    // surface as either Ok(()) or an io::Error; the important thing is that the future
    // completes and the error path is exercised.
    assert!(result.is_ok() || result.is_err());

    // 3. The connection associated with this server is closed after the failure.
    //    Reuse whatever helper the test module already uses for this check.
    assert!(connection_is_closed(&server));

```

for details

        let _server = match PreambleHandlerKind::Failure {
            PreambleHandlerKind::Failure => server.on_preamble_decode_failure(
                move |_err: &DecodeError, _stream| {
                    let c = counter.clone();
                    Box::pin(async move {
                        c.fetch_add(1, Ordering::SeqCst);
                        Err::<(), io::Error>(io::Error::new(io::ErrorKind::Other, "boom"))
                    })
                },
            ),
            // If other variants are required by the builder in this scope, mirror them from
            // the existing `test_bind_success` implementation.
        };

        server
    };

    // Act: trigger a preamble failure on this server/connection.
    //
    // This should follow the same pattern used by the existing tests to induce a preamble
    // decode failure (e.g., connecting a client and sending an invalid/partial preamble).
    let result = trigger_preamble_failure(server.clone()).await;

    // Assert:
    //
    // 1. The failure handler was invoked exactly once.
    assert_eq!(counter.load(Ordering::SeqCst), 1);

    // 2. The processing completed without panicking. Depending on the helpers, this may
    // surface as either Ok(()) or an io::Error; the important thing is that the future
    // completes and the error path is exercised.
    assert!(result.is_ok() || result.is_err());

    // 3. The connection associated with this server is closed after the failure.
    //    Reuse whatever helper the test module already uses for this check.
    assert!(connection_is_closed(&server));
>>>>>>> REPLACE
</file_operation>
</file_operations>

<additional_changes>
To fully implement and compile this new test, you will need to:

1. Replace `make_test_server()` with the actual server/builder setup used in `test_bind_success`:
   - Copy whatever code constructs and configures the `server` in `test_bind_success` into this test.
   - Keep it identical except for the `on_preamble_decode_failure` configuration, which should use the error-returning handler shown in this patch.

2. Ensure the `match PreambleHandlerKind::Failure { ... }` block matches your existing pattern:
   - In `test_bind_success`, if you currently match on multiple `PreambleHandlerKind` variants (e.g., `Success`, `Failure`, etc.), mirror all of those arms here so the builder is configured consistently.
   - The only behavioral difference in this test should be that the `Failure` arm returns `Err(io::Error::new(io::ErrorKind::Other, "boom"))` after incrementing the counter.

3. Implement or reuse the helper to trigger a preamble failure:
   - Replace `trigger_preamble_failure(server.clone()).await` with the same sequence your existing tests use to cause a preamble decode failure (e.g., create a client connection, send an invalid preamble, and wait for the server to process it).
   - If you already have a helper function for this in the test module, call that instead of `trigger_preamble_failure`.

4. Implement or reuse the connection-closed assertion:
   - Replace `connection_is_closed(&server)` with your real assertion for checking that the connection was closed after the failure.
   - For example, this might be:
     - Checking that `process_stream` returned an error and the underlying transport reports `is_closed()`, or
     - Inspecting a captured log / metric / internal state that indicates the connection was closed, depending on how the rest of the tests are structured.

5. Verify imports:
   - If `io`, `AtomicUsize`, `Ordering`, `Arc`, `DecodeError`, or `PreambleHandlerKind` are already imported at the module level in this file, you can remove the `use` statements inside the test to avoid duplication.
   - Otherwise, move these `use` statements to the top of the file with the other imports to keep the style consistent with the existing tests.

With these adjustments, `test_preamble_failure_handler_error` will:
- Use a failure handler that immediately returns `Err(io::Error::new(io::ErrorKind::Other, "boom"))`,
- Trigger a preamble decode failure to invoke that handler,
- Assert that the handler was called,
- Assert that the connection is closed afterward,
- And confirm that the test completes without panic, exercising the error-logging/cleanup path in `process_stream`.
</issue_to_address>

### Comment 4
<location> `src/server/mod.rs:65` </location>
<code_context>
+///
+/// Implementors may perform asynchronous I/O on the provided stream to emit a
+/// response before the connection is closed.
+pub trait PreambleFailureHandler<T>:
+    for<'a> Fn(&'a DecodeError, &'a mut tokio::net::TcpStream) -> BoxFuture<'a, io::Result<()>>
+    + Send
</code_context>

<issue_to_address>
**issue (complexity):** Consider replacing the new PreambleFailureHandler trait and its blanket impl with a single type alias for the async callback to make the handler’s role clearer and the code simpler.

You can simplify the new preamble failure handler without losing any functionality by dropping the trait and blanket impl, and keeping just a single type alias for the callback.

Right now, `PreambleFailureHandler<T>` is just a renamed function signature with no additional behavior, and the extra indirection makes it harder to see what the handler actually is.

You can replace the trait + impl + alias with a single alias:

```rust
/// Handler invoked when decoding a connection preamble fails.
///
/// Implementors may perform asynchronous I/O on the provided stream to emit a
/// response before the connection is closed.
pub type PreambleFailure<T> = Arc<
    dyn for<'a> Fn(
            &'a DecodeError,
            &'a mut tokio::net::TcpStream,
        ) -> BoxFuture<'a, io::Result<()>>
        + Send
        + Sync
        + 'static
>;
```

And then keep the usage unchanged in the server struct:

```rust
pub struct WireframeServer<T, S, F>
where
    T: Preamble,
    S: ServerState,
{
    // ...
    pub(crate) on_preamble_success: Option<PreambleHandler<T>>,
    pub(crate) on_preamble_failure: Option<PreambleFailure<T>>,
    // ...
}
```

This:

- Removes an extra public trait and impl that don’t add capabilities (no methods/assoc types).
- Makes it immediately obvious that `PreambleFailure` is an async callback type.
- Keeps the existing async behavior and mut access to the `TcpStream` intact.
</issue_to_address>

### Comment 5
<location> `src/server/config/preamble.rs:73` </location>
<code_context>
+    ///     WireframeServer::new(|| WireframeApp::default()).preamble_timeout(Duration::from_secs(1));
+    /// ```
+    #[must_use]
+    pub fn preamble_timeout(mut self, timeout: Duration) -> Self {
+        let normalised = timeout.max(Duration::from_millis(1));
+        self.preamble_timeout = Some(normalised);
</code_context>

<issue_to_address>
**issue (review_instructions):** Add behavioural and unit tests covering preamble_timeout, including clamping and end-to-end timeout behaviour.

Add tests that exercise this new preamble_timeout feature. At minimum:
- Add unit tests that verify the clamping behaviour (e.g. values < 1ms are normalised to 1ms, values >= 1ms are preserved).
- Add behavioural/integration tests that configure a server with a small preamble_timeout and a client that delays or omits the preamble, and assert that the connection times out as expected and routes through the preamble failure path.
These tests are required by the review rule that all new feature changes must be covered by both behavioural and unit tests.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural and unit tests are required.

</details>
</issue_to_address>

### Comment 6
<location> `src/server/connection.rs:68` </location>
<code_context>
     T: Preamble,
 {
-    match read_preamble::<_, T>(&mut stream).await {
+    let preamble_result = match preamble_timeout {
+        Some(limit) => match timeout(limit, read_preamble::<_, T>(&mut stream)).await {
+            Ok(result) => result,
</code_context>

<issue_to_address>
**issue (review_instructions):** Add tests validating that preamble timeouts and asynchronous failure handlers behave correctly and are invoked as documented.

Add tests that directly exercise the new timeout path and asynchronous failure handler:
- Add unit tests around process_stream (or a suitably factored helper) that simulate a timeout (e.g. using a stream that never produces a full preamble) and assert that timeout_error is produced and that the failure handler is called.
- Add behavioural/integration tests using on_preamble_decode_failure that perform async I/O on the TcpStream (e.g. writing a marker response) and assert that the client observes this response before the connection closes.
This ensures the new timeout and async failure handling behaviour is verified in accordance with the requirement for both behavioural and unit tests for new features.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
For any new feature or change to an existing feature, both behavioural and unit tests are required.

</details>
</issue_to_address>

### Comment 7
<location> `docs/users-guide.md:395` </location>
<code_context>
-success handlers and synchronous failure callbacks, letting you reject
-connections or log decode errors before the application runs.[^20]
+success handlers and asynchronous failure callbacks that receive the stream,
+letting you reply or log decode errors before the application runs. An optional
+`preamble_timeout` caps how long `read_preamble` waits; timeouts use the
+failure callback path.[^20]
</code_context>

<issue_to_address>
**suggestion (review_instructions):** This sentence uses the second-person pronoun "you", which the style instructions forbid.

The documentation guidelines ask to avoid first- and second-person pronouns. Consider rephrasing this sentence to avoid "you", for example:

- "letting the server reply or log decode errors before the application runs", or
- "enabling replies or logging of decode errors before the application runs".

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Avoid 2nd person or 1st person pronouns ("I", "you", "we").

</details>
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/preamble.rs
Comment thread tests/preamble.rs
Comment thread src/server/config/tests.rs
Comment thread src/server/mod.rs Outdated
Comment thread docs/users-guide.md Outdated
@coderabbitai coderabbitai Bot added the codex label Nov 25, 2025
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: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 450c0df and 40d93ac.

📒 Files selected for processing (12)
  • docs/hardening-wireframe-a-guide-to-production-resilience.md (1 hunks)
  • docs/preamble-validator.md (1 hunks)
  • docs/server/configuration.md (1 hunks)
  • docs/users-guide.md (2 hunks)
  • src/server/config/binding.rs (2 hunks)
  • src/server/config/mod.rs (1 hunks)
  • src/server/config/preamble.rs (3 hunks)
  • src/server/config/tests.rs (6 hunks)
  • src/server/connection.rs (8 hunks)
  • src/server/mod.rs (4 hunks)
  • src/server/runtime.rs (9 hunks)
  • tests/preamble.rs (5 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/server/configuration.md
  • docs/preamble-validator.md
  • docs/users-guide.md
  • docs/hardening-wireframe-a-guide-to-production-resilience.md
**/*.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/server/config/preamble.rs
  • src/server/config/mod.rs
  • src/server/config/binding.rs
  • src/server/connection.rs
  • src/server/mod.rs
  • src/server/runtime.rs
  • tests/preamble.rs
  • src/server/config/tests.rs
🧬 Code graph analysis (5)
src/server/config/preamble.rs (1)
src/server/runtime.rs (1)
  • normalised (75-82)
src/server/config/binding.rs (1)
src/server/config/preamble.rs (1)
  • preamble_timeout (73-77)
src/server/connection.rs (2)
src/server/config/preamble.rs (1)
  • preamble_timeout (73-77)
src/server/config/mod.rs (1)
  • new (68-81)
src/server/runtime.rs (2)
src/server/config/mod.rs (1)
  • new (68-81)
src/server/connection.rs (3)
  • spawn_connection_task (18-55)
  • spawn_connection_task (155-155)
  • spawn_connection_task (205-205)
src/server/config/tests.rs (3)
src/server/test_util.rs (1)
  • factory (23-25)
src/server/config/mod.rs (1)
  • new (68-81)
src/server/config/preamble.rs (1)
  • preamble_timeout (73-77)
🔍 Remote MCP Deepwiki

Perfect! Now let me compile the findings into a concise summary.

Summary of Additional Context for PR #398 Review

Preamble Architecture & Related Work

The preamble support mechanism allows for optional connection preamble validation and negotiation as soon as a client connects using bincode to decode any type implementing bincode::BorrowDecode. The read_preamble function is an asynchronous function that reads bytes from the TcpStream and attempts to decode them using bincode::borrow_decode_from_slice, with DecodeError::UnexpectedEnd triggering additional reads until decoding succeeds.

This PR builds upon PR #28's preamble handler introduction. PR #28 introduced the PreambleSuccessHandler trait and PreambleHandler type alias, and updated spawn_connection_task to accept on_success and on_failure handlers as arguments. The current PR extends this by converting the synchronous failure handler to an async variant that receives the stream.

Connection Lifecycle & Error Handling

The connection lifecycle in spawn_connection_task wraps process_stream in std::panic::AssertUnwindSafe and catch_unwind() to isolate panics within individual connection tasks, preventing a single misbehaving connection from crashing the entire server. Following successful preamble processing, the TcpStream is wrapped in a RewindStream to handle any leftover bytes from the preamble reading.

DoS Protection & Resource Management Context

The server employs several DoS protection strategies including per-connection rate limiting using an asynchronous token-bucket algorithm, and memory caps and re-assembly timeouts for message re-assembly. A non-optional, configurable reassembly_timeout is applied to partial messages, preventing buffer filling by abandoned or slow-sent fragments. This PR adds a complementary preamble_timeout mechanism to bound the handshake phase specifically.

Timeout Implementation Patterns

Timeouts in the server are enforced using tokio::time::timeout around critical operations, with I/O timeouts applied during handshake phases to prevent resource exhaustion. The PR's use of tokio::time::timeout around read_preamble aligns with established timeout patterns in the codebase.

Async Callback Patterns

BoxFuture is a type alias for Pin<Box<dyn Future<Output = T> + Send>>, which allows for trait objects of futures enabling dynamic dispatch of asynchronous operations. The PreambleSuccessHandler<T> trait defines a handler that takes the decoded preamble T and a mutable TcpStream, returning a BoxFuture<'a, io::Result<()>> for asynchronous I/O operations. The PR's conversion of the failure handler to match this pattern maintains consistency with existing architectural patterns for async stream operations.

DecodeError Handling

During preamble reading, read_preamble handles DecodeError::UnexpectedEnd by attempting to read more bytes from the stream, while other DecodeError variants are returned directly. A custom DecodeError::Other("preamble too long") is returned if the preamble exceeds MAX_PREAMBLE_LEN (1024 bytes). The PR's timeout mechanism creates an additional error path via a synthetic timeout error, requiring proper integration with this error handling flow.

⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: build-test
🔇 Additional comments (27)
src/server/config/mod.rs (1)

77-77: LGTM!

The new preamble_timeout field is correctly initialized to None by default, ensuring backward compatibility. Clients must explicitly opt into timeout enforcement via the builder method.

src/server/config/binding.rs (1)

44-44: LGTM!

The preamble_timeout field is correctly destructured and propagated through the binding transition from Unbound to Bound state.

Also applies to: 61-61

docs/hardening-wireframe-a-guide-to-production-resilience.md (1)

238-245: LGTM!

Clear, concise documentation of the preamble timeout mechanism. The explanation correctly describes how timeout expiry routes through the asynchronous failure callback path, enabling protocol-specific error responses before socket closure.

docs/server/configuration.md (1)

85-112: LGTM!

Well-documented section covering the preamble timeout and async failure handler. The code example clearly demonstrates the pattern, including the use of .boxed() to return the required BoxFuture from the closure.

docs/users-guide.md (1)

626-627: LGTM!

Footnote correctly updated to reference both implementation files for the preamble handling feature.

src/server/config/tests.rs (5)

9-9: LGTM!

Import additions support the new async failure handler tests requiring io::Error, DecodeError, and Tokio networking types.

Also applies to: 17-17, 19-19, 32-32


77-87: LGTM!

Good coverage of the preamble timeout configuration, including verification of the 1ms minimum clamp for zero duration inputs.


142-150: LGTM!

Failure handler registration correctly updated to the new two-argument async signature (&DecodeError, &mut TcpStream) -> BoxFuture<Result<(), io::Error>>.


180-190: LGTM!

Properly sets up a real TCP connection to test the async failure handler with stream access. The pattern correctly maintains the client connection lifetime via _client.


214-214: LGTM!

Minimal update to align with the new failure handler signature whilst maintaining focus on builder method chaining verification.

src/server/config/preamble.rs (3)

55-77: LGTM! Clean builder method for preamble timeout.

The normalisation using .max(Duration::from_millis(1)) aligns with the BackoffConfig::normalised() pattern seen in src/server/runtime.rs. Documentation is clear and the example demonstrates usage well.


105-132: Well-documented API change.

The documentation clearly explains the new async signature and provides a practical example showing how to write a response before connection closure.


38-53: Timeout propagation is correct.

The preamble_timeout is appropriately preserved when switching preamble types, as it represents server-wide configuration rather than preamble-specific behaviour.

src/server/mod.rs (1)

142-146: Clear field documentation.

The doc comment accurately describes the timeout semantics and normalisation behaviour.

src/server/connection.rs (4)

68-74: Timeout implementation is sound.

The timeout wrapping around read_preamble correctly handles both the timeout case (converting elapsed to a synthetic DecodeError) and the no-timeout case. The pattern aligns with established timeout usage in the codebase.


107-112: Appropriate error representation for timeout.

Using DecodeError::Io with ErrorKind::TimedOut is the correct choice - it integrates cleanly with existing decode error handling paths and clearly communicates the timeout nature to failure handlers.


89-103: Correct async failure handler invocation.

The handler is awaited and its errors are logged without propagation, which is appropriate for connection cleanup paths. The logging pattern mirrors the success handler path.


155-155: Test call site updated correctly.

The additional None parameter for preamble_timeout is correctly propagated to the test invocations.

src/server/runtime.rs (3)

85-109: Good abstraction for bundling preamble configuration.

PreambleHooks<T> cleanly groups the related preamble callbacks and timeout. The manual Clone and Default implementations are necessary since the Arc<dyn ...> fields prevent deriving these traits automatically.


229-248: Clean refactor to use PreambleHooks.

The bundling of preamble configuration into a single struct simplifies the API and makes the code more maintainable. The per-worker cloning is appropriate given the Arc-wrapped handlers.


455-462: Tests correctly updated to use PreambleHooks::default().

The test invocations properly use the new struct-based API.

tests/preamble.rs (6)

44-65: Helper function signature correctly updated.

The server_with_handlers function now accepts the new async failure handler signature, maintaining consistency with the API changes.


231-266: Good coverage for failure handler stream writing.

This test validates the core new capability: writing a response before connection closure on preamble failure.


268-311: Comprehensive timeout behaviour test.

The test validates:

  1. Timeout triggers after configured duration
  2. Failure handler receives TimedOut error kind
  3. Connection closes after timeout

The handling of both graceful close (Ok(0)) and ConnectionReset is appropriate for cross-platform compatibility.


313-348: Good edge case: success handler without failure handler.

This validates that the server functions correctly when only a success handler is configured, which is a valid use case.


375-390: Helper function correctly updated to async signature.

The failure_cb helper now returns the expected boxed future, consistent with the success_cb pattern.


155-166: Test callback correctly adapted to new signature.

The failure callback closure is properly updated to the two-argument async form.

Comment thread docs/preamble-validator.md
Comment thread docs/users-guide.md Outdated
Comment thread src/server/mod.rs Outdated
Comment thread src/server/runtime.rs
@leynos leynos changed the title feat(server,preamble): add configurable preamble read timeout with fa… feat(server,preamble): preamble timeout and async failure handler Nov 25, 2025
…ow with timeout

Added a detailed sequence diagram to the preamble-validator documentation. The diagram illustrates the interaction between the accept loop, connection task, preamble decoding, timeout handling, success and failure callbacks, and handoff to the application. This enhances the understanding of the preamble processing flow and timeout management.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title feat(server,preamble): preamble timeout and async failure handler feat(server,preamble): add PreambleHooks and async preamble failure with timeout Nov 26, 2025
…ameter

Changed spawn_connection_task and related calls to accept a single PreambleHooks struct instead of separate on_success, on_failure, and preamble_timeout parameters. This reduces parameter bloat and simplifies passing preamble-related handlers and timeout settings together. Updated relevant tests and documentation to reflect this change.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos changed the title feat(server,preamble): add PreambleHooks and async preamble failure with timeout feat(server,preamble): PreambleHooks, clamp timeout, async failure callback Nov 26, 2025
@leynos leynos merged commit 46c2fdf into main Nov 26, 2025
6 checks passed
@leynos leynos deleted the feature/preamble-timeout branch November 26, 2025 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant