asterdex-sdk 0.1.3

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// US-003: HTTP request building utilities (pub(crate))

use reqwest::Response;
use serde::de::DeserializeOwned;

use crate::rest::error::AsterDexError;
use crate::rest::response::ApiResponse;

/// Max number of bytes of a response body to include in an error message
/// for diagnostic purposes. Prevents `AsterDexError::SerdeError` from
/// carrying megabytes of response body across logs and metrics.
const ERROR_BODY_PREVIEW: usize = 512;

/// Extract ApiResponse from a reqwest::Response — handles status code mapping and header extraction.
///
/// # Errors
/// - `AsterDexError::RateLimited` if HTTP 429
/// - `AsterDexError::IpBanned` if HTTP 418
/// - `AsterDexError::ApiError` if 4xx with `{ "code": int, "msg": string }` body
/// - `AsterDexError::HttpError` if other non-success status (body truncated to `ERROR_BODY_PREVIEW`)
/// - `AsterDexError::NetworkError` / `Timeout` if the body could not be read
/// - `AsterDexError::SerdeError` if body cannot be deserialized into `T` (body preview only)
pub(crate) async fn parse_response<T: DeserializeOwned>(
    response: Response,
) -> Result<ApiResponse<T>, AsterDexError> {
    // Extract rate-limit headers BEFORE consuming the response
    let used_weight = response
        .headers()
        .get("X-MBX-USED-WEIGHT-1MINUTE")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u32>().ok());
    let order_count = response
        .headers()
        .get("X-MBX-ORDER-COUNT-1MINUTE")
        .and_then(|v| v.to_str().ok())
        .and_then(|s| s.parse::<u32>().ok());

    let status = response.status();

    // Handle 429 and 418 BEFORE reading body
    if status.as_u16() == 429 {
        let retry_after = response
            .headers()
            .get("Retry-After")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u64>().ok())
            .map(std::time::Duration::from_secs);
        return Err(AsterDexError::RateLimited { retry_after });
    }
    if status.as_u16() == 418 {
        let retry_after = response
            .headers()
            .get("Retry-After")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u64>().ok())
            .map(std::time::Duration::from_secs);
        return Err(AsterDexError::IpBanned { retry_after });
    }

    // Read body as bytes (avoids UTF-8 validation overhead + double-alloc of .text())
    let bytes = response.bytes().await.map_err(AsterDexError::from_reqwest)?;

    if !status.is_success() {
        // Try to parse as API error { code, msg } directly from bytes
        if let Ok(api_err) = serde_json::from_slice::<serde_json::Value>(&bytes) {
            if let (Some(code), Some(msg)) = (
                api_err.get("code").and_then(|c| c.as_i64()),
                api_err.get("msg").and_then(|m| m.as_str()),
            ) {
                return Err(AsterDexError::ApiError {
                    code,
                    msg: msg.to_string(),
                });
            }
        }
        return Err(AsterDexError::HttpError {
            status: status.as_u16(),
            body: body_preview(&bytes),
        });
    }

    // Success path: deserialize directly from bytes (no intermediate String)
    let data = serde_json::from_slice::<T>(&bytes).map_err(|e| AsterDexError::SerdeError {
        message: format!(
            "failed to deserialize response: {e} (body preview: {})",
            body_preview(&bytes)
        ),
    })?;

    Ok(ApiResponse {
        data,
        used_weight,
        order_count,
    })
}

/// Truncate body to `ERROR_BODY_PREVIEW` bytes for diagnostic messages.
/// Uses `from_utf8_lossy` so binary/garbled payloads don't panic.
fn body_preview(bytes: &[u8]) -> String {
    let slice = &bytes[..bytes.len().min(ERROR_BODY_PREVIEW)];
    let mut s = String::from_utf8_lossy(slice).into_owned();
    if bytes.len() > ERROR_BODY_PREVIEW {
        s.push_str(&format!("...[+{} bytes truncated]", bytes.len() - ERROR_BODY_PREVIEW));
    }
    s
}