xtask/rmcp_release_monitor/
diagnostics.rs1pub(super) const JSON_PREVIEW_BYTES: usize = 200;
9
10pub(super) fn json_parse_context(label: &str, payload: &str) -> String {
20 let mut message = format!("failed to parse {label}; {} bytes", payload.len());
21 if let Some(diagnosis) = diagnose_json_payload(payload) {
22 message.push_str("; ");
23 message.push_str(diagnosis);
24 }
25 message.push_str("; payload head: ");
26 message.push_str(&escaped_head(payload, JSON_PREVIEW_BYTES));
27 message
28}
29
30pub(super) fn diagnose_json_payload(payload: &str) -> Option<&'static str> {
33 if payload.starts_with('\u{feff}') {
34 return Some("payload starts with a UTF-8 byte-order mark");
35 }
36 let trimmed = payload.trim_start();
37 if trimmed.is_empty() {
38 return Some("payload is empty - the upstream fetch wrote nothing");
39 }
40 if trimmed.contains("\"message\"") && trimmed.contains("\"documentation_url\"") {
41 return Some(
42 "payload looks like a GitHub API error object - check the token, its permissions, \
43 and the rate limit",
44 );
45 }
46 match trimmed.chars().next()? {
47 '\u{1b}' => Some(
48 "payload starts with an ANSI escape - a CLI wrote colorized output instead of raw \
49 JSON. `gh` forces color when CLICOLOR_FORCE is set to anything but 0, even with \
50 stdout redirected to a file, and NO_COLOR does not override it; set \
51 CLICOLOR_FORCE=0 on the fetch step",
52 ),
53 '<' => Some("payload starts with '<' - this looks like HTML, not JSON"),
54 _ => None,
55 }
56}
57
58pub(super) fn escaped_head(payload: &str, max_bytes: usize) -> String {
61 let mut end = payload.len().min(max_bytes);
62 while !payload.is_char_boundary(end) {
63 end -= 1;
64 }
65 let escaped: String = payload[..end]
66 .chars()
67 .flat_map(char::escape_debug)
68 .collect();
69 if end < payload.len() {
70 format!("\"{escaped}\"...")
71 } else {
72 format!("\"{escaped}\"")
73 }
74}