pnpr npm Surface 显式路由语义解析:404/405 响应契约与 `/~<name>/` 注册表前缀约定

pnpr npm Surface 显式路由语义解析:404/405 响应契约与 `/~<name>/` 注册表前缀约定 pnpr npm Surface 显式路由语义解析404/405 响应契约与/~name/注册表前缀约定【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpmpnpr 是 pnpm 生态中用 Rust 编写的 npm 兼容注册表服务器见 pnpr/crates/pnpr/README.md。本文围绕 pnpm 仓库中 .changeset/npm-surface-explicit-routes.md 声明的行为变更系统解析其 npm surface 的显式路由语义不再服务的 URL 必须返回404、仅支持其他 HTTP 方法的 URL 必须返回405以及/~name/注册表前缀必须以未编码的~到达每一个生态 surface。读完本文你将理解 pnpr 服务端路由契约的精确边界、底层实现位置与验证方式能够在部署、客户端对接与二次开发中正确预期其 HTTP 行为。变更的核心三条约定的语义契约该 changeset 以pnpm/pnpr: patch的版本变更声明将 npm surface 的路由行为收敛为三条显式契约404Not Foundpnpr 的 npm surface不服务的 URL必须以404作答。405Method Not AllowedURL 路径本身存在、但仅支持其他 HTTP 方法的请求必须以405作答。未编码的~前缀/~name/注册表前缀必须在每一个生态 surfacenpm、Cargo、Python、OCI上以未编码的~到达服务端——即客户端不能把~写成%7E之类的百分号编码形式。这三点共同构成 pnpr 路由层的显式路由explicit routes语义每一个请求要么命中一个确切的服务端点要么收到一个语义精确的错误响应而不是被隐式吞掉、被代理到别处或得到含糊的结果。这与路由分类模块 pnpr/crates/route/src/lib.rs 中fail closed失败即关闭的设计哲学一脉相承。语义一不服务的 URL 返回 404底层实现pnpr 服务端在 pnpr/crates/pnpr/src/server.rs 中定义了一个统一的not_found()构造器fn not_found() - Response { RegistryError::NotFound.into_response() }即所有资源不存在的场景都收敛为同一个RegistryError::NotFound错误类型再由它转换为标准的 HTTP404响应。npm surface 的各个读路径都显式调用它例如包元数据读取package_reads.rs通过/~registry或路径无前缀形式寻址注册表时若addressed_registry解析不出目标None立即返回not_found()包元数据packument读取结果为无时返回not_found()上游返回确定的404FetchOutcome::NotFound时同样收敛为not_found()并且会清理对应缓存条目而不是返回 502 之类的代理错误。触发 404 的典型场景结合路由注册表routing.rsnpm surface 下典型触发404的请求包括场景说明包不存在默认目标或寻址注册表中没有该包名的 packument未知注册表/~ghost/...引用了未配置的注册表名未配置默认目标路径无前缀形式bare base在配置没有 default 时无法寻址上游确定缺失上游明确返回404pnpr 不再回退到其他 source见 READMEA missing package or failed upstream is final, without fallback未挂载的 surface某个 featureresolver/registry/artifacts/pipeline未启用时对应路径不存在值得注意的是 pnpr 刻意不做 source 回退一个注册表 router 选择第一个 namespace 声称拥有该包的 source若该 source 缺失或失败结果是终局性的404绝不静默落到第二个 source——这正是显式路由语义的一部分。语义二路径存在但方法不支持时返回 405npm surface方法级路由注册npm surface 的每一个路由都按方法注册。在 routing.rs 的npm_registry_routes()中所有包路径都显式声明了允许的方法组合例如.route(path(/{name}), get(get_packument).put(put_package)) .route(path(/{name}/-/{filename}), get(get_tarball)) .route(path(/-/{name}/dist-tags/{tag}), put(put_package_dist_tag).delete(delete_package_dist_tag))这意味着GET /lodash命中 packument 读取PUT /lodash命中发布而DELETE /lodash走不到任何 handler——axum 对路径已注册、方法未注册的请求返回405 Method Not Allowed一个只读的 tarball 路径/{name}/-/{filename}只注册了GET对其PUT会得到405。OCI surface显式的 method_not_allowed相比 npm surface 依赖 axum 方法路由的隐含行为OCI镜像注册表surface 在 oci/response.rs 中实现了显式的method_not_allowed()pub(super) fn method_not_allowed() - Response { error(ErrorCode::Unsupported, unsupported method for this endpoint) }它在 manifest、blob、upload session、referrers、catalog、tags 等端点的match self.method分支中兜底使用如 oci.rs、manifest_request.rs、discovery.rs。凡是端点存在但方法不在GET/HEAD/PUT/DELETE/POST的允许集合内一律返回405。这种显式方法枚举的写法保证了即使未来新增端点未预料到的方法也不会被静默当作其他语义处理——与本次变更显式路由的意图完全一致。语义三/~name/前缀必须在每个生态 surface 以未编码~到达什么是/~name/注册表前缀pnpr 支持在同一实例上服务 npm、Cargo、Python、OCI 多种生态见 pnpr/crates/pnpr/README.md 的 Cargo and Python registries 一节。每个生态 surface 都提供两种寻址方式无前缀路径寻址该生态配置的默认注册表default registry/~name/路径显式寻址一个命名注册表。在多生态部署下它们都位于生态前缀之下/npm/~name/、/cargo/~name/、/pypi/~name/单生态部署则省略生态前缀直接是/~name/。该机制在 ecosystem.rs 的mount_bases中统一实现pub(super) fn mount_bases(ecosystem: Ecosystem, prefixed: bool) - [String; 2] { let prefix if prefixed { format!(/{ecosystem}) } else { String::new() }; [prefix.clone(), format!({prefix}/~{{registry}})] }npm surface 则直接在每个路由上双重注册routing.rs 的for base in [, /~{registry}]OCI surface 额外在/oci/~name/v2/下响应以兼容podman/containerd的路径寻址。为什么~必须未编码/~name/前缀的解析发生在 url_credentials.rs 的addressed_registry_segmentpub(super) fn addressed_registry_segmenta( fetch: a str, npm_endpoint: str, ) - Optiona str { let rest fetch.strip_prefix(npm_endpoint)?; let registry rest .strip_prefix(~)? .split(/) .next()?; (!registry.is_empty()).then_some(registry) }解析逻辑对路径做的是字面量strip_prefix(~)它只认原始字符~。如果客户端把~百分号编码成%7Estrip_prefix(~)直接失败、返回None该请求便不会被识别为寻址某个命名注册表从而落入其他路由语义可能 404或按无前缀默认目标处理。因此在每个生态 surface 上/~name/的~都必须以未编码形式到达。同一文件中的nerf_prefix也印证了这一点pnpr 对自身public_url计算 nerf-dart 前缀如//host/pnpr/用于识别本服务托管的 fetch——路径感知的匹配同样依赖原始字符而不是解码后的 URL。部署在路径前缀下依然成立pnpr 即使被反向代理挂载在子路径如https://host.example/pnpr/下/~name/寻址依然有效。测试 pnpr/crates/route/src/tests.rs 的self_endpoint_recognized_when_pnpr_is_served_under_a_path_prefix验证了这一点/pnpr/~corp/acme%2Fwidget仍被正确分类为代理到corp上游的Proxied路由。路由分类/~name/与三类路由判定/~name/前缀不仅在 HTTP 层决定访问哪个注册表还直接进入 pnpr 解析缓存的路由分类route classification。route/src/lib.rs 中RouteContext::classify把一个 fetch 归类为三类RouteClass含义典型来源Public公开路由匿名获取、全局共享内置的registry.npmjs.org与npm.jsr.io路由、operator 声明的公开路由、托管包访问策略允许所有人Hosted { policy_id }pnpr 自托管且访问策略为私有托管注册表中需要授权的包Proxied { alias, credential_digest }以上游凭据代理声明了access:的上游注册表经/~name/寻址关键在classify_own_originlib.rs一个指向 pnpr 自身/~name/端点的 fetch 直接寻址该注册表——因为包名永远不可能以~开头所以/~name/段只会被解读为注册表寻址不会与包名冲突。这一前提正是未编码~前缀契约得以成立的基础。同时/~name/还按生态隔离凭据测试self_upstream_endpoint_uses_ecosystem_scoped_credentialstests.rs验证了/npm/~internal/demo只对授权给npm/internal上游的调用者授予代理凭据cargo/internal的授权用户无法复用——每个生态 surface 的/~name/前缀是独立的寻址空间。对客户端与运维的实践意义不要预编码~配置注册表 URL如pnpm --registry https://pnpr.example.com/~corp/或cargo的index sparse.../~corp/index/时保持~原样不要使用%7E否则寻址会失效。预期精确错误码包不存在是404而非 502/超时路径正确但方法不对例如对 tarball URL 发PUT是405。据此可以编写更准确的客户端重试与告警策略。多生态下带上生态前缀多生态部署中命名注册表位于/npm/~name/、/cargo/~name/等前缀下OCI 的/v2/例外始终在主机根不同生态的同名注册表是相互独立的。代理型上游需要access:只有声明了access:的上游才会作为代理凭据参与路由分类、并在/~name/端点按调用者授权放行纯镜像上游只能匿名代理见 config/src/upstream.rs 对UpstreamConfig::access的注释。公开路由声明失败即关闭operator 声明的public路由若 registry URL 或 package glob 无法解析整条规则会被丢弃None而不是退化为 match-all防止一个 typo 把私有元数据泄漏到公开路径上——与 404/405 的显式、不静默哲学完全一致。小结.changeset/npm-surface-explicit-routes.md用三句话把 pnpr npm surface 的路由行为钉死为显式契约不服务的 URL 明确404、方法不匹配明确405、/~name/注册表前缀在每个生态 surface 上必须以未编码~到达。从 routing.rs 的方法级路由注册、server.rs 的not_found()、oci/response.rs 的method_not_allowed()到 url_credentials.rs 的字面量strip_prefix(~)与 route/src/lib.rs 的三类路由分类这套语义在每个层面都贯彻显式路由、失败即关闭的原则——让服务端的行为对客户端完全可预期。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考