{"record":{"id":"758bd2d8b8f852a5","repo":"jdx/mise","slug":"host-published-a-stamp-list-for-project-before-sequence-last","errorCode":null,"errorMessage":"{host} published a stamp list for {project} before (sequence {last}) but now answers 404 at {url}; refusing to treat that as no list, since it would drop the yanks that list carried","messagePattern":"(.+?) published a stamp list for (.+?) before \\(sequence (.+?)\\) but now answers 404 at (.+?); refusing to treat that as no list, since it would drop the yanks that list carried","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"src/packslip_stamps.rs","lineNumber":241,"sourceCode":"fn check_sequence(host: &str, project: &str, list: &ReleaseListStatement) -> Result<()> {\n    let dir = state_dir();\n    let _lock = locked(&dir, host)?;\n    check_sequence_in(&dir, host, project, list)\n}\n\n/// A host that answered 404 for a project it had a list for before is not\n/// \"no list\": it would drop that list's yanks and let another host's stamp\n/// stand alone. Refuse until the host publishes again.\nfn missing_list(host: &str, project: &str, url: &str) -> Result<()> {\n    let dir = state_dir();\n    let _lock = locked(&dir, host)?;\n    missing_list_in(&dir, host, project, url)\n}\n\nfn missing_list_in(dir: &Path, host: &str, project: &str, url: &str) -> Result<()> {\n    let state = read_state(dir, host)?;\n    if let Some(last) = state.sequences.get(project) {\n        bail!(\n            \"{host} published a stamp list for {project} before (sequence {last}) but now answers 404 at {url}; refusing to treat that as no list, since it would drop the yanks that list carried\"\n        );\n    }\n    Ok(())\n}\n\nfn check_sequence_in(\n    dir: &Path,\n    host: &str,\n    project: &str,\n    list: &ReleaseListStatement,\n) -> Result<()> {\n    let mut state = read_state(dir, host)?;\n    let sequence = list.predicate.sequence;\n    if let Some(&last) = state.sequences.get(project)\n        && sequence < last\n    {\n        bail!(","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/packslip_stamps.rs#L223-L259","documentation":"Raised by packslip_stamps::missing_list_in when the host previously published a stamp list for the project (a sequence number is recorded in the local per-host state) but the list URL now returns 404. Treating the 404 as 'no list exists' would silently discard the yanks carried by the previously accepted list, so mise refuses instead.","triggerScenarios":"Calling missing_list (or missing_list_in) during a stamp-list refresh when read_state shows a stored sequence for this host+project, yet the HTTP fetch of the list URL got a 404.","commonSituations":"The host deleted or moved the project's stamp list; a mirror/proxy is serving stale 404s; a network middlebox or wrong URL rewrite turns real requests into 404s; a registry reorganization removed the project path.","solutions":["Verify the list URL is correct and the host still publishes the list; restore or re-publish the list on the host.","Use a different/mirror host for this project that still serves the stamp list.","If the list was deliberately removed and you accept dropping its yanks, clear the local per-host stamp state for this project (explicitly, not implicitly) after confirming with the publisher.","Check for proxy/CDN misconfigurations that return 404 for valid URLs."],"exampleFix":"// before: host moved the list but state still records sequence 7\n// after: point configuration at the new host/URL that serves the list,\n// or restore the list at the original URL\nmise packslip refresh --host mirrors.example.com  # fetch from a host that serves it","handlingStrategy":"retry","validationCode":"// before treating a 404 as 'no list', check local accepted state\nconst state = readHostState(dir, host);\nif (state.sequences && state.sequences[project] !== undefined) {\n  // a 404 here is NOT 'no list' — surface it loudly\n  throw new Error(`${host} previously published a list for ${project}; 404 must not be ignored`);\n}","typeGuard":"function hadPublishedList(state, project) {\n  return typeof state?.sequences?.[project] === \"number\";\n}","tryCatchPattern":"try {\n  fetchStampList(host, project);\n} catch (e) {\n  if (String(e).includes(\"answers 404\")) {\n    // verify out-of-band with the publisher; do not silently drop yanks\n    console.error(`List vanished from ${host}; restore it or explicitly clear state after confirmation.`);\n    process.exitCode = 1;\n  } else throw e;\n}","preventionTips":["Pin mirror/proxy configs so valid list URLs cannot be answered with 404s by a CDN.","Monitor stamp-list URLs with uptime checks so vanishing lists are caught before installs fail.","When a publisher moves a list, update client config before decommissioning the old URL."],"tags":["packslip","http-404","supply-chain","rollback-protection"],"backgroundTag":"resource-not-found","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}