Skip to content

[Detail Bug] SDK Append: enabling compression with NoSideEffects retry policy makes all appends fail preflight #309

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_69fe7eaf-c4e7-45d2-b015-947cc069b40c

Summary

  • Context: The SDK's append functionality uses a frame monitoring system (introduced in recent commit 5061307) to detect if any request body data was sent over the network, enabling safe retries when using AppendRetryPolicy::NoSideEffects.
  • Bug: When compression is enabled and AppendRetryPolicy::NoSideEffects is used, append requests fail immediately with a compression error before any data is sent.
  • Actual vs. expected: The append operation fails with "streaming request bodies cannot be compressed" error instead of successfully compressing and sending the request.
  • Impact: Users who enable compression with NoSideEffects retry policy cannot perform append operations at all - all appends fail immediately.

Code with Bug

In sdk/src/client.rs, the monitored() method wraps a body in a streaming wrapper:

pub(crate) fn monitored(self, signal: FrameSignal) -> Self {
    Self(BodyInner::Streaming(BoxBody::new(  // <-- BUG 🔴 Converts Full to Streaming
        RequestFrameMonitorBody::new(self.into_http_body(), signal),
    )))
}

Later, compress_body() attempts to compress the body:

async fn compress_body(
    body: Body,
    compression: Compression,
) -> Result<(Body, Option<HeaderValue>), Error> {
    match compression {
        Compression::None => Ok((body, None)),
        Compression::Gzip => {
            let Some(data) = body.as_bytes() else {  // <-- BUG 🔴 Returns None for Streaming body
                return Err(Error::Compression(
                    "streaming request bodies cannot be compressed".into(),
                ));
            };
            // ... compression code
        }
        // ... similar for Zstd
    }
}

And as_bytes() returns None for streaming bodies:

fn as_bytes(&self) -> Option<&[u8]> {
    match &self.0 {
        BodyInner::Empty => Some(&[]),
        BodyInner::Full(bytes) => Some(bytes),
        BodyInner::Streaming(_) => None,  // <-- BUG 🔴 Streaming bodies can't be compressed
    }
}

Explanation

AppendRetryPolicy::NoSideEffects enables frame monitoring by wrapping the request body with RequestFrameMonitorBody, which changes the body representation to BodyInner::Streaming. The compression code path requires body.as_bytes() to retrieve the full payload; for streaming bodies as_bytes() returns None, causing compress_body() to error out with "streaming request bodies cannot be compressed". This happens before any network I/O, and the error is mapped to a non-retryable client error, so the append fails immediately.

Execution path (high level): user enables compression + NoSideEffects → request is cloned and with_monitored_body(...) is applied (body becomes streaming) → execute_unary_with calls compress_body(...) → compression fails because body is streaming.

Recommended Fix

Apply compression before wrapping the body with frame monitoring (i.e., compress while the body is still Full, then apply with_monitored_body(...)). This preserves monitoring behavior while allowing compression to operate on bytes.

History

This bug was introduced in commit 5061307. The commit added frame monitoring for append retry safety by wrapping request bodies in a RequestFrameMonitorBody, which converts bodies from BodyInner::Full to BodyInner::Streaming. The bug occurred because compression (which existed prior) requires calling body.as_bytes() to access the full payload, but this method returns None for streaming bodies, causing compression to fail before any monitoring or retry logic can execute.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions