app-server-test-client/src/loopback_responses_server.rs
145 lines
use anyhow::Context;use anyhow::Result;use std::io;use std::io::Read;use std::io::Write;use std::net::TcpListener;use std::net::TcpStream;use std::sync::Arc;use std::sync::atomic::AtomicBool;use std::sync::atomic::Ordering;use std::thread;use std::thread::JoinHandle;use std::time::Duration;pub(super) struct LoopbackResponsesServer { base_url: String, shutdown: Arc<AtomicBool>, thread: Option<JoinHandle<()>>,}impl LoopbackResponsesServer { pub(super) fn start() -> Result<Self> { let listener = TcpListener::bind("127.0.0.1:0").context("bind loopback Responses API server")?; listener .set_nonblocking(true) .context("set loopback Responses API server nonblocking")?; let address = listener.local_addr()?; let shutdown = Arc::new(AtomicBool::new(false)); let thread_shutdown = Arc::clone(&shutdown); let thread = thread::spawn(move || { while !thread_shutdown.load(Ordering::Relaxed) { match listener.accept() { Ok((stream, _)) => { if let Err(err) = handle_model_connection(stream) { eprintln!("loopback Responses API server error: {err}"); } } Err(err) if err.kind() == io::ErrorKind::WouldBlock => { thread::sleep(Duration::from_millis(10)); } Err(err) => { eprintln!("loopback Responses API accept error: {err}"); break; } } } }); Ok(Self { base_url: format!("http://{address}"), shutdown, thread: Some(thread), }) } pub(super) fn base_url(&self) -> &str { &self.base_url }}impl Drop for LoopbackResponsesServer { fn drop(&mut self) { self.shutdown.store(true, Ordering::Relaxed); if let Some(thread) = self.thread.take() { let _ = thread.join(); } }}fn handle_model_connection(mut stream: TcpStream) -> io::Result<()> { stream.set_nonblocking(false)?; stream.set_read_timeout(Some(Duration::from_secs(2)))?; let request = read_http_request(&mut stream)?; let request_line = request .split(|byte| *byte == b'\n') .next() .and_then(|line| std::str::from_utf8(line).ok()) .unwrap_or_default(); if request_line.starts_with("POST ") && request_line.contains("/responses ") { let body = concat!( "event: response.created\n", "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp-plugin-analytics\"}}\n\n", "event: response.completed\n", "data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp-plugin-analytics\",\"usage\":{\"input_tokens\":0,\"input_tokens_details\":null,\"output_tokens\":0,\"output_tokens_details\":null,\"total_tokens\":0}}}\n\n" ); write_http_response(&mut stream, "200 OK", "text/event-stream", body) } else { write_http_response( &mut stream, "404 Not Found", "application/json", r#"{"error":"not found"}"#, ) }}fn read_http_request(stream: &mut TcpStream) -> io::Result<Vec<u8>> { let mut request = Vec::new(); let mut buffer = [0_u8; 4096]; let header_end = loop { let read = stream.read(&mut buffer)?; if read == 0 { return Ok(request); } request.extend_from_slice(&buffer[..read]); if let Some(position) = request.windows(4).position(|window| window == b"\r\n\r\n") { break position + 4; } }; let content_length = parse_content_length(&request[..header_end]); while request.len() < header_end + content_length { let read = stream.read(&mut buffer)?; if read == 0 { break; } request.extend_from_slice(&buffer[..read]); } Ok(request)}fn parse_content_length(headers: &[u8]) -> usize { String::from_utf8_lossy(headers) .lines() .find_map(|line| { let (name, value) = line.split_once(':')?; name.eq_ignore_ascii_case("content-length") .then(|| value.trim().parse().ok()) .flatten() }) .unwrap_or(0)}fn write_http_response( stream: &mut TcpStream, status: &str, content_type: &str, body: &str,) -> io::Result<()> { write!( stream, "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() )?; stream.flush()}