@@ -18,6 +18,7 @@ mod plugin;
1818mod protocol_guard;
1919mod runtime;
2020mod server;
21+ mod server_not_running;
2122mod spec;
2223mod status;
2324mod tab;
@@ -745,17 +746,20 @@ pub(super) fn send_request(request: &Request) -> std::io::Result<serde_json::Val
745746 ensure_server_protocol_compatible ( & client, & request. id ) ?;
746747 client
747748 . request_value ( request)
748- . map_err ( api_client_error_to_io )
749+ . map_err ( |err| map_server_not_running_or_io ( err , & request . id , & client ) )
749750}
750751
751752pub ( super ) fn send_request_unchecked ( request : & Request ) -> std:: io:: Result < serde_json:: Value > {
752- ApiClient :: local ( )
753+ let client = ApiClient :: local ( ) ;
754+ client
753755 . request_value ( request)
754- . map_err ( api_client_error_to_io )
756+ . map_err ( |err| map_server_not_running_or_io ( err , & request . id , & client ) )
755757}
756758
757759fn ensure_server_protocol_compatible ( client : & ApiClient , request_id : & str ) -> std:: io:: Result < ( ) > {
758- let status = client. status ( ) . map_err ( api_client_error_to_io) ?;
760+ let status = client
761+ . status ( )
762+ . map_err ( |err| map_server_not_running_or_io ( err, request_id, client) ) ?;
759763 let server_protocol = status
760764 . protocol
761765 . ok_or_else ( || std:: io:: Error :: other ( "server ping did not include a protocol version" ) ) ?;
@@ -778,6 +782,49 @@ pub(crate) fn protocol_mismatch_was_reported(err: &std::io::Error) -> bool {
778782 protocol_guard:: was_reported ( err)
779783}
780784
785+ pub ( crate ) fn server_not_running_was_reported ( err : & std:: io:: Error ) -> bool {
786+ server_not_running:: was_reported ( err)
787+ }
788+
789+ /// Returns the `ErrorResponse` carried by a `server_not_running` marker, if any,
790+ /// so the edge that surfaces the error can print it exactly once (deferred
791+ /// printing: recovering callers like plugin offline fallback print nothing).
792+ pub ( crate ) fn server_not_running_reported_response (
793+ err : & std:: io:: Error ,
794+ ) -> Option < & crate :: api:: schema:: ErrorResponse > {
795+ server_not_running:: reported_response ( err)
796+ }
797+
798+ /// True when an io::Error indicates nothing is listening on the API socket.
799+ /// Classify by `ErrorKind` only: Windows named pipes surface different raw
800+ /// errno values than Unix domain sockets but the same error kinds.
801+ pub ( super ) fn server_not_running_error ( err : & std:: io:: Error ) -> bool {
802+ matches ! (
803+ err. kind( ) ,
804+ std:: io:: ErrorKind :: NotFound | std:: io:: ErrorKind :: ConnectionRefused
805+ )
806+ }
807+
808+ /// Maps an `ApiClientError` from a socket command into the io::Error that
809+ /// bubbles up to `main`. A dead-server connect failure is reported as a
810+ /// friendly `server_not_running` JSON error plus a recognizable marker; all
811+ /// other errors fall through unchanged so existing handling is preserved.
812+ fn map_server_not_running_or_io (
813+ err : ApiClientError ,
814+ request_id : & str ,
815+ client : & ApiClient ,
816+ ) -> std:: io:: Error {
817+ match err {
818+ ApiClientError :: Io ( io_err) if server_not_running_error ( & io_err) => {
819+ server_not_running:: reported_error ( server_not_running:: response (
820+ request_id,
821+ & client. socket_path ( ) ,
822+ ) )
823+ }
824+ err => api_client_error_to_io ( err) ,
825+ }
826+ }
827+
781828fn api_client_error_to_io ( err : ApiClientError ) -> std:: io:: Error {
782829 match err {
783830 ApiClientError :: Io ( err) => err,
@@ -1036,4 +1083,42 @@ mod tests {
10361083 "env must use KEY=VALUE"
10371084 ) ;
10381085 }
1086+
1087+ #[ test]
1088+ fn maps_dead_server_connect_failure_to_friendly_error ( ) {
1089+ use crate :: api:: client:: { ApiClient , ApiClientError } ;
1090+
1091+ let client = ApiClient :: local ( ) ;
1092+ let socket = client. socket_path ( ) . display ( ) . to_string ( ) ;
1093+
1094+ // The helper does NOT print; it returns a recognizable marker carrying
1095+ // the ErrorResponse so the surfacing edge can print it exactly once.
1096+ let mapped = super :: map_server_not_running_or_io (
1097+ ApiClientError :: Io ( std:: io:: Error :: from ( std:: io:: ErrorKind :: NotFound ) ) ,
1098+ "cli:workspace:create" ,
1099+ & client,
1100+ ) ;
1101+
1102+ let response = super :: server_not_running:: reported_response ( & mapped)
1103+ . expect ( "dead-server connect failure should carry a server_not_running response" ) ;
1104+ assert_eq ! ( response. id, "cli:workspace:create" ) ;
1105+ assert_eq ! ( response. error. code, "server_not_running" ) ;
1106+ assert ! ( response. error. message. contains( & socket) ) ;
1107+
1108+ // The mapping is recognizable without string matching.
1109+ assert ! ( super :: server_not_running:: was_reported( & mapped) ) ;
1110+ }
1111+
1112+ #[ test]
1113+ fn classifier_ignores_unrelated_io_kinds ( ) {
1114+ use crate :: api:: client:: { ApiClient , ApiClientError } ;
1115+
1116+ let client = ApiClient :: local ( ) ;
1117+ let mapped = super :: map_server_not_running_or_io (
1118+ ApiClientError :: Io ( std:: io:: Error :: from ( std:: io:: ErrorKind :: TimedOut ) ) ,
1119+ "cli:workspace:create" ,
1120+ & client,
1121+ ) ;
1122+ assert ! ( !super :: server_not_running:: was_reported( & mapped) ) ;
1123+ }
10391124}
0 commit comments