BigPizzaV3/CodexPlusPlus · error

release payload missing tag_name

Error message

release payload missing tag_name

What it means

release_from_github_payload parses a GitHub Releases API JSON payload into a Release struct. The library requires the payload to contain a string 'tag_name' field, which is the sole source of the release version. If the field is absent or is not a JSON string (e.g. null, number, object), the parser cannot determine the version and throws this error instead of returning a Release with an empty version.

Solutions

  1. Inspect the payload with serde_json::to_string_pretty and confirm a string 'tag_name' field exists at the top level before parsing.
  2. Check the HTTP status before parsing — only feed release_from_github_payload bodies from a 200 response of the releases API endpoint.
  3. Ensure the release is published (draft releases are not exposed via the public API and other endpoints may return stub bodies).
  4. For custom/mirrored payloads, normalize them first: inject tag_name from whatever field holds the version.
  5. Wrap the call and fall back to release_from_latest_json_payload, which accepts 'version' or 'tag_name'.

Example fix

// before
let payload: serde_json::Value = resp.json().await?;
let release = release_from_github_payload(&payload)?;
// after
let payload: serde_json::Value = resp.json().await?;
if payload.get("tag_name").and_then(|v| v.as_str()).is_none() {
    anyhow::bail!("unexpected payload, no tag_name: {}", payload);
}
let release = release_from_github_payload(&payload)?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_tag_name(payload: &serde_json::Value) -> bool {
    payload.get("tag_name").and_then(|v| v.as_str()).map_or(false, |s| !s.is_empty())
}
// call: if !has_tag_name(&payload) { /* fetch correct endpoint or bail */ }

Type guard

fn as_tag_name(payload: &serde_json::Value) -> Option<&str> {
    payload.get("tag_name")?.as_str()
}

Try / catch

match release_from_github_payload(&payload) {
    Ok(release) => release,
    Err(e) if e.to_string().contains("missing tag_name") => {
        // fall back to latest.json parser which also accepts 'version'
        release_from_latest_json_payload(&payload)?
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling release_from_github_payload with: (1) a payload from a non-release endpoint (e.g. a rate-limit or error JSON body), (2) a draft release object lacking tag_name, (3) hand-written test/fixture JSON that omits tag_name, or (4) a payload where tag_name is a non-string value.

Common situations: Fetching the GitHub 'latest release' URL while unauthenticated and receiving a 404 JSON body without tag_name; proxy/mirror services returning a wrapped or stripped payload; CI scripts posting partial release metadata; unit-test fixtures missing the field after a schema change.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/9f3d5ca566d376a0. Report an issue: GitHub.

Appendix: source

Thrown at crates/codex-plus-core/src/update.rs:77

        .split('.')
        .map(|part| part.parse::<u64>().map_err(Into::into))
        .collect()
}

pub fn is_newer_version(candidate: &str, current: &str) -> anyhow::Result<bool> {
    let mut left = parse_version_tag(candidate)?;
    let mut right = parse_version_tag(current)?;
    let len = left.len().max(right.len());
    left.resize(len, 0);
    right.resize(len, 0);
    Ok(left > right)
}

pub fn release_from_github_payload(payload: &Value) -> anyhow::Result<Release> {
    let version = payload
        .get("tag_name")
        .and_then(Value::as_str)
        .ok_or_else(|| anyhow::anyhow!("release payload missing tag_name"))?
        .to_string();
    let assets = payload
        .get("assets")
        .and_then(Value::as_array)
        .into_iter()
        .flatten()
        .filter_map(|asset| {
            Some((
                asset.get("name")?.as_str()?.to_string(),
                asset.get("browser_download_url")?.as_str()?.to_string(),
            ))
        })
        .collect::<Vec<_>>();
    let selected = select_update_asset(&assets);
    Ok(Release {
        version,
        url: payload
            .get("html_url")

View on GitHub (pinned to b1ed92e5e4)