{"record":{"id":"7397b824a9600f19","repo":"denoland/deno","slug":"error-7397b8","errorCode":null,"errorMessage":"{}: {}","messagePattern":"\\{\\}: \\{\\}","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cli/tools/publish/mod.rs","lineNumber":1435,"sourceCode":"\n  // Carried into the error so that a report of this warning is traceable in the\n  // registry's own logs, which is where the reason for a rejection lives.\n  let x_deno_ray = response\n    .headers()\n    .get(\"x-deno-ray\")\n    .and_then(|value| value.to_str().ok())\n    .map(|s| s.to_string());\n\n  // The endpoint answers 204 with an empty body on success, so there is nothing\n  // to deserialize; on failure the body is the registry's JSON error, which\n  // `ApiError` renders as \"<message> (<code>)\".\n  let body = response.collect().await?.to_bytes();\n  match serde_json::from_slice::<registry::ApiError>(&body) {\n    Ok(mut err) => {\n      err.x_deno_ray = x_deno_ray;\n      Err(err.into())\n    }\n    Err(_) => bail!(\"{}: {}\", status, response_body_snippet(&body)),\n  }\n}\n\n/// Returns a truncated, lossy UTF-8 rendering of a response body for use in\n/// error messages, so that a non-JSON response (e.g. an HTML error page) is\n/// diagnosable instead of surfacing as an opaque deserialization error.\nfn response_body_snippet(bytes: &[u8]) -> String {\n  const MAX_LEN: usize = 512;\n  let text = String::from_utf8_lossy(bytes);\n  let text = text.trim();\n  if text.len() > MAX_LEN {\n    let mut end = MAX_LEN;\n    while !text.is_char_boundary(end) {\n      end -= 1;\n    }\n    format!(\"{}... (truncated)\", &text[..end])\n  } else {\n    text.to_string()","sourceCodeStart":1417,"sourceCodeEnd":1453,"githubUrl":"https://github.com/denoland/deno/blob/f7822238cab635a3a19f99f493f675fa81a7f9d8/cli/tools/publish/mod.rs#L1417-L1453","documentation":"This is the fallback branch that converts a failed registry HTTP response into an error: it first tries to decode the body as JSR's `ApiError` JSON (rendered as '<message> (<code>)'); when the body is not that JSON, it bails with '<status>: <snippet>' so a non-JSON response (an HTML error page, an empty 502) stays diagnosable instead of surfacing as an opaque deserialization error.","triggerScenarios":"A JSR API request (publish, package creation, etc.) returns an error status whose body is not the expected ApiError JSON — gateway 502/504 HTML pages, Cloudflare interstitials, corporate proxies rewriting responses, empty bodies.","commonSituations":"Publishing behind corporate proxies or self-hosted runners with egress filtering; transient CDN errors during releases; JSR incidents.","solutions":["Retry with backoff — non-JSON error bodies almost always come from transient intermediaries.","If a proxy intercepts traffic, bypass it or allowlist api.jsr.io / jsr.io.","Use the status to triage: 5xx → wait and retry; 4xx → inspect the snippet to see which hop rejected the request."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"#!/usr/bin/env bash\nfor attempt in 1 2 3; do\n  out=\"$(deno publish 2>&1)\" && exit 0\n  # non-JSON error bodies (HTML proxy/CDN pages) are almost always transient\n  if printf '%s' \"$out\" | grep -Eq '50[0234]: '; then\n    sleep $((attempt * 20)); continue\n  fi\n  printf '%s\\n' \"$out\" >&2; exit 1\ndone\nexit 1","preventionTips":["Allowlist api.jsr.io and jsr.io on corporate proxies/self-hosted runners so error pages are not injected.","Retry releases with backoff when a status:snippet error shows gateway codes or HTML.","Log the snippet verbatim in CI — it identifies which hop (proxy, CDN, registry) produced the failure."],"tags":["publish","jsr","http","non-json-response","proxy"],"backgroundTag":"non-json-response","analyzedSha":"f7822238cab635a3a19f99f493f675fa81a7f9d8","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}