36 KiB
HTTP Client Consolidation Analysis & Plan
Date: 2026-01-28
Status: Ready for Implementation
Priority: High (Phase 1), Medium (Phase 2), Low (Phase 3)
Estimated Effort: 4-6 hours total
Executive Summary
Current State: We have both reqwest (high-level) and hyper (low-level) as dependencies, plus duplicate versions due to eventsource-client and jsonschema.
Goal: Eliminate redundancy, consolidate on reqwest, and resolve version conflicts.
Key Finding: We don't actually need direct hyper dependency - it's only used in test utilities and can be easily replaced.
Table of Contents
- Reqwest vs Hyper - Relationship & Usage
- EventSource Client Analysis
- Test Helpers Using http-body-util
- Rustls Version Conflicts
- JsonSchema & Reqwest 0.12 vs 0.13
- Implementation Plan
- Expected Results
- Risks & Mitigations
- Testing Plan
- Timeline & Effort
- Recommendation
- Success Criteria
1. Reqwest vs Hyper - Relationship & Usage
What They Are
- hyper: Low-level HTTP implementation (protocol details, connection pooling)
- reqwest: High-level HTTP client built on top of hyper (ergonomic API)
Key Point: reqwest uses hyper internally, so having both directly is redundant unless you need low-level hyper features.
Our Usage Analysis
Direct hyper Usage
Location: crates/api/Cargo.toml (dev-dependencies only)
[dev-dependencies]
hyper = { workspace = true }
http-body-util = "0.1"
Actual Usage: crates/api/tests/helpers.rs
use http_body_util::BodyExt;
// Used to read response bodies in tests:
let body = response.into_body().collect().await?.to_bytes();
let json: Value = serde_json::from_slice(&body)?;
Conclusion: ✅ Only used in test utilities for reading HTTP response bodies
reqwest Usage
Locations: Production code across multiple crates
attune-common: HTTP utilities, external API callsattune-api: Outbound HTTP requestsattune-cli: API client for talking to Attune APIattune-worker: Downloading artifacts, making HTTP requests
Conclusion: ✅ Core production dependency used extensively
Verdict
✅ We don't need hyper directly - it's only used for test utilities that can be replaced with Axum's built-in utilities or simple helpers.
2. EventSource Client Analysis
Current Usage
Library: eventsource-client = "0.13" (dev-dependency)
Location: crates/api/tests/sse_execution_stream_tests.rs
Purpose: Testing Server-Sent Events (SSE) endpoints
Test Coverage:
test_sse_stream_receives_execution_updates- Core SSE functionalitytest_sse_stream_filters_by_execution_id- Filteringtest_sse_stream_requires_authentication- Authtest_sse_stream_all_executions- Multi-execution streamingtest_postgresql_notify_trigger_fires- DB trigger verification
Functionality Tested:
- PostgreSQL NOTIFY → Notifier Service → WebSocket → SSE flow
- Real-time execution status updates
- Authentication and authorization
- Event filtering
Problems with Current Library
❌ Uses old hyper 0.14 ecosystem
eventsource-client 0.13
└── hyper 0.14.32
└── http 0.2.12
└── rustls 0.21.12
└── tokio-rustls 0.24.1
❌ Not actively maintained
- Last updated: 1+ years ago
- Issues with newer Rust versions
- No updates to modern ecosystem
❌ Pulls in entire old dependency tree
- Creates version conflicts across the board
- Adds ~10-15 transitive dependencies
Alternative Options
Option A: reqwest-eventsource ✅ RECOMMENDED
Crate: reqwest-eventsource = "0.6"
Advantages:
- ✅ Built on top of
reqwest(uses our existing HTTP client) - ✅ Actively maintained (last updated 2024)
- ✅ Clean, simple API
- ✅ Modern dependencies (hyper 1.x, rustls 0.23)
- ✅ Well-documented with good examples
API Example:
use reqwest_eventsource::{Event, EventSource};
use futures::StreamExt;
let mut stream = EventSource::get(url);
while let Some(event) = stream.next().await {
match event {
Ok(Event::Open) => println!("Connected"),
Ok(Event::Message(msg)) => {
println!("Event: {}", msg.event);
println!("Data: {}", msg.data);
}
Err(e) => eprintln!("Error: {}", e),
}
}
Migration Complexity: Low
- Similar API to current library
- Event structure is compatible
- Authentication via headers or URL params
Option B: Implement SSE parsing ourselves
Complexity: ~100-150 lines of code
Pros:
- No external dependency
- Full control over implementation
- SSE protocol is simple (text-based)
Cons:
- Additional maintenance burden
- Need to handle edge cases (reconnection, keep-alive, etc.)
- Need to test thoroughly
- Reinventing the wheel
Recommendation: ❌ Not worth it - reqwest-eventsource is actively maintained and solves this well
Option C: async-sse or tokio-sse
Status: Less mature, smaller community
Recommendation: ❌ Stick with reqwest-eventsource (better maintained)
Option D: Remove SSE tests entirely
Recommendation: ❌ Strongly discouraged
- These tests are valuable
- They test critical real-time notification functionality
- SSE is a key feature of the API
Recommended Solution
✅ Migrate to reqwest-eventsource
- Best balance of functionality, maintenance, and ecosystem alignment
- Eliminates entire old dependency tree
- Low migration effort (2-3 hours)
3. Test Helpers Using http-body-util
Current Code
Location: crates/api/tests/helpers.rs
use http_body_util::BodyExt;
// In tests:
let body = response.into_body().collect().await?.to_bytes();
let json: Value = serde_json::from_slice(&body)?;
Purpose: Converting HTTP response bodies to bytes for JSON parsing in tests
Replacement Options
Option 1: Use Axum's built-in utilities ✅ RECOMMENDED
use axum::body::Body;
use axum::http::Response;
// Axum provides this directly:
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await?;
let json: Value = serde_json::from_slice(&body_bytes)?;
Advantages:
- ✅ Already in our dependencies (via axum)
- ✅ No additional dependencies
- ✅ Simple one-line replacement
Option 2: Create a helper function
use futures::stream::StreamExt;
async fn body_to_bytes(body: Body) -> Result<Vec<u8>> {
let mut bytes = Vec::new();
let mut stream = body.into_data_stream();
while let Some(chunk) = stream.next().await {
bytes.extend_from_slice(&chunk?);
}
Ok(bytes)
}
async fn body_to_json<T: DeserializeOwned>(body: Body) -> Result<T> {
let bytes = body_to_bytes(body).await?;
Ok(serde_json::from_slice(&bytes)?)
}
Advantages:
- ✅ More explicit control
- ✅ Can add custom error handling
- ✅ Reusable across tests
Recommended Solution
✅ Option 1: Use Axum's axum::body::to_bytes()
- Simplest solution
- Already available
- Well-tested by Axum team
4. Rustls Version Conflicts
Current Situation
rustls v0.21.12
└── rustls-native-certs v0.6.3
└── hyper-rustls v0.24.2
└── eventsource-client v0.13
rustls v0.23.36
└── rustls-native-certs v0.8.3
└── hyper-rustls v0.27.7
└── reqwest v0.13
Root Cause
eventsource-client 0.13 uses the old hyper 0.14 ecosystem, which transitively pulls in rustls 0.21.
Impact
- Two versions of entire TLS stack
- ~2-3 MB binary size overhead
- More crates to audit for security
- Potential for subtle TLS configuration differences
Solution
✅ Switching to reqwest-eventsource eliminates this entirely
Reason:
reqwest-eventsourceusesreqwest0.12+reqwest0.13 uses modernrustls0.23 ecosystem- All TLS dependencies consolidated to single version tree
Result After Migration:
rustls v0.23.36 (single version)
└── rustls-native-certs v0.8.3
└── hyper-rustls v0.27.7
└── reqwest v0.13
└── reqwest-eventsource v0.6
5. JsonSchema & Reqwest 0.12 vs 0.13
Current Situation
reqwest v0.12.28
└── jsonschema v0.38.1
└── attune-common
reqwest v0.13.1
└── [our workspace dependencies]
Analysis
Why jsonschema uses reqwest 0.12:
jsonschemausesreqwestto fetch remote schemas (HTTP URLs in$ref)- Library maintainers haven't updated to reqwest 0.13 yet
- jsonschema 0.38.1 is recent (released 2024)
Actual Impact:
- ❌ Binary size: ~2 MB duplication (two reqwest versions)
- ❌ SBOM: Additional entries for reqwest 0.12 tree
- ✅ Functionality: Both versions work fine (stable APIs)
- ✅ Risk: Low - both are mature, stable releases
How Much Do We Use JsonSchema?
Need to investigate:
grep -r "jsonschema::" crates/
grep -r "use jsonschema" crates/
Current Usage (based on dependencies):
- Used in
attune-common - Purpose: Likely JSON Schema validation for actions/workflows
- Criticality: TBD (needs verification)
Options
Option A: Wait for upstream update ⏳ RECOMMENDED
Rationale:
jsonschemamaintainers will likely update to reqwest 0.13 soon- Minimal actual impact (both versions are stable)
- Low risk of issues
- Cost is acceptable (~2 MB per binary)
Action: Monitor upstream, update when available
Timeline: Likely within 3-6 months
Option B: Use cargo patch ⚠️ NOT RECOMMENDED
[patch.crates-io]
jsonschema = { git = "https://github.com/Stranger6667/jsonschema-rs", branch = "main" }
Risks:
- ⚠️ Untested upstream changes
- ⚠️ Potential breaking changes before release
- ⚠️ Maintenance burden (need to track upstream)
- ⚠️ May break in unexpected ways
Verdict: Not worth the risk for ~2 MB savings
Option C: Remove jsonschema dependency 🔍 INVESTIGATE
Prerequisites: Need to determine:
- Where is jsonschema actually used?
- Can we replace it with alternatives?
- Is the validation critical?
Potential Alternatives:
schemars- JSON Schema generation (we already use this)validator- Rust-native validation (we already use this)- Manual validation with serde
Action Items:
- Audit jsonschema usage:
grep -r "jsonschema" crates/ - Determine if removable
- If yes: Remove and implement alternative
- If no: Accept the duplication
Timeline: 1-2 hours investigation + potential migration
Option D: Accept the duplication ✅ SHORT-TERM
Rationale:
- Only ~2 MB overhead
- Both versions are stable
- Upstream will update eventually
- Other priorities are higher
Recommendation: Accept for now, revisit in quarterly review
Recommended Solution
✅ Short-term: Accept reqwest 0.12 duplication (Option D) 🔍 Medium-term: Investigate jsonschema usage (Option C) ⏳ Long-term: Wait for upstream update (Option A)
Implementation Plan
Phase 1: Replace EventSource Client (High Impact) ⚡
Priority: HIGH
Effort: 2-3 hours
Impact: Eliminates entire old hyper ecosystem
Steps
1. Add reqwest-eventsource to workspace dependencies
File: Cargo.toml
[workspace.dependencies]
# ... existing dependencies ...
+reqwest-eventsource = "0.6"
2. Update API dev dependencies
File: crates/api/Cargo.toml
[dev-dependencies]
mockall = { workspace = true }
tower = { workspace = true }
hyper = { workspace = true }
http-body-util = "0.1"
tempfile = { workspace = true }
-eventsource-client = "0.13"
+reqwest-eventsource = { workspace = true }
3. Rewrite SSE test code
File: crates/api/tests/sse_execution_stream_tests.rs
Changes Required (~150 lines):
-use eventsource_client::{self as es, Client};
+use reqwest_eventsource::{Event, EventSource};
use futures::StreamExt;
// Build SSE URL with authentication
let sse_url = format!(
"http://localhost:8080/api/v1/executions/stream?execution_id={}&token={}",
execution.id, token
);
-// Create SSE client
-let client = es::ClientBuilder::for_url(&sse_url)?
- .header("Accept", "text/event-stream")?
- .build();
-
-let mut stream = Client::stream(&client);
+// Create SSE stream
+let mut stream = EventSource::get(&sse_url);
// Wait for SSE events with timeout
while attempts < max_attempts && (!received_running || !received_succeeded) {
match timeout(Duration::from_millis(500), stream.next()).await {
Ok(Some(Ok(event))) => {
match event {
- es::SSE::Connected(_) => {
+ Event::Open => {
println!("SSE connection established");
}
- es::SSE::Event(ev) => {
+ Event::Message(msg) => {
- if let Ok(data) = serde_json::from_str::<Value>(&ev.data) {
+ if let Ok(data) = serde_json::from_str::<Value>(&msg.data) {
// ... rest of logic unchanged ...
}
}
- es::SSE::Comment(_) => {
- println!("Received keep-alive comment");
- }
}
}
Key API Differences:
| eventsource-client | reqwest-eventsource |
|---|---|
es::SSE::Connected(_) |
Event::Open |
es::SSE::Event(ev) |
Event::Message(msg) |
es::SSE::Comment(_) |
(built into Event::Message) |
ev.data |
msg.data |
ClientBuilder |
Direct EventSource::get() |
4. Update all 5 test functions
Apply similar changes to:
test_sse_stream_receives_execution_updatestest_sse_stream_filters_by_execution_idtest_sse_stream_requires_authenticationtest_sse_stream_all_executionstest_postgresql_notify_trigger_fires
5. Handle authentication
The new library supports authentication via:
// Option 1: URL parameters (current approach, keep working)
let url = format!("{}?token={}", base_url, token);
EventSource::get(url)
// Option 2: Headers (better practice)
use reqwest::Client;
let client = Client::builder()
.default_headers({
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
format!("Bearer {}", token).parse().unwrap()
);
headers
})
.build()?;
let stream = EventSource::new(client.get(url));
6. Test thoroughly
cd crates/api
cargo test sse_execution_stream_tests -- --nocapture
Expected Benefits
- ✅ Removes
hyper0.14 dependency tree (~8-10 crates) - ✅ Removes
rustls0.21 dependency tree (~5-7 crates) - ✅ Removes
http0.2 dependency tree (~3-4 crates) - ✅ Total: ~15-20 crates eliminated from dependency tree
- ✅ Binary size reduction: ~3-5 MB per binary
- ✅ Compilation time: ~20-40 seconds faster on clean builds
- ✅ SBOM reduction: ~15-20 fewer entries
Phase 2: Remove Direct Hyper Dependency (Low Impact) 🔧
Priority: MEDIUM
Effort: 30 minutes
Impact: Cleanup only (hyper is transitive via reqwest anyway)
Steps
1. Replace http-body-util usage in test helpers
File: crates/api/tests/helpers.rs
use axum::{
body::Body,
http::{header, Method, Request, StatusCode},
};
-use http_body_util::BodyExt;
use serde::de::DeserializeOwned;
// Add helper function:
async fn body_to_json<T: DeserializeOwned>(body: Body) -> Result<T> {
- let bytes = body.collect().await?.to_bytes();
+ let bytes = axum::body::to_bytes(body, usize::MAX).await?;
Ok(serde_json::from_slice(&bytes)?)
}
2. Update all test helper usages
Find and replace pattern:
-let body = response.into_body().collect().await?.to_bytes();
-let json: Value = serde_json::from_slice(&body)?;
+let json: Value = body_to_json(response.into_body()).await?;
3. Remove from dev-dependencies
File: crates/api/Cargo.toml
[dev-dependencies]
mockall = { workspace = true }
tower = { workspace = true }
-hyper = { workspace = true }
-http-body-util = "0.1"
tempfile = { workspace = true }
reqwest-eventsource = { workspace = true }
4. Remove from workspace if no longer needed
File: Cargo.toml
Check if any other crate uses hyper:
grep -r "hyper" crates/*/Cargo.toml
If none:
[workspace.dependencies]
-hyper = { version = "1.0", features = ["full"] }
5. Test
cargo test --workspace
Expected Benefits
- ✅ No direct
hyperdependency in our code - ✅ Cleaner dependency tree
- ✅ ~100 KB binary size reduction (marginal)
- ✅ Hyper still present as transitive dependency (expected and fine)
Note
Hyper will remain in the dependency tree because:
reqwestuses it internallyaxumuses it internally- This is expected and desirable (it's the underlying HTTP implementation)
Phase 3: Investigate JsonSchema Usage (Optional) 🔍
Priority: LOW
Effort: 1-2 hours
Impact: Medium if removable, low if keeping
Steps
1. Find all uses of jsonschema
cd attune
grep -r "jsonschema::" crates/ --include="*.rs"
grep -r "use jsonschema" crates/ --include="*.rs"
grep -r "JsonSchema" crates/ --include="*.rs"
2. Analyze usage patterns
Determine:
- What is jsonschema used for?
- Is it critical functionality?
- Can it be replaced?
3. Decision tree
If used for JSON Schema validation:
- Option A: Keep it, accept reqwest 0.12 duplication
- Option B: Implement validation differently
- Use
validatorcrate (we already have it) - Use manual serde validation
- Generate validation code at compile time
- Use
If used for schema generation:
- We already have
schemarsfor this - Check if jsonschema can be removed in favor of schemars
If barely used:
- Remove it entirely
- Implement needed functionality differently
4. Implementation (if removing)
Example replacement:
-use jsonschema::JSONSchema;
+use validator::Validate;
-let schema = JSONSchema::compile(&schema_json)?;
-schema.validate(&instance)?;
+#[derive(Validate)]
+struct MyData {
+ #[validate(length(min = 1, max = 100))]
+ name: String,
+}
+
+my_data.validate()?;
5. Test thoroughly
cargo test --workspace
Decision Matrix
| Usage Level | Recommendation | Action |
|---|---|---|
| Critical feature | Keep it | Accept reqwest 0.12 duplication (~2 MB) |
| Nice-to-have | Evaluate alternatives | If easy to replace, do it |
| Barely used | Remove it | Eliminate dependency entirely |
| Not sure | Keep for now | Revisit in quarterly dependency review |
Expected Benefits (If Removed)
- ✅ Eliminates reqwest 0.12 duplication
- ✅ Binary size reduction: ~2 MB per binary
- ✅ SBOM reduction: ~5-8 fewer entries
- ✅ Faster compilation: ~5-10 seconds
Expected Cost (If Removed)
- ⚠️ Need to implement alternative validation
- ⚠️ Testing required to ensure validation still works
- ⚠️ Potential behavior changes
Expected Results After Implementation
Dependency Tree Reduction
Before Implementation
HTTP Clients:
- reqwest v0.12.28 (via jsonschema)
- reqwest v0.13.1 (workspace)
- hyper v0.14.32 (via eventsource-client)
- hyper v1.8.1 (via reqwest/axum)
HTTP Types:
- http v0.2.12 (via hyper 0.14)
- http v1.4.0 (via hyper 1.x)
- http-body v0.4.6 (via hyper 0.14)
- http-body v1.0.1 (via hyper 1.x)
TLS:
- rustls v0.21.12 (via hyper 0.14 ecosystem)
- rustls v0.23.36 (via reqwest)
- rustls-native-certs v0.6.3 (old)
- rustls-native-certs v0.8.3 (new)
- rustls-pemfile v1.0.4 (old)
- rustls-pemfile v2.2.0 (new)
- tokio-rustls v0.24.1 (old)
- tokio-rustls v0.26.4 (new)
Total duplicate versions: ~15-20
After Phase 1
HTTP Clients:
- reqwest v0.12.28 (via jsonschema) - acceptable
- reqwest v0.13.1 (workspace) ✓
- hyper v1.8.1 (transitive via reqwest/axum) ✓
HTTP Types:
- http v1.4.0 (single version) ✓
- http-body v1.0.1 (single version) ✓
TLS:
- rustls v0.23.36 (single version) ✓
- rustls-native-certs v0.8.3 (single version) ✓
- rustls-pemfile v2.2.0 (single version) ✓
- tokio-rustls v0.26.4 (single version) ✓
Total duplicate versions: ~2-3 (just reqwest)
After Phase 2
HTTP Clients:
- reqwest v0.12.28 (via jsonschema) - acceptable
- reqwest v0.13.1 (workspace) ✓
- hyper v1.8.1 (transitive, no direct dependency) ✓
No direct hyper dependency in our code ✓
After Phase 3 (If jsonschema removed)
HTTP Clients:
- reqwest v0.13.1 (single version) ✓
- hyper v1.8.1 (transitive) ✓
Total duplicate versions: 0 ✓✓✓
Binary Size Impact
| Phase | Per Binary | Total (7 binaries) | Cumulative |
|---|---|---|---|
| Phase 1 | -3 to -5 MB | -21 to -35 MB | -3 to -5 MB |
| Phase 2 | -100 KB | -700 KB | -3.1 to -5.1 MB |
| Phase 3 | -2 MB | -14 MB | -5.1 to -7.1 MB |
| Total | -5 to -7 MB | -35 to -50 MB | - |
Compilation Time Impact
| Phase | Clean Build | Incremental | Reason |
|---|---|---|---|
| Phase 1 | -20 to -40 sec | -2 to -5 sec | 15-20 fewer crates compiled |
| Phase 2 | -2 to -5 sec | < 1 sec | Marginal improvement |
| Phase 3 | -5 to -10 sec | -1 to -2 sec | reqwest 0.12 tree eliminated |
| Total | -27 to -55 sec | -3 to -8 sec | - |
SBOM (Software Bill of Materials) Impact
| Phase | Crates Removed | Security Impact |
|---|---|---|
| Phase 1 | 15-20 | High - eliminates old TLS stack |
| Phase 2 | 2-3 | Low - cleanup only |
| Phase 3 | 5-8 | Medium - eliminates reqwest duplication |
| Total | 22-31 | Significant reduction in audit surface |
Dependency Count
| Metric | Before | After Phase 1 | After Phase 2 | After Phase 3 |
|---|---|---|---|---|
| Total dependencies | ~250 | ~230 | ~228 | ~220 |
| Duplicate versions | 15-20 | 2-3 | 2-3 | 0 |
| Direct HTTP deps | 2 (reqwest, hyper) | 1 (reqwest) | 1 (reqwest) | 1 (reqwest) |
| TLS versions | 2 | 1 | 1 | 1 |
Risks & Mitigations
Risk 1: SSE Test Behavior Changes
Phase: 1
Probability: Low
Impact: Medium
Description: reqwest-eventsource may handle events slightly differently than eventsource-client
Mitigation:
- Both libraries implement the SSE specification correctly
- Test extensively before merging
- Keep old tests temporarily in a branch for comparison
- Run tests multiple times to ensure stability
- Test with actual SSE server (not just mocks)
Rollback Plan: Revert to eventsource-client if tests fail consistently
Risk 2: API Differences in reqwest-eventsource
Phase: 1
Probability: Low
Impact: Low
Description: API for creating and handling streams is slightly different
Mitigation:
- API is well-documented with clear examples
- Main differences are in connection setup, not event handling
- Event structure is similar (both follow SSE spec)
- Review library documentation before migration
Rollback Plan: Easy to revert (only dev-dependency, only tests affected)
Risk 3: Authentication Handling
Phase: 1
Probability: Very Low
Impact: Low
Description: Authentication might work differently
Mitigation:
reqwest-eventsourcesupports both URL params and headers- Current tests pass token in URL (will continue working)
- Can also use Authorization header for better security
- Test authentication explicitly
Validation:
// Test that auth still works
#[test]
async fn test_sse_auth_with_token() {
let url = format!("{}?token={}", base_url, token);
let mut stream = EventSource::get(&url);
// Should connect successfully
}
Risk 4: Test Helper Breakage
Phase: 2
Probability: Very Low
Impact: Low
Description: Replacing http-body-util with Axum utilities might break tests
Mitigation:
- Axum's
to_bytes()is well-tested and stable - Simple one-line replacement
- Test thoroughly after changes
- Keep old code commented out temporarily
Rollback Plan: Revert to http-body-util (2-line change)
Risk 5: JsonSchema Functionality Loss
Phase: 3
Probability: Low to Medium (depends on usage)
Impact: Medium to High (depends on criticality)
Description: Removing jsonschema might break validation functionality
Mitigation:
- First: Thoroughly audit usage before making any changes
- Implement alternative validation before removing
- Test all validation scenarios
- Consider keeping if used extensively
Decision Point: Don't proceed with Phase 3 if jsonschema is critical
Testing Plan
Phase 1 Testing
Unit Tests
# Run SSE-specific tests
cd crates/api
cargo test sse_execution_stream_tests -- --nocapture
# Expected output:
# test test_sse_stream_receives_execution_updates ... ok
# test test_sse_stream_filters_by_execution_id ... ok
# test test_sse_stream_requires_authentication ... ok
# test test_sse_stream_all_executions ... ok
# test test_postgresql_notify_trigger_fires ... ok
Integration Tests
# Start E2E services
./scripts/setup-e2e-db.sh
./scripts/start-e2e-services.sh
# Run full API test suite
cd crates/api
cargo test
# Stop services
./scripts/stop-e2e-services.sh
Manual Testing
# Start API server
cargo run --bin attune-api
# In another terminal, test SSE endpoint manually:
curl -N -H "Accept: text/event-stream" \
"http://localhost:8080/api/v1/executions/stream?token=YOUR_TOKEN"
# Should see:
# event: connected
# data: {"message":"Connected to execution stream"}
#
# event: execution_update
# data: {"entity_type":"execution","data":{...}}
Stress Testing
# Run SSE tests multiple times to ensure stability
for i in {1..10}; do
echo "Run $i"
cargo test sse_execution_stream_tests -- --test-threads=1
done
Phase 2 Testing
Unit Tests
# Run all tests that use test helpers
cargo test --workspace
# Specifically test API endpoints
cd crates/api
cargo test
Verify No Regressions
# Compare test output before and after
cargo test --workspace 2>&1 | tee before.txt
# ... make changes ...
cargo test --workspace 2>&1 | tee after.txt
diff before.txt after.txt
# Should show no test failures, only dependency changes
Phase 3 Testing (If Implemented)
Validation Tests
# Identify and run all tests that use jsonschema
grep -r "jsonschema" crates/*/tests/ | cut -d: -f1 | sort -u
# Run those specific test files
# Example:
cargo test --test schema_validation_tests
Manual Validation Testing
# Test action/workflow validation with various inputs
# Test both valid and invalid schemas
# Ensure error messages are still clear
Dependency Verification
# Check for duplicate dependencies
cargo tree -d
# Expected after Phase 1:
# Should NOT see: hyper v0.14, rustls v0.21, http v0.2
# Should see: reqwest v0.12 and v0.13 (acceptable until Phase 3)
# Check binary sizes
cargo clean
cargo build --release
ls -lh target/release/attune-* > before_sizes.txt
# ... make changes ...
cargo clean
cargo build --release
ls -lh target/release/attune-* > after_sizes.txt
diff before_sizes.txt after_sizes.txt
Compilation Time Measurement
# Before changes
cargo clean
time cargo build --workspace > /dev/null
# After changes
cargo clean
time cargo build --workspace > /dev/null
# Compare times
Regression Testing Checklist
- All unit tests pass
- All integration tests pass
- SSE streaming works correctly
- Authentication still works
- Event filtering works
- PostgreSQL NOTIFY triggers fire
- WebSocket connections stable
- No new compiler warnings
- Documentation is updated
- Binary sizes reduced (or unchanged)
- No performance regressions
Timeline & Effort
Phase 1: Replace EventSource Client
| Task | Estimated Time | Difficulty |
|---|---|---|
| Add reqwest-eventsource dependency | 5 min | Easy |
| Update imports and setup | 15 min | Easy |
| Rewrite event handling logic | 60 min | Medium |
| Update all 5 test functions | 45 min | Medium |
| Test and debug | 30 min | Medium |
| Total Phase 1 | 2.5-3 hours | Medium |
Timeline: Can be completed in one work session
Priority: ⚡ HIGH - Biggest impact
Phase 2: Remove Direct Hyper Dependency
| Task | Estimated Time | Difficulty |
|---|---|---|
| Replace http-body-util in helpers | 10 min | Easy |
| Update all usages | 10 min | Easy |
| Remove from dependencies | 5 min | Easy |
| Test | 10 min | Easy |
| Total Phase 2 | 30-45 min | Easy |
Timeline: Can be done immediately after Phase 1
Priority: 🔧 MEDIUM - Cleanup and polish
Phase 3: Investigate JsonSchema Usage
| Task | Estimated Time | Difficulty |
|---|---|---|
| Audit jsonschema usage | 20 min | Easy |
| Analyze criticality | 15 min | Easy |
| Research alternatives | 25 min | Medium |
| Decision: keep or remove | - | - |
| If removing: | ||
| - Implement alternative | 30-60 min | Medium-Hard |
| - Update all usages | 20-40 min | Medium |
| - Test thoroughly | 30 min | Medium |
| Total Phase 3 | 1-3 hours | Medium-Hard |
Timeline: Separate task, can be done later
Priority: 🔍 LOW - Nice to have
Overall Timeline
| Scenario | Total Effort | Timeline | Recommendation |
|---|---|---|---|
| Phase 1 only | 2.5-3 hours | Half day | ✅ Do this now |
| Phases 1 + 2 | 3-4 hours | Half day | ✅ Do together |
| All phases | 4-7 hours | 1-2 days | ⏳ Split across time |
Recommendation
Immediate Actions (This Week)
✅ Phase 1: Replace EventSource Client
DO THIS NOW - Highest impact, reasonable effort
Why:
- Eliminates entire old
hyper0.14 ecosystem - Removes
rustls0.21 security concerns - Reduces binary size by 3-5 MB
- Speeds up compilation by 20-40 seconds
- Reduces SBOM by 15-20 entries
- Well-maintained replacement available
Risk: Low - well-tested library, similar API
Success Metric: cargo tree -d shows no more hyper 0.14
✅ Phase 2: Remove Direct Hyper Dependency
DO AFTER PHASE 1 - Easy cleanup
Why:
- Completes the consolidation
- Simple changes, low risk
- Cleaner architecture
Risk: Very low - straightforward replacement
Success Metric: No direct hyper in our Cargo.toml files
Follow-up Actions (Next Month)
🔍 Phase 3: Investigate JsonSchema
DO LATER - Lower priority
Why:
- Need to understand usage first
- May not be worth the effort
- Upstream might update soon anyway
Timeline: During next quarterly dependency review
Decision Point: Only proceed if:
- jsonschema is barely used, OR
- Easy alternative exists, AND
- Team has bandwidth
Do NOT Do
❌ Don't use cargo patch for jsonschema
Reason: Too risky for minimal benefit (~2 MB)
Risk: Potential breaking changes, maintenance burden
Better: Wait for upstream to update to reqwest 0.13
❌ Don't implement SSE parsing ourselves
Reason: reqwest-eventsource is actively maintained
Risk: Reinventing the wheel, maintenance burden
Better: Use the well-tested library
❌ Don't skip testing
Reason: SSE tests are critical for real-time features
Risk: Breaking production functionality
Better: Test thoroughly, especially SSE streaming
Success Criteria
After Phase 1
- ✅ No
hyper0.14.x incargo tree - ✅ No
rustls0.21.x incargo tree - ✅ No
http0.2.x incargo tree - ✅ All 5 SSE tests pass consistently
- ✅ SSE streaming works in E2E environment
- ✅ Binary sizes reduced by 3-5 MB each
- ✅ Compilation time reduced by 20-40 seconds
- ✅ SBOM reduced by 15-20 entries
After Phase 2
- ✅ No direct
hyperdependency in any Cargo.toml - ✅ No
http-body-utildependency - ✅ All tests still pass
- ✅ Test helpers work correctly
- ✅
cargo tree | grep hypershows only transitive (via reqwest/axum)
After Phase 3 (If Implemented)
- ✅ Only one version of
reqwestincargo tree(0.13) - ✅ No
jsonschemadependency (if removed) - ✅ Alternative validation works correctly
- ✅ All validation tests pass
- ✅ Binary sizes reduced by additional ~2 MB
Overall Success
✅ Consolidated HTTP client strategy
- Single high-level HTTP client (
reqwest) - No direct low-level HTTP dependencies
- Modern TLS stack (single version)
✅ Reduced maintenance burden
- Fewer dependencies to audit
- Fewer security updates to track
- Cleaner dependency tree
✅ Improved build times and binary sizes
- Faster CI/CD pipelines
- Smaller deployment artifacts
- Quicker development iteration
✅ Better developer experience
- Clearer architecture
- Easier to understand dependencies
- Documented strategy for HTTP clients
Appendix: Code Examples
Example: reqwest-eventsource Basic Usage
use reqwest_eventsource::{Event, EventSource, Error};
use futures::StreamExt;
async fn consume_sse_stream(url: &str) -> Result<(), Error> {
let mut stream = EventSource::get(url);
while let Some(event) = stream.next().await {
match event {
Ok(Event::Open) => {
println!("SSE connection opened");
}
Ok(Event::Message(msg)) => {
println!("Event type: {}", msg.event);
println!("Data: {}", msg.data);
println!("ID: {:?}", msg.id);
}
Err(e) => {
eprintln!("Error: {}", e);
break;
}
}
}
Ok(())
}
Example: reqwest-eventsource with Authentication
use reqwest::{Client, header::{HeaderMap, HeaderValue, AUTHORIZATION}};
use reqwest_eventsource::EventSource;
async fn authenticated_sse_stream(url: &str, token: &str) -> EventSource {
// Option 1: URL parameter (current approach)
let url_with_token = format!("{}?token={}", url, token);
EventSource::get(&url_with_token)
// Option 2: Authorization header (better security)
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", token)).unwrap()
);
let client = Client::builder()
.default_headers(headers)
.build()
.unwrap();
EventSource::new(client.get(url))
}
Example: Axum Body to JSON (Phase 2)
use axum::body::Body;
use serde::de::DeserializeOwned;
async fn body_to_json<T: DeserializeOwned>(body: Body) -> Result<T, Box<dyn std::error::Error>> {
let bytes = axum::body::to_bytes(body, usize::MAX).await?;
let value = serde_json::from_slice(&bytes)?;
Ok(value)
}
// Usage in tests:
let response = app.oneshot(request).await?;
let json: MyStruct = body_to_json(response.into_body()).await?;
assert_eq!(json.field, expected_value);
References
Libraries
-
reqwest-eventsource: https://crates.io/crates/reqwest-eventsource
-
reqwest: https://crates.io/crates/reqwest
-
axum: https://crates.io/crates/axum
- Body utilities: https://docs.rs/axum/latest/axum/body/
Specifications
- Server-Sent Events: https://html.spec.whatwg.org/multipage/server-sent-events.html
- HTTP/1.1: https://httpwg.org/specs/rfc9110.html
Related Documentation
docs/dependency-deduplication.md- General dependency analysisdocs/dependency-deduplication-results.md- Phase 1 resultsdocs/api-sse-streaming.md- SSE implementation details (if exists)
Document Status: Ready for Implementation
Next Step: Implement Phase 1
Owner: Engineering Team
Last Updated: 2026-01-28