use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
use futures::StreamExt;
use buffa::encoding::{decode_varint, encode_varint, skip_field, Tag, WireType};
use buffa::{DecodeContext, DecodeError, DefaultInstance, Message, SizeCache};
use bytes::{Buf, BufMut, Bytes};
use foundation_core::synca::OnSignal;
use foundation_core::valtron::initialize_pool;
use foundation_core::valtron::valtron_test;
use foundation_http::native::server::{HttpServer, ServerConfig};
use foundation_http::shared::app::HttpApp;
use foundation_http::shared::serve::Serve;
use foundation_connectrpc::shared::codec::CodecFor;
use foundation_connectrpc::{
ConnectResult, ConnectRpcServe, HandlerOptions, IdempotencyLevel, JsonCodec, ProcedureCodecs,
Request, Response, Router,
};
#[derive(Clone, Default, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
struct TestMsg {
id: i32,
name: String,
}
impl DefaultInstance for TestMsg {
fn default_instance() -> &'static Self {
static INST: std::sync::OnceLock<TestMsg> = std::sync::OnceLock::new();
INST.get_or_init(TestMsg::default)
}
}
impl Message for TestMsg {
fn compute_size(&self, _c: &mut SizeCache) -> u32 {
0
}
fn write_to(&self, _c: &mut SizeCache, buf: &mut impl BufMut) {
Tag::new(1, WireType::Varint).encode(buf);
encode_varint(self.id as u64, buf);
Tag::new(2, WireType::LengthDelimited).encode(buf);
encode_varint(self.name.len() as u64, buf);
buf.put_slice(self.name.as_bytes());
}
fn merge_field(
&mut self,
tag: Tag,
buf: &mut impl Buf,
_c: DecodeContext<'_>,
) -> Result<(), DecodeError> {
match tag.field_number() {
1 => {
self.id = decode_varint(buf)? as i32;
Ok(())
}
2 => {
let len = decode_varint(buf)? as usize;
let mut b = vec![0u8; len];
buf.copy_to_slice(&mut b);
self.name = String::from_utf8(b).map_err(|_| DecodeError::InvalidUtf8)?;
Ok(())
}
_ => skip_field(tag, buf),
}
}
fn clear(&mut self) {
*self = Self::default();
}
}
const PROCEDURE: &str = "/test.EchoService/Echo";
fn echo_serve() -> Arc<dyn Serve> {
let mut router = Router::new();
router.unary(
PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, req: Request<TestMsg>| async move { Ok(Response::new(req.msg)) },
HandlerOptions::new().with_idempotency(IdempotencyLevel::NoSideEffects),
);
Arc::new(ConnectRpcServe::new(router.into_handler()))
}
fn read_response(stream: &mut TcpStream) -> (u16, Vec<u8>) {
let (status, head, mut rest) = read_head(stream);
if is_chunked(&head) {
let mut body = Vec::new();
while let Some(chunk) = read_chunk(stream, &mut rest) {
body.extend_from_slice(&chunk);
}
return (status, body);
}
let content_length = head
.lines()
.find_map(|l| {
let (k, v) = l.split_once(':')?;
(k.trim().eq_ignore_ascii_case("content-length"))
.then(|| v.trim().parse::<usize>().ok())
.flatten()
})
.unwrap_or_else(|| {
panic!(
"no content-length; raw head:\n{head}\n---body-so-far---\n{}",
String::from_utf8_lossy(&rest)
)
});
let mut tmp = [0u8; 1024];
while rest.len() < content_length {
let n = stream.read(&mut tmp).expect("read body");
assert!(n > 0, "connection closed before body completed");
rest.extend_from_slice(&tmp[..n]);
}
rest.truncate(content_length);
(status, rest)
}
fn read_head(stream: &mut TcpStream) -> (u16, String, Vec<u8>) {
let mut buf = Vec::new();
let mut tmp = [0u8; 1024];
let header_end = loop {
if let Some(pos) = find_subsequence(&buf, b"\r\n\r\n") {
break pos + 4;
}
let n = stream.read(&mut tmp).expect("read headers");
assert!(n > 0, "connection closed before headers completed");
buf.extend_from_slice(&tmp[..n]);
};
let head = String::from_utf8_lossy(&buf[..header_end]).to_string();
let status = head
.lines()
.next()
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|c| c.parse::<u16>().ok())
.expect("status line");
(status, head, buf[header_end..].to_vec())
}
fn is_chunked(head: &str) -> bool {
head.lines().any(|l| {
l.split_once(':').is_some_and(|(k, v)| {
k.trim().eq_ignore_ascii_case("transfer-encoding")
&& v.to_ascii_lowercase().contains("chunked")
})
})
}
fn read_chunk(stream: &mut TcpStream, rest: &mut Vec<u8>) -> Option<Vec<u8>> {
let mut tmp = [0u8; 4096];
let line_end = loop {
if let Some(pos) = find_subsequence(rest, b"\r\n") {
break pos;
}
let n = stream.read(&mut tmp).expect("read chunk size");
assert!(n > 0, "connection closed inside a chunk size line");
rest.extend_from_slice(&tmp[..n]);
};
let size_text = String::from_utf8_lossy(&rest[..line_end]).to_string();
let size = usize::from_str_radix(size_text.split(';').next().unwrap().trim(), 16)
.expect("chunk size");
let needed = line_end + 2 + size + 2;
while rest.len() < needed {
let n = stream.read(&mut tmp).expect("read chunk data");
assert!(n > 0, "connection closed inside a chunk");
rest.extend_from_slice(&tmp[..n]);
}
let data = rest[line_end + 2..line_end + 2 + size].to_vec();
rest.drain(..needed);
(size > 0).then_some(data)
}
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
#[test]
fn unary_round_trip_over_real_socket() {
let _guard = initialize_pool(41, Some(4));
let mut app = HttpApp::new_serve();
let serve = echo_serve();
app.router.add_route_any(PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let msg = TestMsg {
id: 99,
name: "socket".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let request = format!(
"POST {PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
json.len()
);
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
stream.write_all(request.as_bytes()).expect("write head");
stream.write_all(&json).expect("write body");
stream.flush().unwrap();
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "unary over socket returns 200");
let echoed: TestMsg = JsonCodec.unmarshal(Bytes::from(body)).expect("decode echo");
assert_eq!(echoed, msg, "server echoed the request over the wire");
shutdown.turn_on();
}
const LIST_PROCEDURE: &str = "/test.ListService/Items";
fn list_serve() -> Arc<dyn Serve> {
let mut router = Router::new();
router.server_stream(
LIST_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, req: Request<TestMsg>| async move {
let base = req.msg.id;
let items: Vec<ConnectResult<TestMsg>> = (0..3)
.map(|i| {
Ok(TestMsg {
id: base + i,
name: format!("item-{i}"),
})
})
.collect();
Ok(futures::stream::iter(items))
},
HandlerOptions::new(),
);
Arc::new(ConnectRpcServe::new(router.into_handler()))
}
#[test]
fn server_stream_over_real_socket() {
let _guard = initialize_pool(42, Some(4));
let mut app = HttpApp::new_serve();
let serve = list_serve();
app.router.add_route_any(LIST_PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let msg = TestMsg {
id: 10,
name: "seed".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let envelope = {
let mut buf = Vec::new();
buf.push(0u8);
buf.extend_from_slice(&(json.len() as u32).to_be_bytes());
buf.extend_from_slice(&json);
buf
};
let request = format!(
"POST {LIST_PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/connect+json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
envelope.len()
);
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
stream.write_all(request.as_bytes()).expect("write head");
stream.write_all(&envelope).expect("write body");
stream.flush().unwrap();
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "server-stream over socket returns 200");
let (messages, saw_end_stream) = parse_connect_envelope_stream(&body);
assert_eq!(messages.len(), 3, "three server-stream items");
assert!(saw_end_stream, "EndStream frame present");
for (i, msg_bytes) in messages.iter().enumerate() {
let item: TestMsg = JsonCodec
.unmarshal(Bytes::from(msg_bytes.clone()))
.expect("decode item");
assert_eq!(item.id, 10 + i as i32, "item {i} id");
assert_eq!(item.name, format!("item-{i}"), "item {i} name");
}
shutdown.turn_on();
}
const FLAG_END_STREAM: u8 = 0x02;
fn parse_connect_envelope_stream(body: &[u8]) -> (Vec<Vec<u8>>, bool) {
let mut messages = Vec::new();
let mut end = false;
let mut pos = 0;
while pos + 5 <= body.len() {
let flags = body[pos];
let len = u32::from_be_bytes([body[pos + 1], body[pos + 2], body[pos + 3], body[pos + 4]])
as usize;
let start = pos + 5;
let stop = (start + len).min(body.len());
let payload = body[start..stop].to_vec();
if flags & FLAG_END_STREAM != 0 {
end = true;
} else {
messages.push(payload);
}
pos = stop;
}
(messages, end)
}
fn connect_with_retry(addr: std::net::SocketAddr) -> TcpStream {
for _ in 0..50 {
if let Ok(s) = TcpStream::connect(addr) {
return s;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("could not connect to test server at {addr}");
}
#[test]
fn h1_transport_capabilities_are_correct() {
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::H1Transport;
use foundation_netio::http::NativeHttpClient;
use foundation_netio::shared::http::Proto;
let client = NativeHttpClient::from_system();
let transport = H1Transport::new(Arc::new(client));
let caps = transport.capabilities();
assert!(caps.request_streaming, "h1 supports chunked upload");
assert!(!caps.full_duplex, "h1 is half-duplex");
assert!(!caps.h2_trailers);
assert_eq!(caps.http_versions, &[Proto::HTTP11]);
assert!(!caps.multiplexed);
}
#[valtron_test(seed = 44, threads = 8)]
async fn h1_client_transport_over_real_socket() {
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::H1Transport;
use foundation_netio::http::NativeHttpClient;
use foundation_netio::shared::http::{
Proto, RequestDescriptor, SimpleHeaders, SimpleMethod, SimpleUrl,
};
use foundation_core::url::Uri;
let mut app = HttpApp::new_serve();
let serve = echo_serve();
app.router.add_route_any(PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let _ = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("server ready");
let transport = H1Transport::new(Arc::new(NativeHttpClient::from_system()));
let url = format!("http://127.0.0.1:{}{PROCEDURE}", addr.port());
let uri = Uri::parse(&url).expect("parse URL");
let descriptor = RequestDescriptor {
proto: Proto::HTTP11,
request_url: SimpleUrl::url_only(url.clone()),
request_uri: uri,
headers: {
let mut h = SimpleHeaders::new();
h.insert(
foundation_netio::shared::http::SimpleHeader::CONTENT_TYPE,
vec!["application/json".to_string()],
);
h
},
method: SimpleMethod::POST,
};
let stream = transport.open(descriptor).expect("open");
let mut head = stream.head;
let mut recv_body = stream.recv_body;
let msg = TestMsg {
id: 77,
name: "transport".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal");
stream
.send_body
.try_send(Bytes::from(json.to_vec()))
.expect("send request body");
stream.send_body.close();
let (status, _headers) = head
.next()
.await
.expect("response head chunk")
.expect("response head ok");
assert_eq!(status, foundation_netio::shared::http::Status::OK);
let body_bytes = recv_body
.next()
.await
.expect("response body chunk")
.expect("response body ok");
let echoed: TestMsg = JsonCodec.unmarshal(body_bytes).expect("decode echo");
assert_eq!(
echoed, msg,
"H1Transport: echoed message matches request over real socket"
);
shutdown.turn_on();
}
#[valtron_test(seed = 45, threads = 8)]
async fn h1_client_server_stream_over_real_socket() {
use foundation_connectrpc::shared::transport::Transport;
use foundation_connectrpc::H1Transport;
use foundation_core::url::Uri;
use foundation_netio::http::NativeHttpClient;
use foundation_netio::shared::http::{
Proto, RequestDescriptor, SimpleHeaders, SimpleMethod, SimpleUrl,
};
let mut app = HttpApp::new_serve();
let serve = list_serve();
app.router.add_route_any(LIST_PROCEDURE, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let _ = TcpStream::connect_timeout(&addr, Duration::from_secs(2)).expect("server ready");
let transport = H1Transport::new(Arc::new(NativeHttpClient::from_system()));
let url = format!("http://127.0.0.1:{}{LIST_PROCEDURE}", addr.port());
let uri = Uri::parse(&url).expect("parse URL");
let descriptor = RequestDescriptor {
proto: Proto::HTTP11,
request_url: SimpleUrl::url_only(url.clone()),
request_uri: uri,
headers: {
let mut h = SimpleHeaders::new();
h.insert(
foundation_netio::shared::http::SimpleHeader::CONTENT_TYPE,
vec!["application/connect+json".to_string()],
);
h
},
method: SimpleMethod::POST,
};
let stream = transport.open(descriptor).expect("open");
let mut head = stream.head;
let mut recv_body = stream.recv_body;
let seed = TestMsg {
id: 10,
name: "seed".to_string(),
};
let json = JsonCodec.marshal(&seed).expect("marshal").to_vec();
let mut envelope = Vec::with_capacity(5 + json.len());
envelope.push(0u8);
envelope.extend_from_slice(&(json.len() as u32).to_be_bytes());
envelope.extend_from_slice(&json);
stream
.send_body
.try_send(Bytes::from(envelope))
.expect("send request envelope");
stream.send_body.close();
let (status, _headers) = head
.next()
.await
.expect("response head chunk")
.expect("response head ok");
assert_eq!(status, foundation_netio::shared::http::Status::OK);
let mut body = Vec::new();
while let Some(chunk) = recv_body.next().await {
let bytes = chunk.expect("body chunk ok");
body.extend_from_slice(&bytes);
}
let (messages, saw_end_stream) = parse_connect_envelope_stream(&body);
assert_eq!(
messages.len(),
3,
"H1 client received all 3 server-stream items"
);
assert!(saw_end_stream, "H1 client saw the EndStream frame");
for (i, msg_bytes) in messages.iter().enumerate() {
let item: TestMsg = JsonCodec
.unmarshal(Bytes::from(msg_bytes.clone()))
.expect("decode item");
assert_eq!(item.id, 10 + i as i32, "item {i} id over H1 client");
assert_eq!(item.name, format!("item-{i}"), "item {i} name over H1 client");
}
shutdown.turn_on();
}
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::task::{Context, Poll};
use foundation_connectrpc::{Code, ConnectError};
fn start_server(route: &str, serve: Arc<dyn Serve>) -> (std::net::SocketAddr, Arc<OnSignal>) {
let mut app = HttpApp::new_serve();
app.router.add_route_any(route, &serve);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
(addr, shutdown)
}
fn envelope(flags: u8, payload: &[u8]) -> Vec<u8> {
let mut buf = Vec::with_capacity(5 + payload.len());
buf.push(flags);
buf.extend_from_slice(&(payload.len() as u32).to_be_bytes());
buf.extend_from_slice(payload);
buf
}
fn json_envelope(msg: &TestMsg) -> Vec<u8> {
envelope(0, &JsonCodec.marshal(msg).expect("marshal"))
}
fn read_envelope(
stream: &mut TcpStream,
rest: &mut Vec<u8>,
carry: &mut Vec<u8>,
) -> Option<(u8, Vec<u8>)> {
loop {
if carry.len() >= 5 {
let len = u32::from_be_bytes([carry[1], carry[2], carry[3], carry[4]]) as usize;
if carry.len() >= 5 + len {
let flags = carry[0];
let payload = carry[5..5 + len].to_vec();
carry.drain(..5 + len);
return Some((flags, payload));
}
}
let chunk = read_chunk(stream, rest)?;
carry.extend_from_slice(&chunk);
}
}
fn send_chunked_request(
stream: &mut TcpStream,
procedure: &str,
content_type: &str,
body: &[Vec<u8>],
) {
let head = format!(
"POST {procedure} HTTP/1.1\r\nHost: t\r\nContent-Type: {content_type}\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n"
);
stream.write_all(head.as_bytes()).expect("write head");
for piece in body {
write!(stream, "{:x}\r\n", piece.len()).expect("chunk size");
stream.write_all(piece).expect("chunk data");
stream.write_all(b"\r\n").expect("chunk end");
}
stream.write_all(b"0\r\n\r\n").expect("last chunk");
stream.flush().expect("flush");
}
struct WaitFor(Arc<AtomicBool>);
impl Future for WaitFor {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.0.load(Ordering::SeqCst) {
Poll::Ready(())
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
fn open_stream_request(addr: std::net::SocketAddr, procedure: &str) -> (TcpStream, Vec<u8>) {
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let seed = TestMsg {
id: 1,
name: "seed".to_string(),
};
send_chunked_request(
&mut stream,
procedure,
"application/connect+json",
&[json_envelope(&seed)],
);
let (status, head, rest) = read_head(&mut stream);
assert_eq!(status, 200, "stream opens with 200; head:\n{head}");
assert!(is_chunked(&head), "a stream is sent chunked; head:\n{head}");
(stream, rest)
}
const GATED_PROCEDURE: &str = "/test.StreamService/Gated";
#[test]
fn first_frame_arrives_before_the_handler_finishes() {
let _guard = initialize_pool(46, Some(4));
let released = Arc::new(AtomicBool::new(false));
let gate = released.clone();
let mut router = Router::new();
router.server_stream(
GATED_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
move |_ctx, _req: Request<TestMsg>| {
let gate = gate.clone();
async move {
Ok(futures::stream::unfold(0u8, move |step| {
let gate = gate.clone();
async move {
match step {
0 => Some((
Ok(TestMsg {
id: 0,
name: "first".into(),
}),
1,
)),
1 => {
WaitFor(gate).await;
Some((
Ok(TestMsg {
id: 1,
name: "second".into(),
}),
2,
))
}
_ => None,
}
}
}))
}
},
HandlerOptions::new(),
);
let (addr, shutdown) = start_server(
GATED_PROCEDURE,
Arc::new(ConnectRpcServe::new(router.into_handler())),
);
let (mut stream, mut rest) = open_stream_request(addr, GATED_PROCEDURE);
let mut carry = Vec::new();
let (flags, payload) = read_envelope(&mut stream, &mut rest, &mut carry)
.expect("first frame arrives while the handler waits");
assert_eq!(flags, 0);
let first: TestMsg = JsonCodec.unmarshal(Bytes::from(payload)).expect("decode");
assert_eq!(first.name, "first");
released.store(true, Ordering::SeqCst);
let (_, payload) = read_envelope(&mut stream, &mut rest, &mut carry).expect("second frame");
let second: TestMsg = JsonCodec.unmarshal(Bytes::from(payload)).expect("decode");
assert_eq!(second.name, "second");
let (flags, _) = read_envelope(&mut stream, &mut rest, &mut carry).expect("end of stream");
assert_eq!(flags & FLAG_END_STREAM, FLAG_END_STREAM);
assert!(
read_envelope(&mut stream, &mut rest, &mut carry).is_none(),
"body ends"
);
shutdown.turn_on();
}
const ENDLESS_PROCEDURE: &str = "/test.StreamService/Endless";
struct DropFlag(Arc<AtomicBool>);
impl Drop for DropFlag {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
#[test]
fn endless_stream_follows_the_client_and_stops_when_it_leaves() {
let _guard = initialize_pool(47, Some(4));
let produced = Arc::new(AtomicUsize::new(0));
let dropped = Arc::new(AtomicBool::new(false));
let big = "x".repeat(64 * 1024);
let (count, drop_flag) = (produced.clone(), dropped.clone());
let mut router = Router::new();
router.server_stream(
ENDLESS_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
move |_ctx, _req: Request<TestMsg>| {
let (count, drop_flag, big) = (count.clone(), drop_flag.clone(), big.clone());
async move {
let guard = DropFlag(drop_flag);
Ok(futures::stream::unfold(guard, move |guard| {
let n = count.fetch_add(1, Ordering::SeqCst);
let big = big.clone();
async move {
Some((
Ok(TestMsg {
id: n as i32,
name: big,
}),
guard,
))
}
}))
}
},
HandlerOptions::new(),
);
let (addr, shutdown) = start_server(
ENDLESS_PROCEDURE,
Arc::new(ConnectRpcServe::new(router.into_handler())),
);
let (mut stream, mut rest) = open_stream_request(addr, ENDLESS_PROCEDURE);
let mut carry = Vec::new();
for i in 0..5 {
let (flags, payload) =
read_envelope(&mut stream, &mut rest, &mut carry).expect("frame from endless stream");
assert_eq!(flags, 0);
let msg: TestMsg = JsonCodec.unmarshal(Bytes::from(payload)).expect("decode");
assert_eq!(msg.id, i, "frames arrive in order");
}
let window = Duration::from_millis(300);
let deadline = std::time::Instant::now() + Duration::from_secs(10);
let mut stalled_at = produced.load(Ordering::SeqCst);
loop {
std::thread::sleep(window);
let now = produced.load(Ordering::SeqCst);
if now == stalled_at {
break;
}
assert!(
std::time::Instant::now() < deadline,
"the producer is held back while the client is not reading; \
it was still producing ({stalled_at} -> {now}) after 10s"
);
stalled_at = now;
}
assert!(
stalled_at < 500,
"only a bounded number of frames is in flight, got {stalled_at}"
);
drop(stream);
let deadline = std::time::Instant::now() + Duration::from_secs(10);
while !dropped.load(Ordering::SeqCst) {
assert!(
std::time::Instant::now() < deadline,
"the handler's stream was not released after the client left"
);
std::thread::sleep(Duration::from_millis(20));
}
shutdown.turn_on();
}
const FAILING_PROCEDURE: &str = "/test.StreamService/FailsAfterOne";
#[test]
fn error_after_frames_is_sent_in_band() {
let _guard = initialize_pool(48, Some(4));
let mut router = Router::new();
router.server_stream(
FAILING_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, _req: Request<TestMsg>| async move {
let items: Vec<ConnectResult<TestMsg>> = vec![
Ok(TestMsg {
id: 1,
name: "ok".into(),
}),
Err(ConnectError::new(Code::ResourceExhausted, "out of widgets").into()),
];
Ok(futures::stream::iter(items))
},
HandlerOptions::new(),
);
let (addr, shutdown) = start_server(
FAILING_PROCEDURE,
Arc::new(ConnectRpcServe::new(router.into_handler())),
);
let (mut stream, mut rest) = open_stream_request(addr, FAILING_PROCEDURE);
let mut carry = Vec::new();
let (flags, _) = read_envelope(&mut stream, &mut rest, &mut carry).expect("data frame");
assert_eq!(flags, 0);
let (flags, payload) = read_envelope(&mut stream, &mut rest, &mut carry).expect("end frame");
assert_eq!(flags & FLAG_END_STREAM, FLAG_END_STREAM);
let end = String::from_utf8_lossy(&payload);
assert!(
end.contains("resource_exhausted"),
"end-stream carries the code: {end}"
);
assert!(
end.contains("out of widgets"),
"end-stream carries the message: {end}"
);
shutdown.turn_on();
}
const REFUSING_PROCEDURE: &str = "/test.StreamService/Refuses";
#[test]
fn error_before_any_frame_is_reported() {
let _guard = initialize_pool(49, Some(4));
let mut router = Router::new();
router.server_stream(
REFUSING_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, _req: Request<TestMsg>| async move {
Err::<futures::stream::Empty<ConnectResult<TestMsg>>, _>(
ConnectError::new(Code::PermissionDenied, "not yours").into(),
)
},
HandlerOptions::new(),
);
let (addr, shutdown) = start_server(
REFUSING_PROCEDURE,
Arc::new(ConnectRpcServe::new(router.into_handler())),
);
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let seed = TestMsg::default();
send_chunked_request(
&mut stream,
REFUSING_PROCEDURE,
"application/connect+json",
&[json_envelope(&seed)],
);
let (status, body) = read_response(&mut stream);
let text = String::from_utf8_lossy(&body);
assert!(
text.contains("permission_denied") && text.contains("not yours"),
"status {status}, body: {text}"
);
shutdown.turn_on();
}
#[test]
fn unary_request_body_in_many_chunks_arrives_whole() {
let _guard = initialize_pool(50, Some(4));
let (addr, shutdown) = start_server(PROCEDURE, echo_serve());
let msg = TestMsg {
id: 7,
name: "a name long enough to split into many small chunks".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let pieces: Vec<Vec<u8>> = json.chunks(3).map(<[u8]>::to_vec).collect();
assert!(pieces.len() > 10);
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
send_chunked_request(&mut stream, PROCEDURE, "application/json", &pieces);
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "body: {}", String::from_utf8_lossy(&body));
let echoed: TestMsg = JsonCodec.unmarshal(Bytes::from(body)).expect("decode echo");
assert_eq!(echoed, msg);
shutdown.turn_on();
}
const COUNT_PROCEDURE: &str = "/test.StreamService/Count";
#[test]
fn client_stream_in_many_chunks_delivers_every_message() {
let _guard = initialize_pool(51, Some(4));
let mut router = Router::new();
router.client_stream(
COUNT_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, mut reqs: foundation_connectrpc::RequestStream<TestMsg>| async move {
let mut count = 0;
while let Some(msg) = reqs.next().await {
msg?;
count += 1;
}
Ok(Response::new(TestMsg {
id: count,
name: "counted".into(),
}))
},
HandlerOptions::new(),
);
let (addr, shutdown) = start_server(
COUNT_PROCEDURE,
Arc::new(ConnectRpcServe::new(router.into_handler())),
);
let pieces: Vec<Vec<u8>> = (0..12)
.map(|i| {
json_envelope(&TestMsg {
id: i,
name: format!("m{i}"),
})
})
.collect();
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
send_chunked_request(
&mut stream,
COUNT_PROCEDURE,
"application/connect+json",
&pieces,
);
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200);
let (messages, saw_end) = parse_connect_envelope_stream(&body);
assert!(saw_end, "end-stream frame present");
assert_eq!(messages.len(), 1);
let reply: TestMsg = JsonCodec
.unmarshal(Bytes::from(messages[0].clone()))
.expect("decode");
assert_eq!(reply.id, 12, "every message reached the handler");
shutdown.turn_on();
}
fn send_slowly(stream: &mut TcpStream, head: &str, pieces: &[Vec<u8>], pause: Duration) {
stream.write_all(head.as_bytes()).expect("write head");
stream.flush().expect("flush head");
for piece in pieces {
std::thread::sleep(pause);
stream.write_all(piece).expect("write piece");
stream.flush().expect("flush piece");
}
}
#[test]
fn client_stream_body_arriving_slowly_is_waited_for() {
let _guard = initialize_pool(52, Some(4));
let mut router = Router::new();
router.client_stream(
COUNT_PROCEDURE,
ProcedureCodecs::<TestMsg, TestMsg>::defaults(),
|_ctx, mut reqs: foundation_connectrpc::RequestStream<TestMsg>| async move {
let mut count = 0;
while let Some(msg) = reqs.next().await {
msg?;
count += 1;
}
Ok(Response::new(TestMsg {
id: count,
name: "counted".into(),
}))
},
HandlerOptions::new(),
);
let (addr, shutdown) = start_server(
COUNT_PROCEDURE,
Arc::new(ConnectRpcServe::new(router.into_handler())),
);
let mut pieces: Vec<Vec<u8>> = (0..5)
.map(|i| {
let env = json_envelope(&TestMsg {
id: i,
name: format!("m{i}"),
});
let mut chunk = format!("{:x}\r\n", env.len()).into_bytes();
chunk.extend_from_slice(&env);
chunk.extend_from_slice(b"\r\n");
chunk
})
.collect();
pieces.push(b"0\r\n\r\n".to_vec());
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let head = format!(
"POST {COUNT_PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/connect+json\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n"
);
send_slowly(&mut stream, &head, &pieces, Duration::from_millis(50));
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "body: {}", String::from_utf8_lossy(&body));
let (messages, saw_end) = parse_connect_envelope_stream(&body);
assert!(saw_end, "end-stream frame present");
assert_eq!(messages.len(), 1);
let reply: TestMsg = JsonCodec
.unmarshal(Bytes::from(messages[0].clone()))
.expect("decode");
assert_eq!(reply.id, 5, "every message reached the handler");
shutdown.turn_on();
}
#[test]
fn unary_body_arriving_slowly_is_waited_for() {
let _guard = initialize_pool(53, Some(4));
let (addr, shutdown) = start_server(PROCEDURE, echo_serve());
let msg = TestMsg {
id: 9,
name: "sent a few bytes at a time".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let pieces: Vec<Vec<u8>> = json.chunks(8).map(<[u8]>::to_vec).collect();
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let head = format!(
"POST {PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
json.len()
);
send_slowly(&mut stream, &head, &pieces, Duration::from_millis(50));
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200, "body: {}", String::from_utf8_lossy(&body));
let echoed: TestMsg = JsonCodec.unmarshal(Bytes::from(body)).expect("decode echo");
assert_eq!(echoed, msg);
shutdown.turn_on();
}
fn keep_alive_unary(stream: &mut TcpStream, id: i32) {
let msg = TestMsg {
id,
name: "again".to_string(),
};
let json = JsonCodec.marshal(&msg).expect("marshal").to_vec();
let head = format!(
"POST {PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n",
json.len()
);
stream.write_all(head.as_bytes()).expect("write head");
stream.write_all(&json).expect("write body");
let (status, body) = read_response(stream);
assert_eq!(status, 200, "body: {}", String::from_utf8_lossy(&body));
let echoed: TestMsg = JsonCodec.unmarshal(Bytes::from(body)).expect("decode echo");
assert_eq!(echoed, msg);
}
#[test]
fn keep_alive_connection_serves_unary_and_streams_in_turn() {
let _guard = initialize_pool(54, Some(4));
let mut app = HttpApp::new_serve();
let echo = echo_serve();
let list = list_serve();
app.router.add_route_any(PROCEDURE, &echo);
app.router.add_route_any(LIST_PROCEDURE, &list);
let shutdown = Arc::new(OnSignal::new());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let addr = listener.local_addr().expect("addr");
let server = HttpServer::with_config(
app,
&format!("127.0.0.1:{}", addr.port()),
ServerConfig::defaults(),
);
let shutdown_thread = shutdown.clone();
std::thread::spawn(move || server.serve_with_listener(&listener, &shutdown_thread));
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
keep_alive_unary(&mut stream, 1);
let seed = json_envelope(&TestMsg {
id: 10,
name: "seed".to_string(),
});
let head = format!(
"POST {LIST_PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: application/connect+json\r\nContent-Length: {}\r\n\r\n",
seed.len()
);
stream.write_all(head.as_bytes()).expect("write head");
stream.write_all(&seed).expect("write body");
let (status, body) = read_response(&mut stream);
assert_eq!(status, 200);
let (messages, saw_end) = parse_connect_envelope_stream(&body);
assert_eq!(messages.len(), 3);
assert!(saw_end);
keep_alive_unary(&mut stream, 2);
shutdown.turn_on();
}
#[test]
fn rejected_request_with_unread_body_closes_the_connection() {
let _guard = initialize_pool(55, Some(4));
let (addr, shutdown) = start_server(PROCEDURE, echo_serve());
let mut stream = connect_with_retry(addr);
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let body = b"GET /smuggled HTTP/1.1\r\nHost: t\r\n\r\n";
let head = format!(
"POST {PROCEDURE} HTTP/1.1\r\nHost: t\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n",
body.len()
);
stream.write_all(head.as_bytes()).expect("write head");
stream.write_all(body).expect("write body");
let (status, _) = read_response(&mut stream);
assert_eq!(status, 415);
let mut rest = Vec::new();
let read = stream.read_to_end(&mut rest);
assert!(
read.is_ok() && rest.is_empty(),
"the connection closes after the response and nothing else is answered: {read:?}, {}",
String::from_utf8_lossy(&rest)
);
shutdown.turn_on();
}