{"record":{"id":"865528d05e267061","repo":"davila7/claude-code-templates","slug":"unexpected-contents-api-response","errorCode":null,"errorMessage":"unexpected contents API response","messagePattern":"unexpected contents API response","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"cli-rust/src/github.rs","lineNumber":96,"sourceCode":"    skill_base_name: &str,\n    out: &mut Vec<DownloadedFile>,\n) -> Result<bool> {\n    let resp = client()?\n        .get(api_url)\n        .header(\"Accept\", \"application/vnd.github.v3+json\")\n        .send()?;\n\n    if resp.status().as_u16() == 404 {\n        return Ok(false);\n    }\n    if !resp.status().is_success() {\n        return Err(anyhow!(\"HTTP {}\", resp.status().as_u16()));\n    }\n\n    let contents: Value = resp.json()?;\n    let items = contents\n        .as_array()\n        .ok_or_else(|| anyhow!(\"unexpected contents API response\"))?;\n\n    for item in items {\n        let name = item.get(\"name\").and_then(|v| v.as_str()).unwrap_or(\"\");\n        let item_type = item.get(\"type\").and_then(|v| v.as_str()).unwrap_or(\"\");\n        let item_path = if relative_path.is_empty() {\n            name.to_string()\n        } else {\n            format!(\"{relative_path}/{name}\")\n        };\n\n        if item_type == \"file\" {\n            match item.get(\"download_url\").and_then(|v| v.as_str()) {\n                Some(download_url) => match fetch_raw_optional(download_url) {\n                    Some(content) => {\n                        let executable = name.ends_with(\".py\") || name.ends_with(\".sh\");\n                        out.push(DownloadedFile {\n                            target_rel_path: format!(\n                                \".claude/skills/{skill_base_name}/{item_path}\"","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/davila7/claude-code-templates/blob/a0851ed10c7c60463dac8cfaaca124cf32d5804d/cli-rust/src/github.rs#L78-L114","documentation":"The GitHub contents API response for a directory deserialized into JSON that is not an array. `walk` expects a list of entries when listing a directory path; receiving an object means the path points to a file, or GitHub returned an error object despite a 2xx-ish path, or the response shape changed.","triggerScenarios":"walk() on a path that is a file (contents API returns a single object), a path that doesn't exist combined with a redirect, or truncated directory listings (contents API returns {\"message\": \"This API returns blobs up to 1 MB...\"} for huge dirs with array truncation metadata), or an unexpected API response shape from GitHub Enterprise.","commonSituations":"Pointing download_skill_tree at a file instead of a directory; skill directory containing >1000 entries; GitHub Enterprise Server with older API shapes; trailing-slash differences in path construction.","solutions":["Verify the path passed to walk is a directory in the repo, not a file","If the directory is huge, use the Git Trees API (recursive) instead of the Contents API","Add diagnostics: log the actual JSON value type/keys before failing","Handle the 'truncated' object response by falling back to the trees endpoint"],"exampleFix":"// before\nlet items = contents.as_array().ok_or_else(|| anyhow!(\"unexpected contents API response\"))?;\n// after\nlet items = match contents {\n    Value::Array(items) => items,\n    Value::Object(o) if o.get(\"message\").is_some() =>\n        return Err(anyhow!(\"contents API error: {}\", o[\"message\"])),\n    other => return Err(anyhow!(\"expected array, got {}: {other}\",\n        match other { Value::Object(_) => \"object\", _ => \"scalar\" })),\n};","handlingStrategy":"validation","validationCode":"// Validate the path targets a directory before walking\n// (contents API returns an object for files)\nlet meta = client()?.get(&format!(\n    \"https://api.github.com/repos/{repo}/contents/{path}?ref={ref_}\"))\n    .send()?.json::<Value>()?;\nensure!(meta.is_array(), \"path {path} is not a directory\");","typeGuard":"fn is_dir_listing(v: &Value) -> bool { v.is_array() }","tryCatchPattern":"match contents.as_array() {\n    Some(items) => { /* walk items */ }\n    None => match &contents {\n        Value::Object(o) if o.get(\"message\").is_some() =>\n            return Err(anyhow!(\"contents API: {}\", o[\"message\"])),\n        _ => return Err(anyhow!(\"expected directory listing, got non-array\")),\n    },\n}","preventionTips":["Prefer the Git Trees API (git/trees?recursive=1) for deep or large directories","Assert directory semantics upstream (paths end in a dir, not a file)","Log the raw JSON when shape validation fails so mismatches are diagnosable"],"tags":["github-api","json-parsing","rust","schema-validation"],"backgroundTag":"api-response-schema-mismatch","analyzedSha":"a0851ed10c7c60463dac8cfaaca124cf32d5804d","analyzedAt":"2026-08-28T14:11:56.058Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}