Skip to content

fix(runtime): 未知 auth 子路径给干净 404,不再外漏内部 TypeError (#5085) - #5774

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-5085-auth-forward-unknown-subpath
Aug 6, 2026
Merged

fix(runtime): 未知 auth 子路径给干净 404,不再外漏内部 TypeError (#5085)#5774
baozhoutao merged 2 commits into
mainfrom
claude/issue-5085-auth-forward-unknown-subpath

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes#5085

前提复核(基于最新 origin/main889ae47,非 issue 基线 29c6c9d)

issue 的判读成立,且缺陷在最新 main 上仍然存在。不是静态推断——在 LiteKernel + HonoServerPlugin + createDispatcherPlugin 的真实 hono boot 上打了一次探针,拿回的响应体与 issue 贴的逐字一致:

PROBE_STATUS=500
PROBE_BODY={"success":false,"error":{"code":"INTERNAL_ERROR","message":"request.headers.get is not a function","httpStatus":500}}

责任面确认落在 packages/runtime,不涉 plugin-auth(边界规则不触发):plugin-auth/auth/* 通配转发的是 c.req.raw(真实 Fetch Request),packages/adapters/hono 两处也都是 c.req.raw。全仓唯一把非 Fetch 请求交给 better-auth 的产出方,是 dispatcher-plugin.ts 的那一条遗留显式路由。

成因

IHttpServer 交给 handler 的是适配器内部的 IHttpRequest,其 headersHonoHttpServer.runHandlerc.req.header() 造的普通对象,不是 HeaderscreateDispatcherPlugin 过去挂了一条 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/login route"),而且 /auth 域自 #4113完全不按子路径路由。它相对于原生应用上的 /auth/* 通配唯一多出来的东西,就是一个 500 —— 通配本来会让 better-auth 用真实 Request 给出它自己的干净 404。

修法(两半,均在 packages/runtime)

① 产出方:删掉那条遗留路由。 按 Prime Directive #12,修产出方而非在消费方加转换:在这里补一个 Fetch Request 转换属于消费方宽容,而且换来的只是一个更贵的 404。删后未知 auth 子路径与其它所有路径一样落给命名空间属主。原位置留下一段 DELIBERATELY NOT MOUNTED 注释,记下测量证据与理由,免得下次有人再把它加回来。

② 出口:handleAuthRequesthandleRequest 的 throw 无条件扣留原文。 auth service 自己拥有路由,所以它抛出的东西在这一层是不可归因的:本域没看过子路径、没解析过 body,分不清是调用方写错还是 handler 有 bug。#5462 已经记过「关键词启发式的一次否定不是安全的证据」,#5489mapDataError 的终端分支(UNCLASSIFIED_FAULT)写下的正是同一条纪律,并且点名 handler TypeError 就是落到那里的形状。现在按 #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"} ← 泄漏没了,但错误类别仍然错
两半都在(本 PR)404

中间那一档正是「两半都要」的实证:光消毒不删路由,拿到的还是一个语义错误的 500。

把遗留路由加回去,新增的三条断言立刻转红,报错正是缺陷本身:

FAIL src/auth-unknown-subpath.hono.integration.test.ts > answers an unknown auth sub-path with better-auth's own 404
AssertionError: expected 500 to be 404
FAIL src/auth-unknown-subpath.hono.integration.test.ts > does not resurrect /auth/login for any verb
AssertionError: expected 405 to be 404
FAIL src/auth-forward-fault-sanitization.test.ts > mounts nothing under ${prefix}/auth
AssertionError: expected [ …(52) ] to not include 'POST /api/v1/auth/login'

(GET 那条拿到 405 而不是 404,是被恢复的 POST mount 的错方法回收桶——这本身也是遗留路由多出来的一份噪声。)

测试

本地实测:pnpm --filter @objectstack/runtime test101 files / 1456 tests 全绿;typechecktsc --noEmit 无输出;@objectstack/hono 73 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

…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
@vercel

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 6, 2026 4:42am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via packages/runtime)
  • content/docs/api/index.mdx(via @objectstack/runtime)
  • content/docs/api/wire-format.mdx(via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx(via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx(via packages/runtime)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/runtime)
  • content/docs/deployment/index.mdx(via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx(via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx(via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx(via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/runtime)
  • content/docs/kernel/cluster.mdx(via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx(via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx(via packages/runtime)
  • content/docs/plugins/packages.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx(via @objectstack/runtime)
  • content/docs/releases/v17.mdx(via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@baozhoutao@claude