fix(runtime): 未知 auth 子路径给干净 404,不再外漏内部 TypeError (#5085) - #5774
Merged
Conversation
…aked internal TypeError
The dispatcher plugin mounted one legacy explicit route, POST ${prefix}/auth/login,
which handed better-auth the adapter's INTERNAL IHttpRequest (headers is a plain
object, not Headers). better-auth's fetch-style handler opens with
request.headers.get(...), so the route answered HTTP 500 with the raw
'request.headers.get is not a function' in the response body. /login is not a
better-auth endpoint at all, so the mount could never work for any caller.
- dispatcher-plugin.ts: delete the legacy route. Unknown auth sub-paths now fall
to the /auth/* wildcard the namespace owner mounts on the raw Hono app, which
forwards a real Fetch Request and yields better-auth's own clean 404.
- domains/auth.ts: a throw out of IAuthService.handleRequest is unattributable
here, so its message is withheld unconditionally (#5437/#5464/#5489 discipline)
— 500 INTERNAL_ERROR with the original error on the server log.
Refs #5085
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DWUR56YsttL5sTF72Q75TQ
…angeset
- auth-forward-fault-sanitization.test.ts: the dispatcher plugin mounts NO route
under ${prefix}/auth (the specific legacy mount AND the general invariant),
and a throw out of IAuthService.handleRequest is a sanitised 500 whose body
carries none of the thrown text, with the original error on the server log.
Positive controls: a better-auth Response passes through untouched (same
object), its own 401 body is not sanitised, and an empty auth slot still 501s.
- auth-unknown-subpath.hono.integration.test.ts: a real hono boot, with a fake
auth service that is better-auth-SHAPED (reads request.headers.get + new
URL(request.url) first thing, so an internal request object explodes here the
way it did in production). POST /auth/login → 404, sign-in/email → 200 with
its set-cookie.
Refs #5085
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DWUR56YsttL5sTF72Q75TQ
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 6, 2026 04:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5085
前提复核(基于最新
origin/main889ae47,非 issue 基线29c6c9d)issue 的判读成立,且缺陷在最新 main 上仍然存在。不是静态推断——在
LiteKernel + HonoServerPlugin + createDispatcherPlugin的真实 hono boot 上打了一次探针,拿回的响应体与 issue 贴的逐字一致:责任面确认落在
packages/runtime,不涉plugin-auth(边界规则不触发):plugin-auth的/auth/*通配转发的是c.req.raw(真实 FetchRequest),packages/adapters/hono两处也都是c.req.raw。全仓唯一把非 Fetch 请求交给 better-auth 的产出方,是dispatcher-plugin.ts的那一条遗留显式路由。成因
IHttpServer交给 handler 的是适配器内部的IHttpRequest,其headers是HonoHttpServer.runHandler用c.req.header()造的普通对象,不是Headers。createDispatcherPlugin过去挂了一条POST ${prefix}/auth/login(注释自称「legacy explicit … retained for self-hosted clients」),把这个对象原样喂进dispatcher.handleAuth(…, { request: req });/auth域再把context.request整个交给IAuthService.handleRequest(request: Request),而 better-auth 的 fetch 风格 handler 第一件事就是request.headers.get(…)→TypeError→ 落到errorResponseBase,那里只在looksLikeInternalErrorLeak命中时消毒,而该启发式只认 SQL/driver dump,对TypeError一言不发,于是原文出体。这条路由对任何调用方都不可能工作:
/login根本不是 better-auth 端点(既不在plugin-auth/src/auth-route-ledger.ts里,content/docs/api/plugin-endpoints.mdx也早就白纸黑字写着 "There is no/auth/loginroute"),而且/auth域自 #4113 起完全不按子路径路由。它相对于原生应用上的/auth/*通配唯一多出来的东西,就是一个 500 —— 通配本来会让 better-auth 用真实Request给出它自己的干净 404。修法(两半,均在
packages/runtime)① 产出方:删掉那条遗留路由。 按 Prime Directive #12,修产出方而非在消费方加转换:在这里补一个 Fetch
Request转换属于消费方宽容,而且换来的只是一个更贵的 404。删后未知 auth 子路径与其它所有路径一样落给命名空间属主。原位置留下一段DELIBERATELY NOT MOUNTED注释,记下测量证据与理由,免得下次有人再把它加回来。② 出口:
handleAuthRequest里handleRequest的 throw 无条件扣留原文。 auth service 自己拥有路由,所以它抛出的东西在这一层是不可归因的:本域没看过子路径、没解析过 body,分不清是调用方写错还是 handler 有 bug。#5462 已经记过「关键词启发式的一次否定不是安全的证据」,#5489 给mapDataError的终端分支(UNCLASSIFIED_FAULT)写下的正是同一条纪律,并且点名 handlerTypeError就是落到那里的形状。现在按 #5437/#5464 的惯例无条件扣留:500 + 目录里standardErrorCodeForHttpStatus(500)的INTERNAL_ERROR+INTERNAL_ERROR_MESSAGE,原始错误交服务端日志。诚实路径零代价:better-auth 自己的失败是返回
Response而不是抛(这也是AuthPlugin通配要主动记录 >=500 响应 的原因),所以真实的 401/403/404/422 依旧原样返回。反向验证(方向为红,事先预测,三段分开测量)
POST /api/v1/auth/login的线上答复origin/main)500{"code":"INTERNAL_ERROR","message":"request.headers.get is not a function"}← 与 issue 逐字吻合500{"code":"INTERNAL_ERROR","message":"Internal server error"}← 泄漏没了,但错误类别仍然错404中间那一档正是「两半都要」的实证:光消毒不删路由,拿到的还是一个语义错误的 500。
把遗留路由加回去,新增的三条断言立刻转红,报错正是缺陷本身:
(
GET那条拿到405而不是404,是被恢复的POSTmount 的错方法回收桶——这本身也是遗留路由多出来的一份噪声。)测试
packages/runtime/src/auth-forward-fault-sanitization.test.ts—— ① 路由表钉死:既钉具体的POST /api/v1/auth/login不在,也钉更一般的不变式(/api/v1/auth下一条都不许有,因为IHttpServer路由只能拿到IHttpRequest),外加两条 sanity 断言防止空路由表把前两条断言变成空真;② 消毒:TypeError 与非 SQL 的普通 Error 都被扣留,整个序列化 body 里搜不到原文,原始 error 对象原封进console.error。阳性对照三条:成功Response原对象透传(不重新封装)、better-auth 自己的 401 body 不被消毒、空 auth 槽仍然 501(dispatcher 的 /auth 域内置 mock 登录:无 auth 服务时任意邮箱+任意密码都返回 200 + 伪造 session token —— 在发行装配里,不是 dev-only #4113 不变)。packages/runtime/src/auth-unknown-subpath.hono.integration.test.ts—— 真实 hono boot,开真实 socket 走fetch。这里的假 auth service 是故意做成 better-auth 形状的:它第一件事就是request.headers.get('cookie')和new URL(request.url),所以内部请求对象在这里的炸法与线上一致;一个能容忍普通对象headers的假货会让整个套件在缺陷期间一路绿。通配 mount 复刻了AuthPlugin的 plugin-auth 的终结式 catch-all 吞掉 /api/v1/auth/* 下别人的路由 —— console 权限层目前靠 kernel.use() 顺序才活着 #4088「404 即不属于我,让路」行为,并且故意在 bootstrap 之后注册,让它成为最后一个 matcher —— 对本修法最不利的顺序。本地实测:
pnpm --filter @objectstack/runtime test→ 101 files / 1456 tests 全绿;typecheck→tsc --noEmit无输出;@objectstack/hono73 tests 全绿;check:route-envelope/check:wildcard-fallthrough/check:error-code-casing/check:adr-anchors/check:nul-bytes全绿。范围外发现
已作为 #5772 单独立项(observation-class,
finding标签,未认领):packages/plugins/plugin-auth/IMPLEMENTATION_SUMMARY.md的 "API Routes Registered" 一节与packages/client/CLIENT_SERVER_INTEGRATION_TESTS.md的 MSW 示例,仍把/api/v1/auth/login(以及/register、/logout、/session)列为已注册路由 —— 四条一条都不存在。属文档车道,本 PR 不碰。🤖 Generated with Claude Code
https://claude.ai/code/session_01DWUR56YsttL5sTF72Q75TQ
Generated by Claude Code