Files
attune/docs/dependencies/http-client-consolidation-plan.md
2026-02-04 17:46:30 -06:00

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

  1. Reqwest vs Hyper - Relationship & Usage
  2. EventSource Client Analysis
  3. Test Helpers Using http-body-util
  4. Rustls Version Conflicts
  5. JsonSchema & Reqwest 0.12 vs 0.13
  6. Implementation Plan
  7. Expected Results
  8. Risks & Mitigations
  9. Testing Plan
  10. Timeline & Effort
  11. Recommendation
  12. 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 calls
  • attune-api: Outbound HTTP requests
  • attune-cli: API client for talking to Attune API
  • attune-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 functionality
  • test_sse_stream_filters_by_execution_id - Filtering
  • test_sse_stream_requires_authentication - Auth
  • test_sse_stream_all_executions - Multi-execution streaming
  • test_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

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

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

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

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-eventsource uses reqwest 0.12+
  • reqwest 0.13 uses modern rustls 0.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:

  • jsonschema uses reqwest to 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

Rationale:

  • jsonschema maintainers 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

[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:

  1. Where is jsonschema actually used?
  2. Can we replace it with alternatives?
  3. 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:

  1. Audit jsonschema usage: grep -r "jsonschema" crates/
  2. Determine if removable
  3. If yes: Remove and implement alternative
  4. 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

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_updates
  • test_sse_stream_filters_by_execution_id
  • test_sse_stream_requires_authentication
  • test_sse_stream_all_executions
  • test_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 hyper 0.14 dependency tree (~8-10 crates)
  • Removes rustls 0.21 dependency tree (~5-7 crates)
  • Removes http 0.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 hyper dependency 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:

  • reqwest uses it internally
  • axum uses 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 validator crate (we already have it)
    • Use manual serde validation
    • Generate validation code at compile time

If used for schema generation:

  • We already have schemars for 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-eventsource supports 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 hyper 0.14 ecosystem
  • Removes rustls 0.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

  1. No hyper 0.14.x in cargo tree
  2. No rustls 0.21.x in cargo tree
  3. No http 0.2.x in cargo tree
  4. All 5 SSE tests pass consistently
  5. SSE streaming works in E2E environment
  6. Binary sizes reduced by 3-5 MB each
  7. Compilation time reduced by 20-40 seconds
  8. SBOM reduced by 15-20 entries

After Phase 2

  1. No direct hyper dependency in any Cargo.toml
  2. No http-body-util dependency
  3. All tests still pass
  4. Test helpers work correctly
  5. cargo tree | grep hyper shows only transitive (via reqwest/axum)

After Phase 3 (If Implemented)

  1. Only one version of reqwest in cargo tree (0.13)
  2. No jsonschema dependency (if removed)
  3. Alternative validation works correctly
  4. All validation tests pass
  5. 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

Specifications

  • docs/dependency-deduplication.md - General dependency analysis
  • docs/dependency-deduplication-results.md - Phase 1 results
  • docs/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