@@ -37,6 +37,13 @@ impl std::error::Error for HttpError {}
3737async fn parse_response_json ( resp : reqwest:: Response ) -> anyhow:: Result < serde_json:: Value > {
3838 use serde:: Deserialize ;
3939 let bytes = resp. bytes ( ) . await ?;
40+ // Some endpoints return a success status (e.g. 200) with an empty body, such
41+ // as GET /api/v2/on-call/pages/{id} which responds with content-length: 0.
42+ // Treat an empty or whitespace-only body as JSON null rather than failing
43+ // with "EOF while parsing value at line 1 column 0".
44+ if bytes. iter ( ) . all ( u8:: is_ascii_whitespace) {
45+ return Ok ( serde_json:: Value :: Null ) ;
46+ }
4047 let mut de = serde_json:: Deserializer :: from_slice ( & bytes) ;
4148 de. disable_recursion_limit ( ) ;
4249 let de = serde_stacker:: Deserializer :: new ( & mut de) ;
@@ -1106,4 +1113,72 @@ mod tests {
11061113 ) ;
11071114 cleanup_env ( ) ;
11081115 }
1116+
1117+ /// Regression test: a 200 response with an empty body must parse as JSON null
1118+ /// instead of failing with "EOF while parsing value at line 1 column 0".
1119+ #[ tokio:: test]
1120+ async fn test_raw_get_empty_body_returns_null ( ) {
1121+ let _lock = lock_env ( ) . await ;
1122+ let mut server = mockito:: Server :: new_async ( ) . await ;
1123+ let cfg = test_config ( & server. url ( ) ) ;
1124+ let _mock = server
1125+ . mock ( "GET" , "/api/v2/on-call/pages/12345" )
1126+ . with_status ( 200 )
1127+ . with_header ( "content-type" , "application/json" )
1128+ . with_body ( "" )
1129+ . create_async ( )
1130+ . await ;
1131+ let resp = super :: raw_get ( & cfg, "/api/v2/on-call/pages/12345" , & [ ] ) . await ;
1132+ assert ! (
1133+ resp. is_ok( ) ,
1134+ "raw_get with empty body failed: {:?}" ,
1135+ resp. err( )
1136+ ) ;
1137+ assert_eq ! ( resp. unwrap( ) , serde_json:: Value :: Null ) ;
1138+ cleanup_env ( ) ;
1139+ }
1140+
1141+ /// A whitespace-only body is also unparseable JSON and must be treated as null.
1142+ #[ tokio:: test]
1143+ async fn test_raw_get_whitespace_body_returns_null ( ) {
1144+ let _lock = lock_env ( ) . await ;
1145+ let mut server = mockito:: Server :: new_async ( ) . await ;
1146+ let cfg = test_config ( & server. url ( ) ) ;
1147+ let _mock = server
1148+ . mock ( "GET" , "/api/v2/on-call/pages/12345" )
1149+ . with_status ( 200 )
1150+ . with_header ( "content-type" , "application/json" )
1151+ . with_body ( " \n \t " )
1152+ . create_async ( )
1153+ . await ;
1154+ let resp = super :: raw_get ( & cfg, "/api/v2/on-call/pages/12345" , & [ ] ) . await ;
1155+ assert ! (
1156+ resp. is_ok( ) ,
1157+ "raw_get with whitespace body failed: {:?}" ,
1158+ resp. err( )
1159+ ) ;
1160+ assert_eq ! ( resp. unwrap( ) , serde_json:: Value :: Null ) ;
1161+ cleanup_env ( ) ;
1162+ }
1163+
1164+ /// A non-empty JSON body must still parse normally (the empty-body guard must
1165+ /// not shadow the regular parse path).
1166+ #[ tokio:: test]
1167+ async fn test_raw_get_nonempty_body_parses ( ) {
1168+ let _lock = lock_env ( ) . await ;
1169+ let mut server = mockito:: Server :: new_async ( ) . await ;
1170+ let cfg = test_config ( & server. url ( ) ) ;
1171+ let _mock = server
1172+ . mock ( "GET" , "/api/v2/on-call/pages/12345" )
1173+ . with_status ( 200 )
1174+ . with_header ( "content-type" , "application/json" )
1175+ . with_body ( r#"{"data": {"id": "12345"}}"# )
1176+ . create_async ( )
1177+ . await ;
1178+ let resp = super :: raw_get ( & cfg, "/api/v2/on-call/pages/12345" , & [ ] )
1179+ . await
1180+ . expect ( "raw_get with JSON body should succeed" ) ;
1181+ assert_eq ! ( resp[ "data" ] [ "id" ] , "12345" ) ;
1182+ cleanup_env ( ) ;
1183+ }
11091184}
0 commit comments