← 返回文章选择

页面查自己的 API,也绕进了登录入口

工作台空列表的一次排查,以及服务端取数为什么改成直接调用。

工作台先搭成三栏:左边切模块,中间扫文章标题,右边读正文和问答。中栏希望多放几条标题,选中后在旁边接着读,用法很明确。接到 staging 以后却出现过空列表:页面框架都出来了,文章没到。

页面请求自己的 API,绕进了登录入口

原来的服务端页面先根据请求里的 host 和 protocol 拼出公网地址,再请求自己的 /api/articles。API 是复用了,路径却绕了一圈:从服务端出去,经过公网入口,再回到服务端。用户打开页面时有登录状态,这第二次由服务器发出的请求却不会自动继承那份认证。

旧代码里这个 origin 是从 x-forwarded-host、x-forwarded-proto 两个请求头拼出来的,取不到就退回 http://127.0.0.1:3000。整条链路又包在一个 try/catch 里,失败就静默返回空数组。所以现场没有报错页,只有“框架在、文章没到”。这个空列表是兜底值,不是查询结果。

为什么会被拦,入口那一层说得清楚。Caddy 的 forward_auth 把请求的副本转给认证网关,网关回 2xx 才放行,否则把网关的响应原样返回,通常是跳登录。服务器自己发出的请求身上没有登录凭据,网关就按陌生请求处理,回来的不是文章 JSON,于是又落回那个空数组。项目的学习笔记把这条记成了结论:这个请求没有浏览器登录态,容易被认证网关拦截,首屏列表就空了。

页面绕公网认证取数与直接调用服务端函数的路径对照

文字版
原取数路径:服务端页面 → 公网入口与认证 → /api/articles → 查询
修改以后: 服务端页面 → listArticlesForModule → 查询

5 月 13 日的 41386c6 版本在 page.tsx 删掉了那段 origin 拼接,页面直接调用服务端函数(下面是后来带 sort 参数的版本,当时还没有排序):

return listArticlesForModule(moduleResolution.moduleId, DEFAULT_ARTICLES_LIST_LIMIT, sort);

函数负责把 Miniflux 文章和本地评分合在一起,页面取同进程的数据不必重新走入口。Next.js 的服务端取数说明也解释了这次多余的 HTTP 往返。

现在的文档在 Caveats 里写得比“多余”更重:

Fetch data in Server Components directly from its source, not via Route Handlers.

同一节还说,服务端 fetch 用的是绝对 URL,等于往外部服务器走一趟 HTTP;构建期预渲染的页面这么写会直接构建失败,按需渲染的则因为多一次往返而更慢。在前面挂着认证网关的站点上,这条路不只是慢,是会走不通。

这次没有复制一份查询,而是把取数抽成 lib/articles/server.ts 里的 listArticlesForModule 和 getArticleForReader,页面和 /api/articles、/api/articles/[id] 两个 Route Handler 都调它们,Route Handler 只剩一层薄封装。复用的是查询实现,不是 HTTP 接口;接口留给前端的其他调用方。

改完以后,页面取数不再经过公网入口,也就不依赖第二次请求的认证。函数照样要去取 Miniflux 文章和本地评分(其实还有第三样:reader state,稍后读和最近阅读时间就是从这里进列表的),少了一次入口请求,取数照样可能出问题;以后再遇到空列表,至少可以从这个函数往下查。

返回 200,刷新却像没操作过

另一种状态问题是接口返回成功,刷新后又像没操作过。Miniflux 的用户 id 和本地 reader state 使用的身份必须一致,写的一侧用配置里的 READER_MINIFLUX_USER_ID,读的一侧却取列表第一篇的 user_id,两边对不上,就会写到一份记录、读另一份。

回头看 5 月 13 日改动前的代码,读写两侧的身份来源本来就不一样:

身份从哪来
写(稍后读接口) 配置里的 READER_TENANT_ID、READER_MINIFLUX_USER_ID
读(旧 /api/articles) 列表第一篇文章带的 user_id,取不到当 0
存(reader_entry_states) 主键是 tenant_id + miniflux_user_id + miniflux_entry_id

这两个 id 来源不同,是因为应用调 Miniflux 用的是账号用户名和密码的 Basic 认证,Miniflux API 返回的条目又自带 user_id。认证靠账号,条目归属另说,中间那层映射就是配置项。READER_MINIFLUX_USER_ID 在配置里是必填的正整数,两边各自解析、启动时都不报错,只有跑起来读回来才发现。所以配错是一个触发条件,读的身份没从同一个配置来,同样会读丢。这次提交把读取侧也改成了配置值。

主键对不上的结果很安静:查的还是同一张表,只是那一行不存在,既不报错也没有数据。

200 只说明那次请求完成了。写和读是两次请求:记录确实写成了,但读的时候按另一个身份去读,就看不到刚写的那条,界面还是没操作过的样子。所以还得用同一用户把状态读回来,读的时候带上和写入时同一个 user id。我宁愿多做这一步,也不想等界面出问题时再猜到底写给了谁。

三栏页面有东西可读以后,补交互才有着落。空白、loading 和状态回退,在屏幕上看都差不多;这次留下两个检查点:

  1. 页面有没有绕出去重新认证;
  2. 写入和读取是不是同一个用户。

留下几句话 读者来信

读到这里,有什么想法?欢迎留下你的看法。