Skip to content

Commit d3465c3

Browse files
committed
fix(on-call): handle empty 200 body in pages get
GET /api/v2/on-call/pages/{id} can return HTTP 200 with an empty body (content-length: 0). parse_response_json fed the empty body straight to serde_json, producing "EOF while parsing value at line 1 column 0" and failing `pup on-call pages get`. Treat an empty or whitespace-only success body as JSON null in the shared parse_response_json helper, so every raw_* caller (raw_get/raw_post/etc.) degrades gracefully instead of crashing, mirroring the existing 204 No Content handling. - Guard empty/whitespace bodies in raw_client::parse_response_json - Add raw_get unit tests (empty, whitespace-only, valid JSON) - Add pages_get regression test for empty 200 body Closes #638
1 parent 001239b commit d3465c3

2 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/commands/on_call.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,29 @@ mod tests {
830830
cleanup_env();
831831
}
832832

833+
// Regression test for #638: the on-call pages GET endpoint can return a 200
834+
// with an empty body (content-length: 0). pages_get must succeed instead of
835+
// failing with "EOF while parsing value at line 1 column 0".
836+
#[tokio::test]
837+
async fn test_on_call_pages_get_empty_body() {
838+
let _lock = lock_env().await;
839+
let mut s = mockito::Server::new_async().await;
840+
let cfg = test_config(&s.url());
841+
s.mock("GET", "/api/v2/on-call/pages/12345")
842+
.with_status(200)
843+
.with_header("content-type", "application/json")
844+
.with_body("")
845+
.create_async()
846+
.await;
847+
let result = super::pages_get(&cfg, "12345").await;
848+
assert!(
849+
result.is_ok(),
850+
"pages_get with empty body failed: {:?}",
851+
result.err()
852+
);
853+
cleanup_env();
854+
}
855+
833856
#[tokio::test]
834857
async fn test_on_call_pages_get_not_found() {
835858
let _lock = lock_env().await;

src/raw_client.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ impl std::error::Error for HttpError {}
3737
async 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

Comments
 (0)