{"record":{"id":"346f08d0cd5f322c","repo":"pnpm/pnpm","slug":"custom-resolver-returned-invalid-resolution-err","errorCode":null,"errorMessage":"Custom resolver returned invalid resolution: {err}","messagePattern":"Custom resolver returned invalid resolution: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pnpm/crates/hooks/src/custom_resolver_adapter.rs","lineNumber":128,"sourceCode":"\n            let id = result.get(\"id\").and_then(Value::as_str).ok_or_else(|| {\n                let err: ResolveError = Box::new(std::io::Error::new(\n                    std::io::ErrorKind::InvalidData,\n                    \"Custom resolver did not return an 'id' field\",\n                ));\n                err\n            })?;\n\n            let resolution_val = result.get(\"resolution\").ok_or_else(|| {\n                let err: ResolveError = Box::new(std::io::Error::new(\n                    std::io::ErrorKind::InvalidData,\n                    \"Custom resolver did not return a 'resolution' field\",\n                ));\n                err\n            })?;\n\n            let resolution = serde_json::from_value(resolution_val.clone()).map_err(|err| {\n                let resolve_err: ResolveError = Box::new(std::io::Error::new(\n                    std::io::ErrorKind::InvalidData,\n                    format!(\"Custom resolver returned invalid resolution: {err}\"),\n                ));\n                resolve_err\n            })?;\n\n            // The hook's whole result is carried through, so a manifest\n            // the resolver returns must survive — without it the installer\n            // would re-fetch the tarball just to read `package.json`.\n            let manifest = match result.get(\"manifest\") {\n                Some(manifest_val) => {\n                    Some(Arc::new(serde_json::from_value(manifest_val.clone()).map_err(|err| {\n                        let resolve_err: ResolveError = Box::new(std::io::Error::new(\n                            std::io::ErrorKind::InvalidData,\n                            format!(\"Custom resolver returned invalid manifest: {err}\"),\n                        ));\n                        resolve_err\n                    })?))","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/pnpm/pnpm/blob/6261b7f388016d57ca6b90340342411cd1d0d00f/pnpm/crates/hooks/src/custom_resolver_adapter.rs#L110-L146","documentation":"The custom resolver did return a `resolution` value, but `serde_json::from_value` could not deserialize it into pnpm's Resolution type — the object's fields do not match the expected resolution shape (tarball/dir/integrity keys and their types). The appended serde error names the offending field so the mismatch is visible.","triggerScenarios":"`resolve()` returns `resolution` with wrong field names or types: `{ url }` instead of `{ tarball }`, strings where records are expected, or extra fields that fail deserialization for that dependency type.","commonSituations":"Typos in resolution keys; resolvers written against a different pnpm version's resolution schema; passing whole registry metadata as the resolution.","solutions":["Read the appended serde error — it identifies the failing field and expected type","Return the canonical resolution shape for the dependency type (`tarball`, `dir`, `integrity`, etc.)","Cross-check the field names against a lockfile entry produced by a real dependency of the same protocol","Keep `resolution` minimal — only the documented keys for that resolver type"],"exampleFix":"// before\n{ id: 'myproto:foo@1.0.0', resolution: { url: 'https://example.com/foo.tgz' } }\n// after\n{ id: 'myproto:foo@1.0.0', resolution: { tarball: 'https://example.com/foo.tgz' } }","handlingStrategy":"validation","validationCode":"// JS: validate the resolution shape for a tarball dependency before returning\nconst REQUIRED = { tarball: 'string' }\nfor (const [key, type] of Object.entries(REQUIRED)) {\n  if (typeof result.resolution[key] !== type) {\n    throw new Error(`resolution.${key} must be ${type}`)\n  }\n}","typeGuard":"/** @returns {resolution is { tarball: string }} */\nfunction isTarballResolution(resolution) {\n  return resolution != null && typeof resolution.tarball === 'string'\n}","tryCatchPattern":null,"preventionTips":["Model resolution variants in TypeScript (tarball/dir/integrity) so wrong shapes fail at compile time","Keep only documented resolution keys; extra/renamed fields trip deserialization","Test hook returns against pnpm's current resolution schema on every pnpm upgrade"],"tags":["pnpm","hooks","custom-resolver","validation","serde"],"backgroundTag":"hook-invalid-return-shape","analyzedSha":"6261b7f388016d57ca6b90340342411cd1d0d00f","analyzedAt":"2026-08-17T18:30:54.750Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}