use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::cx::Cx;
use crate::http::{HttpClient, Method, Request, Response};
use crate::observability::otel::{OtlpError, OtlpHttpExporter, TraceSpan};
use crate::time::Instant;
use crate::types::{Outcome, TraceId};
#[derive(Clone)]
struct Scripted100HttpClient {
responses: Arc<Mutex<Vec<Response>>>,
request_log: Arc<Mutex<Vec<(Method, String)>>>,
}
impl Scripted100HttpClient {
fn new(responses: Vec<Response>) -> Self {
Self {
responses: Arc::new(Mutex::new(responses)),
request_log: Arc::new(Mutex::new(Vec::new())),
}
}
fn request_count(&self) -> usize {
self.request_log.lock().unwrap().len()
}
fn get_requests(&self) -> Vec<(Method, String)> {
self.request_log.lock().unwrap().clone()
}
}
impl HttpClient for Scripted100HttpClient {
async fn request(
&self,
_cx: &Cx,
method: Method,
url: &str,
_headers: HashMap<String, String>,
_body: Vec<u8>,
) -> Result<Response, crate::http::HttpError> {
self.request_log
.lock()
.unwrap()
.push((method, url.to_string()));
let response = self
.responses
.lock()
.unwrap()
.pop()
.unwrap_or_else(|| Response {
status: 100,
headers: vec![],
body: b"Continue".to_vec(),
});
Ok(response)
}
}
fn create_test_span() -> TraceSpan {
TraceSpan {
trace_id: TraceId::new(),
span_id: [1, 2, 3, 4, 5, 6, 7, 8],
parent_span_id: None,
name: "test_span".to_string(),
start_time: Instant::now(),
end_time: Some(Instant::now()),
status_code: 0,
status_message: None,
attributes: HashMap::new(),
events: Vec::new(),
links: Vec::new(),
resource_attributes: HashMap::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_100_continue_incorrectly_treated_as_terminal() {
eprintln!("\n🚨 BUG AUDIT: HTTP 100 Continue Handling");
eprintln!("=========================================");
let scripted_client = Scripted100HttpClient::new(vec![Response {
status: 100,
headers: vec![],
body: b"Continue".to_vec(),
}]);
let exporter = OtlpHttpExporter::new(
"http://localhost:4318/v1/traces".to_string(),
HashMap::new(),
Duration::from_secs(30),
scripted_client.clone(),
)
.expect("Failed to create OTLP exporter");
let cx = Cx::for_testing();
let spans = vec![create_test_span()];
let result = cx.block_on(async { exporter.export_spans(&cx, &spans).await });
eprintln!("📊 Current Behavior (INCORRECT):");
assert!(
result.is_err(),
"100 Continue should not cause terminal error"
);
let error = result.unwrap_err();
match error {
OtlpError::NonRetryable(message) => {
assert!(
message.contains("100"),
"Error message should contain 100 status: {}",
message
);
assert!(
message.contains("Unexpected OTLP response status"),
"Should show as unexpected: {}",
message
);
eprintln!(" Classification: NonRetryable (WRONG)");
eprintln!(" Error message: {}", message);
eprintln!(" Problem: 100 Continue treated as terminal error");
eprintln!(" Impact: Breaks OTLP export when Expect: 100-continue is used");
}
_ => panic!(
"Expected NonRetryable error for current implementation, got: {:?}",
error
),
}
eprintln!("\n✅ Expected Behavior (CORRECT):");
eprintln!(" Classification: Should wait for final response");
eprintln!(" Error handling: No error on 100 Continue");
eprintln!(" HTTP client: Should handle 1xx transparently");
eprintln!(" Application: Should only see final 2xx/3xx/4xx/5xx response");
assert_eq!(scripted_client.request_count(), 1);
}
#[test]
fn test_informational_response_classification() {
struct InformationalTest {
status: u16,
name: &'static str,
description: &'static str,
should_be_terminal: bool,
}
let informational_codes = vec![
InformationalTest {
status: 100,
name: "Continue",
description: "Server received headers, client should send body",
should_be_terminal: false, },
InformationalTest {
status: 101,
name: "Switching Protocols",
description: "Server switching protocols per client request",
should_be_terminal: false, },
InformationalTest {
status: 102,
name: "Processing",
description: "Server processing request, will send final response later",
should_be_terminal: false, },
InformationalTest {
status: 103,
name: "Early Hints",
description: "Server providing early hints before final response",
should_be_terminal: false, },
];
eprintln!("\n🧪 INFORMATIONAL RESPONSE (1XX) CLASSIFICATION TEST");
eprintln!("===================================================");
for test_case in informational_codes {
let scripted_client = Scripted100HttpClient::new(vec![Response {
status: test_case.status,
headers: vec![],
body: test_case.name.as_bytes().to_vec(),
}]);
let exporter = OtlpHttpExporter::new(
"http://localhost:4318/v1/traces".to_string(),
HashMap::new(),
Duration::from_secs(30),
scripted_client.clone(),
)
.expect("Failed to create OTLP exporter");
let cx = Cx::for_testing();
let spans = vec![create_test_span()];
let result = cx.block_on(async { exporter.export_spans(&cx, &spans).await });
eprintln!(
"\n📊 {} {} ({}):",
test_case.status, test_case.name, test_case.description
);
match result.unwrap_err() {
OtlpError::NonRetryable(message) => {
if test_case.should_be_terminal {
eprintln!(" Current: Terminal ✓ (correct)");
} else {
eprintln!(" Current: Terminal ❌ (BUG - should wait for final response)");
eprintln!(" Error: {}", message);
}
}
other => {
eprintln!(" Unexpected error type: {:?}", other);
}
}
}
eprintln!("\n🚨 BUG SUMMARY:");
eprintln!(" ALL 1xx responses incorrectly treated as terminal");
eprintln!(" Expected: Wait for final response (2xx/3xx/4xx/5xx)");
eprintln!(" Impact: Breaks legitimate HTTP/1.1 informational responses");
}
#[test]
fn test_http_continue_workflow_expectation() {
eprintln!("\n📋 HTTP 100 CONTINUE WORKFLOW SPECIFICATION");
eprintln!("============================================");
eprintln!("Per RFC 9110, proper HTTP 100 Continue workflow:");
eprintln!("");
eprintln!("1. Client sends request with 'Expect: 100-continue' header");
eprintln!("2. Server responds with '100 Continue' (informational)");
eprintln!("3. Client sends request body");
eprintln!("4. Server responds with final status (200 OK, 400 Bad Request, etc.)");
eprintln!("5. HTTP client library returns ONLY the final response");
eprintln!("");
eprintln!("🎯 CORRECT IMPLEMENTATION:");
eprintln!(" • HTTP client handles 1xx responses transparently");
eprintln!(" • Application layer only sees final response");
eprintln!(" • OTLP exporter processes final response normally");
eprintln!("");
eprintln!("🚨 CURRENT BUG:");
eprintln!(" • HTTP client exposes 100 Continue to application");
eprintln!(" • OTLP exporter treats 100 as terminal error");
eprintln!(" • Export fails instead of completing normally");
let scripted_client = Scripted100HttpClient::new(vec![Response {
status: 100,
headers: vec![],
body: b"Continue".to_vec(),
}]);
let exporter = OtlpHttpExporter::new(
"http://localhost:4318/v1/traces".to_string(),
HashMap::new(),
Duration::from_secs(30),
scripted_client.clone(),
)
.expect("Failed to create OTLP exporter");
let cx = Cx::for_testing();
let spans = vec![create_test_span()];
let result = cx.block_on(async { exporter.export_spans(&cx, &spans).await });
match result.unwrap_err() {
OtlpError::NonRetryable(message) => {
eprintln!("\n💡 FIX RECOMMENDATIONS:");
eprintln!("======================");
eprintln!("");
eprintln!("Option 1: HTTP Client Fix (PREFERRED)");
eprintln!(" • Upgrade HTTP client to handle 1xx responses transparently");
eprintln!(" • Client waits for final response automatically");
eprintln!(" • Application only sees 2xx/3xx/4xx/5xx responses");
eprintln!("");
eprintln!("Option 2: Application Layer Fix");
eprintln!(" • Add specific handling for 1xx responses");
eprintln!(" • Wait for final response when 1xx received");
eprintln!(" • More complex but provides explicit control");
eprintln!("");
eprintln!("Current error: {}", message);
eprintln!("\n🎯 IMPLEMENTATION GUIDANCE:");
eprintln!(" 1. Check HTTP client documentation for 1xx handling");
eprintln!(" 2. If client exposes 1xx, add proper handling");
eprintln!(" 3. Test with real servers that send 100 Continue");
eprintln!(" 4. Ensure OTLP export succeeds with large payloads");
}
other => panic!(
"Expected NonRetryable for current implementation, got: {:?}",
other
),
}
}
#[test]
fn test_100_continue_vs_final_responses() {
eprintln!("\n🔄 100 CONTINUE VS FINAL RESPONSES");
eprintln!("==================================");
struct ResponseTest {
status: u16,
description: &'static str,
expected_behavior: &'static str,
}
let response_tests = vec![
ResponseTest {
status: 100,
description: "Continue (informational)",
expected_behavior: "Should wait for final response",
},
ResponseTest {
status: 200,
description: "OK (success)",
expected_behavior: "Should succeed",
},
ResponseTest {
status: 400,
description: "Bad Request (client error)",
expected_behavior: "Should fail terminal",
},
ResponseTest {
status: 500,
description: "Internal Server Error (server error)",
expected_behavior: "Should fail terminal",
},
];
for test_case in response_tests {
let scripted_client = Scripted100HttpClient::new(vec![Response {
status: test_case.status,
headers: vec![],
body: test_case.description.as_bytes().to_vec(),
}]);
let exporter = OtlpHttpExporter::new(
"http://localhost:4318/v1/traces".to_string(),
HashMap::new(),
Duration::from_secs(30),
scripted_client.clone(),
)
.expect("Failed to create OTLP exporter");
let cx = Cx::for_testing();
let spans = vec![create_test_span()];
let result = cx.block_on(async { exporter.export_spans(&cx, &spans).await });
eprintln!("\n📊 {} {}:", test_case.status, test_case.description);
eprintln!(" Expected: {}", test_case.expected_behavior);
match result {
Ok(()) => {
eprintln!(" Actual: Success ✅");
assert_eq!(test_case.status, 200, "Only 200 should succeed");
}
Err(OtlpError::NonRetryable(message)) => {
eprintln!(" Actual: Terminal error");
if test_case.status == 100 {
eprintln!(" Status: ❌ BUG (should wait for final)");
} else {
eprintln!(" Status: ✅ Correct (terminal for final error)");
}
eprintln!(" Message: {}", message);
}
Err(other) => {
eprintln!(" Actual: {:?}", other);
}
}
}
eprintln!("\n✅ SUMMARY:");
eprintln!(" Final responses (2xx/4xx/5xx): Handled correctly");
eprintln!(" Informational responses (1xx): BUG - treated as terminal");
}
#[test]
fn test_rfc_9110_compliance_for_informational_responses() {
eprintln!("\n📋 RFC 9110 INFORMATIONAL RESPONSE COMPLIANCE");
eprintln!("=============================================");
eprintln!("Per RFC 9110 Section 15.2:");
eprintln!(" • 1xx responses are informational");
eprintln!(" • They indicate interim status while request is processed");
eprintln!(" • Client MUST be prepared to receive one or more 1xx responses");
eprintln!(" • Client MUST ignore unexpected 1xx responses");
eprintln!(" • Final response will follow after any 1xx responses");
let scripted_client = Scripted100HttpClient::new(vec![Response {
status: 100,
headers: vec![],
body: b"Continue - send request body".to_vec(),
}]);
let exporter = OtlpHttpExporter::new(
"http://localhost:4318/v1/traces".to_string(),
HashMap::new(),
Duration::from_secs(30),
scripted_client.clone(),
)
.expect("Failed to create OTLP exporter");
let cx = Cx::for_testing();
let spans = vec![create_test_span()];
let result = cx.block_on(async { exporter.export_spans(&cx, &spans).await });
match result.unwrap_err() {
OtlpError::NonRetryable(message) => {
eprintln!("\n🚨 RFC 9110 COMPLIANCE VIOLATION:");
eprintln!(" Current: Treats 1xx as unexpected terminal error");
eprintln!(" Required: Must ignore or wait for final response");
eprintln!(" Impact: Breaks legitimate HTTP/1.1 communication");
eprintln!(" Error: {}", message);
eprintln!("\n✅ COMPLIANT BEHAVIOR WOULD BE:");
eprintln!(" • Receive 100 Continue");
eprintln!(" • Continue waiting for final response");
eprintln!(" • Process final response (200, 400, 500, etc.) normally");
eprintln!(" • Export succeeds or fails based on final response");
eprintln!("\n🔧 IMPLEMENTATION REQUIREMENTS:");
eprintln!(" 1. Add explicit 1xx handling to status code match");
eprintln!(" 2. Wait for final response when 1xx received");
eprintln!(" 3. OR ensure HTTP client handles 1xx transparently");
eprintln!(" 4. Test with servers that use Expect: 100-continue");
}
other => panic!(
"Expected NonRetryable for current implementation, got: {:?}",
other
),
}
eprintln!("\n🎯 PRIORITY: MEDIUM");
eprintln!(" Affects: OTLP clients using large request bodies");
eprintln!(" Servers: Those implementing Expect: 100-continue optimization");
eprintln!(" Fix complexity: Low (add 1xx case to status match)");
}
}