Skip to content

Comments

Add replay (time-travel debugging) feature#98

Merged
Tuntii merged 3 commits intomainfrom
time-travel-debugging
Feb 6, 2026
Merged

Add replay (time-travel debugging) feature#98
Tuntii merged 3 commits intomainfrom
time-travel-debugging

Conversation

@Tuntii
Copy link
Owner

@Tuntii Tuntii commented Feb 6, 2026

This pull request introduces a new "replay" feature for time-travel debugging, allowing HTTP request/response pairs to be recorded and replayed for debugging purposes. The implementation is feature-gated (only enabled with the replay feature flag) and includes new CLI commands, configuration options, and core data structures for replay entries. Secure and sensible defaults are enforced, such as requiring an admin token and redacting sensitive data.

Replay Feature Integration

  • Added a new optional replay feature to both cargo-rustapi and rustapi-core crates, including necessary dependencies and feature flags in Cargo.toml files. [1] [2] [3]
  • Introduced the replay module in rustapi-core (behind the feature flag), enabling the core replay functionality.

CLI and Command Enhancements

  • Added a new Replay CLI subcommand (feature-gated) to the main CLI, and wired it up to the command handler. [1] [2]
  • Integrated the new replay command and argument types in the commands module, conditionally compiling them with the feature flag.

Replay System Implementation

  • Implemented ReplayConfig in replay/config.rs, providing a builder-pattern configuration for the replay system with secure defaults (disabled by default, admin token required, redacted headers, TTL, etc.), as well as utility methods and comprehensive tests.
  • Added core data structures for replay entries in replay/entry.rs, defining how HTTP requests and responses are captured, stored, and serialized for replay/debugging. Includes unit tests for serialization and entry creation.

Introduce a feature-gated replay subsystem for time-travel debugging. Adds core replay types and logic (config, entry, meta, diff, truncation, redaction, store) in rustapi-core, CLI commands in cargo-rustapi (list/show/run/diff) behind a `replay` feature, and supporting extras (auth, client, fs/memory stores, routes, retention) in rustapi-extras. Cargo.toml updates make async-trait and related deps optional and wire the new feature; Cargo.lock is updated accordingly. A cookbook recipe (docs/cookbook/src/recipes/replay.md) is included and the replay config defaults are secure (disabled by default, admin token required, sensitive headers redacted, TTL enforced).
Copilot AI review requested due to automatic review settings February 6, 2026 02:07
Apply whitespace/formatting cleanups and small refactors across replay-related crates for readability. Move the #[cfg(feature = "replay")] pub mod replay declaration up in rustapi-core (removing a duplicate), and tidy several files (commands/replay, replay config/diff/entry/redaction/truncation, extras auth/client/fs_store/layer/memory_store/routes). Changes are non-functional for the most part (reformatting, line-wrapping, import/order tweaks), with the module reordering removing a duplicate declaration that could cause issues.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request introduces a comprehensive time-travel debugging feature for HTTP request/response recording and replay. The implementation follows a feature-gated approach, ensuring the functionality is opt-in and doesn't impact existing deployments. The feature includes middleware for recording, multiple storage backends (in-memory and filesystem), admin API routes for management, CLI commands for interaction, and extensive security controls including token authentication and data redaction.

Changes:

  • Added replay feature flag to all relevant crates with proper dependency management
  • Implemented core data structures and configuration for replay entries with secure defaults (disabled by default, token-required, sensitive data redacted)
  • Created middleware layer for transparent request/response capture with body truncation and sampling support
  • Added admin API routes for listing, showing, replaying, and diffing recorded entries
  • Implemented CLI commands for replay management with multiple subcommands (list, show, run, diff)

Reviewed changes

Copilot reviewed 27 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
docs/cookbook/src/recipes/replay.md Comprehensive documentation for the replay feature including quick start, configuration, API reference, and security guidance
crates/rustapi-core/src/replay/config.rs Configuration system with builder pattern and secure defaults for replay recording
crates/rustapi-core/src/replay/entry.rs Core data structures for recorded requests/responses with metadata
crates/rustapi-core/src/replay/meta.rs Metadata structure for replay entries including duration, client IP, and tags
crates/rustapi-core/src/replay/store.rs Storage trait and query types for pluggable replay backends
crates/rustapi-core/src/replay/redaction.rs Utilities for redacting sensitive headers and JSON body fields
crates/rustapi-core/src/replay/truncation.rs Body truncation and content type detection utilities
crates/rustapi-core/src/replay/diff.rs JSON-aware diffing logic for comparing original and replayed responses
crates/rustapi-core/src/replay/mod.rs Module exports and documentation for the replay core
crates/rustapi-extras/src/replay/layer.rs Middleware layer for capturing request/response pairs with body buffering
crates/rustapi-extras/src/replay/memory_store.rs In-memory ring buffer implementation of the replay store
crates/rustapi-extras/src/replay/fs_store.rs Filesystem-based JSON Lines replay store with rotation support
crates/rustapi-extras/src/replay/client.rs HTTP client for replaying recorded requests against target servers
crates/rustapi-extras/src/replay/auth.rs Bearer token authentication for admin endpoints
crates/rustapi-extras/src/replay/routes.rs Admin API route handlers for replay management
crates/rustapi-extras/src/replay/retention.rs Background job for TTL-based cleanup of expired entries
crates/rustapi-extras/src/replay/mod.rs Module exports for the replay middleware
crates/cargo-rustapi/src/commands/replay.rs CLI commands for interacting with replay admin API
crates/cargo-rustapi/src/commands/mod.rs Integration of replay commands into CLI
crates/cargo-rustapi/src/cli.rs CLI enum extension for replay subcommand
crates/rustapi-core/Cargo.toml Added async-trait dependency for replay feature
crates/rustapi-extras/Cargo.toml Added dependencies (uuid, reqwest, dashmap) for replay feature
crates/rustapi-rs/Cargo.toml Added replay feature flag to full feature set
crates/cargo-rustapi/Cargo.toml Added replay feature flag with reqwest dependency
crates/rustapi-core/src/lib.rs Feature-gated replay module export
crates/rustapi-extras/src/lib.rs Feature-gated replay module export and re-exports
crates/rustapi-rs/src/lib.rs Feature-gated replay module re-export
Cargo.lock Updated with new dependency versions

Comment on lines +237 to +248
fn extract_query_param(uri: &http::Uri, key: &str) -> Option<String> {
uri.query().and_then(|q| {
q.split('&').find_map(|pair| {
let (k, v) = pair.split_once('=')?;
if k == key {
Some(v.to_string())
} else {
None
}
})
})
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The query parameter extraction does not URL-decode values. If query parameters contain URL-encoded characters (like spaces as %20 or special characters), they won't be decoded. This could cause issues when passing target URLs with encoded characters. Consider using urlencoding::decode or similar to properly handle encoded query parameter values.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +64
/// Admin bearer token
#[arg(short, long)]
pub token: String,

/// Maximum number of entries to return
#[arg(short, long)]
pub limit: Option<usize>,

/// Filter by HTTP method
#[arg(short, long)]
pub method: Option<String>,

/// Filter by path substring
#[arg(short, long)]
pub path: Option<String>,
}

/// Arguments for `replay show`
#[derive(Args, Debug)]
pub struct ReplayShowArgs {
/// Replay entry ID
pub id: String,

/// Server URL
#[arg(short, long, default_value = "http://localhost:8080")]
pub server: String,

/// Admin bearer token
#[arg(short, long)]
pub token: String,
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The documentation states that the --token parameter can be set via the RUSTAPI_REPLAY_TOKEN environment variable (line 121-125 of docs/cookbook/src/recipes/replay.md), but the CLI implementation does not actually read from this environment variable. The token field is marked as required without any env support in clap. Either add #[arg(short, long, env = "RUSTAPI_REPLAY_TOKEN")] to the token fields in the Args structs, or update the documentation to remove this claim.

Copilot uses AI. Check for mistakes.
let body = json!({
"error": "unauthorized",
"message": "Missing or invalid admin token. Use Authorization: Bearer <token>"
});
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The token comparison uses simple string equality which is vulnerable to timing attacks. An attacker could potentially use timing differences to guess the token character by character. Consider using a constant-time comparison function like subtle::ConstantTimeEq or a secure comparison from a crypto library to prevent timing side-channel attacks.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +93
// Ensure consistent ordering
if !query.newest_first {
results.reverse();
results.reverse(); // already correct order
}

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The double reverse operation is redundant. Lines 92-93 reverse the results twice, which effectively leaves them in the original order. This block should be removed entirely since when newest_first is false, the results are already in the correct order (oldest first) from the forward iteration in line 80.

Suggested change
// Ensure consistent ordering
if !query.newest_first {
results.reverse();
results.reverse(); // already correct order
}

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +262
let size = body_bytes.len();
if size <= config.max_request_body {
let body_str = String::from_utf8(body_bytes.to_vec()).ok();
// Apply body field redaction
let redacted = body_str.and_then(|s| {
if config.redact_body_fields.is_empty() {
Some(s)
} else {
redact_body(&s, &config.redact_body_fields, "[REDACTED]")
}
});
(size, redacted, false)
} else {
// Body too large - truncate
let body_str = String::from_utf8(body_bytes.to_vec()).ok();
let truncated = body_str.map(|s| {
let (t, _) = truncate_body(&s, config.max_request_body);
t
});
(size, truncated, true)
}
} else {
(0, None, false)
}
} else {
let size = req
.headers()
.get(http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
(size, None, false)
};

// Call the next handler
let response = next(req).await;

Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Critical bug: The request body is consumed with take_body() at line 226 but never restored before calling next(req) at line 262. This means downstream handlers will not be able to access the request body, breaking request handling. The body needs to be restored to the request after capture. Consider storing the captured bytes and then setting req.body = BodyVariant::Buffered(body_bytes) before calling next, or implement a non-destructive way to inspect the body.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +91
| Method | Path | Description |
|--------|------|-------------|
| GET | `/__rustapi/replays` | List recorded entries |
| GET | `/__rustapi/replays/{id}` | Show a single entry |
| POST | `/__rustapi/replays/{id}/run?target=URL` | Replay against target |
| POST | `/__rustapi/replays/{id}/diff?target=URL` | Replay and compute diff |
| DELETE | `/__rustapi/replays/{id}` | Delete an entry |

### Query Parameters for List

- `limit` - Maximum number of entries to return
- `method` - Filter by HTTP method (GET, POST, etc.)
- `path` - Filter by path substring
- `status_min` - Minimum status code filter

### Example: cURL

```bash
# List entries
curl -H "Authorization: Bearer my-secret-token" \
http://localhost:8080/__rustapi/replays?limit=10

# Show a specific entry
curl -H "Authorization: Bearer my-secret-token" \
http://localhost:8080/__rustapi/replays/<id>

# Replay against staging
curl -X POST -H "Authorization: Bearer my-secret-token" \
"http://localhost:8080/__rustapi/replays/<id>/run?target=http://staging:8080"

# Replay and diff
curl -X POST -H "Authorization: Bearer my-secret-token" \
"http://localhost:8080/__rustapi/replays/<id>/diff?target=http://staging:8080"
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The admin API and examples here expose a target query parameter that accepts an arbitrary base URL for replaying requests, but there is no documented restriction on which hosts/schemes are allowed. In the current implementation, this value is passed directly into the server-side HTTP client, so anyone with the admin token can use /__rustapi/replays/{id}/run?target=... or /diff?target=... as an SSRF primitive to make the server call internal or cloud-metadata endpoints and return the response. The docs should explicitly warn about this risk and recommend configuring the replay system to only allow a small, predefined set of replay targets (e.g., specific staging hosts) rather than arbitrary user-provided URLs, and the implementation should enforce such an allowlist by default.

Copilot uses AI. Check for mistakes.
@Tuntii Tuntii merged commit ea0755c into main Feb 6, 2026
7 checks passed
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.

1 participant