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/main 889ae47,非 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

claude added 2 commits August 6, 2026 04:22
…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

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 4:42am

Request Review

@github-actions github-actions Bot added the size/l label Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants