@@ -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,7 +746,7 @@ 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 > {
@@ -755,7 +756,9 @@ pub(super) fn send_request_unchecked(request: &Request) -> std::io::Result<serde
755756}
756757
757758fn ensure_server_protocol_compatible ( client : & ApiClient , request_id : & str ) -> std:: io:: Result < ( ) > {
758- let status = client. status ( ) . map_err ( api_client_error_to_io) ?;
759+ let status = client
760+ . status ( )
761+ . map_err ( |err| map_server_not_running_or_io ( err, request_id, client) ) ?;
759762 let server_protocol = status
760763 . protocol
761764 . ok_or_else ( || std:: io:: Error :: other ( "server ping did not include a protocol version" ) ) ?;
@@ -778,6 +781,49 @@ pub(crate) fn protocol_mismatch_was_reported(err: &std::io::Error) -> bool {
778781 protocol_guard:: was_reported ( err)
779782}
780783
784+ pub ( crate ) fn server_not_running_was_reported ( err : & std:: io:: Error ) -> bool {
785+ server_not_running:: was_reported ( err)
786+ }
787+
788+ /// Returns the `ErrorResponse` carried by a `server_not_running` marker, if any,
789+ /// so the edge that surfaces the error can print it exactly once (deferred
790+ /// printing: recovering callers like plugin offline fallback print nothing).
791+ pub ( crate ) fn server_not_running_reported_response (
792+ err : & std:: io:: Error ,
793+ ) -> Option < & crate :: api:: schema:: ErrorResponse > {
794+ server_not_running:: reported_response ( err)
795+ }
796+
797+ /// True when an io::Error indicates nothing is listening on the API socket.
798+ /// Classify by `ErrorKind` only: Windows named pipes surface different raw
799+ /// errno values than Unix domain sockets but the same error kinds.
800+ pub ( super ) fn server_not_running_error ( err : & std:: io:: Error ) -> bool {
801+ matches ! (
802+ err. kind( ) ,
803+ std:: io:: ErrorKind :: NotFound | std:: io:: ErrorKind :: ConnectionRefused
804+ )
805+ }
806+
807+ /// Maps an `ApiClientError` from a socket command into the io::Error that
808+ /// bubbles up to `main`. A dead-server connect failure is reported as a
809+ /// friendly `server_not_running` JSON error plus a recognizable marker; all
810+ /// other errors fall through unchanged so existing handling is preserved.
811+ fn map_server_not_running_or_io (
812+ err : ApiClientError ,
813+ request_id : & str ,
814+ client : & ApiClient ,
815+ ) -> std:: io:: Error {
816+ match err {
817+ ApiClientError :: Io ( io_err) if server_not_running_error ( & io_err) => {
818+ server_not_running:: reported_error ( server_not_running:: response (
819+ request_id,
820+ & client. socket_path ( ) ,
821+ ) )
822+ }
823+ err => api_client_error_to_io ( err) ,
824+ }
825+ }
826+
781827fn api_client_error_to_io ( err : ApiClientError ) -> std:: io:: Error {
782828 match err {
783829 ApiClientError :: Io ( err) => err,
@@ -1036,4 +1082,42 @@ mod tests {
10361082 "env must use KEY=VALUE"
10371083 ) ;
10381084 }
1085+
1086+ #[ test]
1087+ fn maps_dead_server_connect_failure_to_friendly_error ( ) {
1088+ use crate :: api:: client:: { ApiClient , ApiClientError } ;
1089+
1090+ let client = ApiClient :: local ( ) ;
1091+ let socket = client. socket_path ( ) . display ( ) . to_string ( ) ;
1092+
1093+ // The helper does NOT print; it returns a recognizable marker carrying
1094+ // the ErrorResponse so the surfacing edge can print it exactly once.
1095+ let mapped = super :: map_server_not_running_or_io (
1096+ ApiClientError :: Io ( std:: io:: Error :: from ( std:: io:: ErrorKind :: NotFound ) ) ,
1097+ "cli:workspace:create" ,
1098+ & client,
1099+ ) ;
1100+
1101+ let response = super :: server_not_running:: reported_response ( & mapped)
1102+ . expect ( "dead-server connect failure should carry a server_not_running response" ) ;
1103+ assert_eq ! ( response. id, "cli:workspace:create" ) ;
1104+ assert_eq ! ( response. error. code, "server_not_running" ) ;
1105+ assert ! ( response. error. message. contains( & socket) ) ;
1106+
1107+ // The mapping is recognizable without string matching.
1108+ assert ! ( super :: server_not_running:: was_reported( & mapped) ) ;
1109+ }
1110+
1111+ #[ test]
1112+ fn classifier_ignores_unrelated_io_kinds ( ) {
1113+ use crate :: api:: client:: { ApiClient , ApiClientError } ;
1114+
1115+ let client = ApiClient :: local ( ) ;
1116+ let mapped = super :: map_server_not_running_or_io (
1117+ ApiClientError :: Io ( std:: io:: Error :: from ( std:: io:: ErrorKind :: TimedOut ) ) ,
1118+ "cli:workspace:create" ,
1119+ & client,
1120+ ) ;
1121+ assert ! ( !super :: server_not_running:: was_reported( & mapped) ) ;
1122+ }
10391123}
0 commit comments