@@ -221,6 +221,170 @@ async fn test_serve_unix_accepts_http_post() {
221221 assert ! ( response. contains( "test-conn-1.0.0" ) ) ;
222222}
223223
224+ #[ tokio:: test]
225+ async fn test_tcp_http_keepalive_multi_request ( ) {
226+ let handler = Arc :: new ( test_handler ( ) ) ;
227+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . expect ( "bind" ) ;
228+ let addr = listener. local_addr ( ) . expect ( "addr" ) ;
229+
230+ let server_handler = Arc :: clone ( & handler) ;
231+ let _server = tokio:: spawn ( async move {
232+ let ( stream, _) = listener. accept ( ) . await . expect ( "accept" ) ;
233+ handle_tcp_connection ( server_handler, stream) . await . expect ( "ok" ) ;
234+ } ) ;
235+
236+ let mut client = TcpStream :: connect ( addr) . await . expect ( "connect" ) ;
237+
238+ // First request (keep-alive default)
239+ let body1 = r#"{"jsonrpc":"2.0","method":"toadstool.health","id":1}"# ;
240+ let http1 = format ! (
241+ "POST /rpc HTTP/1.1\r \n Content-Type: application/json\r \n Content-Length: {}\r \n \r \n {}" ,
242+ body1. len( ) ,
243+ body1
244+ ) ;
245+ client. write_all ( http1. as_bytes ( ) ) . await . expect ( "write1" ) ;
246+
247+ let mut buf = vec ! [ 0u8 ; 4096 ] ;
248+ let n = client. read ( & mut buf) . await . expect ( "read1" ) ;
249+ let resp1 = String :: from_utf8_lossy ( & buf[ ..n] ) ;
250+ assert ! ( resp1. contains( "HTTP/1.1 200 OK" ) , "first response ok" ) ;
251+ assert ! (
252+ resp1. contains( "Connection: keep-alive" ) ,
253+ "keep-alive header present"
254+ ) ;
255+ assert ! ( resp1. contains( "healthy" ) , "first response has health data" ) ;
256+
257+ // Second request on same connection (Connection: close to end)
258+ let body2 = r#"{"jsonrpc":"2.0","method":"toadstool.version","id":2}"# ;
259+ let http2 = format ! (
260+ "POST /rpc HTTP/1.1\r \n Content-Type: application/json\r \n Content-Length: {}\r \n Connection: close\r \n \r \n {}" ,
261+ body2. len( ) ,
262+ body2
263+ ) ;
264+ client. write_all ( http2. as_bytes ( ) ) . await . expect ( "write2" ) ;
265+
266+ let mut buf2 = Vec :: new ( ) ;
267+ client. read_to_end ( & mut buf2) . await . expect ( "read2" ) ;
268+ let resp2 = String :: from_utf8_lossy ( & buf2) ;
269+ assert ! ( resp2. contains( "HTTP/1.1 200 OK" ) , "second response ok" ) ;
270+ assert ! (
271+ resp2. contains( "Connection: close" ) ,
272+ "close header on final response"
273+ ) ;
274+ }
275+
276+ #[ tokio:: test]
277+ async fn test_unix_http_keepalive_multi_request ( ) {
278+ let handler = Arc :: new ( test_handler ( ) ) ;
279+ let dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
280+ let socket_path = dir. path ( ) . join ( "keepalive.sock" ) ;
281+
282+ let server_handler = Arc :: clone ( & handler) ;
283+ let sock_path = socket_path. clone ( ) ;
284+ let _server = tokio:: spawn ( async move {
285+ serve_unix ( server_handler, sock_path) . await . expect ( "serve" ) ;
286+ } ) ;
287+
288+ let mut stream = await_unix_socket ( & socket_path) . await ;
289+
290+ // First request (keep-alive)
291+ let body1 = r#"{"jsonrpc":"2.0","method":"toadstool.health","id":1}"# ;
292+ let http1 = format ! (
293+ "POST /rpc HTTP/1.1\r \n Content-Type: application/json\r \n Content-Length: {}\r \n \r \n {}" ,
294+ body1. len( ) ,
295+ body1
296+ ) ;
297+ stream. write_all ( http1. as_bytes ( ) ) . await . expect ( "write1" ) ;
298+
299+ let mut buf = vec ! [ 0u8 ; 4096 ] ;
300+ let n = stream. read ( & mut buf) . await . expect ( "read1" ) ;
301+ let resp1 = String :: from_utf8_lossy ( & buf[ ..n] ) ;
302+ assert ! ( resp1. contains( "Connection: keep-alive" ) ) ;
303+
304+ // Second request on same connection
305+ let body2 = r#"{"jsonrpc":"2.0","method":"toadstool.version","id":2}"# ;
306+ let http2 = format ! (
307+ "POST /rpc HTTP/1.1\r \n Content-Type: application/json\r \n Content-Length: {}\r \n Connection: close\r \n \r \n {}" ,
308+ body2. len( ) ,
309+ body2
310+ ) ;
311+ stream. write_all ( http2. as_bytes ( ) ) . await . expect ( "write2" ) ;
312+
313+ let mut buf2 = Vec :: new ( ) ;
314+ stream. read_to_end ( & mut buf2) . await . expect ( "read2" ) ;
315+ let resp2 = String :: from_utf8_lossy ( & buf2) ;
316+ assert ! ( resp2. contains( "HTTP/1.1 200 OK" ) ) ;
317+ assert ! ( resp2. contains( "Connection: close" ) ) ;
318+ }
319+
320+ #[ tokio:: test]
321+ async fn test_ndjson_with_blank_lines_between_requests ( ) {
322+ let handler = Arc :: new ( test_handler ( ) ) ;
323+ let listener = TcpListener :: bind ( "127.0.0.1:0" ) . await . expect ( "bind" ) ;
324+ let addr = listener. local_addr ( ) . expect ( "addr" ) ;
325+
326+ let server_handler = Arc :: clone ( & handler) ;
327+ let _server = tokio:: spawn ( async move {
328+ let ( stream, _) = listener. accept ( ) . await . expect ( "accept" ) ;
329+ handle_tcp_connection ( server_handler, stream) . await . expect ( "ok" ) ;
330+ } ) ;
331+
332+ let mut client = TcpStream :: connect ( addr) . await . expect ( "connect" ) ;
333+
334+ // Send two NDJSON requests with a blank line between them
335+ let requests = concat ! (
336+ "{\" jsonrpc\" :\" 2.0\" ,\" method\" :\" toadstool.health\" ,\" id\" :1}\n " ,
337+ "\n " ,
338+ "{\" jsonrpc\" :\" 2.0\" ,\" method\" :\" toadstool.version\" ,\" id\" :2}\n " ,
339+ ) ;
340+ client. write_all ( requests. as_bytes ( ) ) . await . expect ( "write" ) ;
341+ client. shutdown ( ) . await . ok ( ) ;
342+
343+ let mut buf = Vec :: new ( ) ;
344+ client. read_to_end ( & mut buf) . await . expect ( "read" ) ;
345+ let text = String :: from_utf8_lossy ( & buf) ;
346+ let responses: Vec < & str > = text. lines ( ) . collect ( ) ;
347+ assert ! (
348+ responses. len( ) >= 2 ,
349+ "expected 2 responses, got {}: {text}" ,
350+ responses. len( )
351+ ) ;
352+
353+ let r1: serde_json:: Value = serde_json:: from_str ( responses[ 0 ] ) . expect ( "json1" ) ;
354+ assert ! ( r1[ "result" ] [ "healthy" ] . as_bool( ) . is_some( ) ) ;
355+ let r2: serde_json:: Value = serde_json:: from_str ( responses[ 1 ] ) . expect ( "json2" ) ;
356+ assert ! ( r2[ "result" ] [ "version" ] . as_str( ) . is_some( ) ) ;
357+ }
358+
359+ #[ tokio:: test]
360+ async fn test_ndjson_unix_persistent_multi_request ( ) {
361+ let handler = Arc :: new ( test_handler ( ) ) ;
362+ let dir = tempfile:: tempdir ( ) . expect ( "tempdir" ) ;
363+ let socket_path = dir. path ( ) . join ( "ndjson-multi.sock" ) ;
364+
365+ let server_handler = Arc :: clone ( & handler) ;
366+ let sock_path = socket_path. clone ( ) ;
367+ let _server = tokio:: spawn ( async move {
368+ serve_unix ( server_handler, sock_path) . await . expect ( "serve" ) ;
369+ } ) ;
370+
371+ let mut stream = await_unix_socket ( & socket_path) . await ;
372+
373+ // Send three requests on the same connection
374+ let r1 = b"{\" jsonrpc\" :\" 2.0\" ,\" method\" :\" toadstool.health\" ,\" id\" :1}\n " ;
375+ let r2 = b"{\" jsonrpc\" :\" 2.0\" ,\" method\" :\" toadstool.version\" ,\" id\" :2}\n " ;
376+ let r3 = b"{\" jsonrpc\" :\" 2.0\" ,\" method\" :\" toadstool.health\" ,\" id\" :3}\n " ;
377+ stream. write_all ( r1) . await . expect ( "w1" ) ;
378+ stream. write_all ( r2) . await . expect ( "w2" ) ;
379+ stream. write_all ( r3) . await . expect ( "w3" ) ;
380+ stream. shutdown ( ) . await . ok ( ) ;
381+
382+ let mut buf = Vec :: new ( ) ;
383+ stream. read_to_end ( & mut buf) . await . expect ( "read" ) ;
384+ let text = String :: from_utf8_lossy ( & buf) ;
385+ assert_eq ! ( text. lines( ) . count( ) , 3 , "expected 3 responses: {text}" ) ;
386+ }
387+
224388#[ tokio:: test]
225389async fn test_process_request_partial_json ( ) {
226390 let handler = test_handler ( ) ;
0 commit comments