第 3 章:Web 请求、REST API 与前端入口

一、路由是协议地图

Mastodon 的 config/routes.rb 不是一张平面表,而是把不同协议分组:网页、OAuth、API、ActivityPub、well-known、管理后台和健康检查。阅读路线应从外往里:

config/routes.rb
  ├─ config/routes/api.rb
  ├─ config/routes/activitypub.rb
  ├─ config/routes/settings.rb
  └─ config/routes/admin.rb

同一个业务对象可能有三种外部形状:

  • Web 页面:HTML shell + React 路由;
  • REST:面向客户端的版本化 JSON;
  • ActivityPub:面向联邦节点的 JSON-LD Activity/Object。

它们共享 Model/Service,但不共享输出协议。这个隔离是兼容性和安全性的基础。

二、一次 REST 请求的五层

POST /api/v1/statuses 为例:

sequenceDiagram
  participant UI as React Compose
  participant R as Rails Route
  participant C as StatusesController
  participant S as PostStatusService
  participant DB as PostgreSQL
  participant Q as Sidekiq
  participant J as JSON Serializer

  UI->>R: POST /api/v1/statuses + OAuth token
  R->>C: route + authentication
  C->>S: call(current_account, params)
  S->>DB: transaction: status + relations
  S->>Q: enqueue distribution/notification
  S-->>C: Status
  C->>J: render status for viewer
  J-->>UI: REST JSON

1. 路由层

只负责把 URL、HTTP method 和 path parameter 映射到 controller action。复杂的参数校验不应该依赖路由本身。

2. 认证层

API 请求通常经过 OAuth access token 解析,再把 token scopes、resource owner 和 account 放入请求上下文。Streaming 使用相似的 token,但在独立 Node 进程中自己解析和校验必要权限。

3. Controller 层

Controller 的理想职责:

  • 读取当前用户/当前账户;
  • 允许参数白名单;
  • 调用一个 service 或查询对象;
  • 选择 serializer 与 HTTP 状态码;
  • 把异常转换成 API error。

如果一个 action 开始直接操作多张表、发多个 worker、拼 ActivityPub payload,就应该把阅读焦点转向对应 service。

4. Service 层

PostStatusService 展示了 Mastodon 的典型用例编排:预处理属性、校验媒体、解析提及、保存状态、挂接标签/引用、提交后分发。Service 不只是“胖 model 的替代品”,它把一个用户意图跨越多个模型和副作用的过程聚合起来。

5. Serializer 层

REST serializer 根据 viewer 和请求上下文决定字段:账户关系、媒体、投票、可见性、编辑信息、应用来源等。输出 JSON 不是数据库行的直译;它是稳定的客户端契约。

三、为什么 Controller 不直接返回 Model

直接 render json: @status 会暴露三个问题:

  1. 内部字段与客户端契约耦合;
  2. viewer-dependent 字段无法统一裁剪;
  3. 同一个模型在 REST、HTML、ActivityPub 中需要不同表示。

所以要区分:

Model        = 数据与关系
Presenter    = 视图所需的组合
Serializer   = 对外协议字段
Policy       = 当前主体能看/能做什么

四、前端如何进入 Rails

Mastodon 的动态 UI 使用 React、Redux 和 Vite。Vite 负责从 app/javascript 构建 bundle;Rails 仍负责页面 shell、静态资源入口和服务端路由。典型的浏览器导航是:

GET /home
  └─ Rails 页面
      ├─ 注入 instance / current user / locale 等初始信息
      └─ 加载 Vite 产物
          └─ React mount
              ├─ route 页面组件
              ├─ Redux store
              └─ API / streaming client

前端的 API client 通常携带 OAuth token 和客户端 headers;服务端把错误映射为用户可见的 toast、表单错误或重新登录。

五、REST 与 ActivityPub 的边界

REST 面向客户端开发者,路径通常是 /api/v1/api/v2,返回 Mastodon API schema。ActivityPub 面向服务器间通信,需要:

  • JSON-LD context;
  • actor、object、activity 的 URI;
  • HTTP Signature / digest;
  • inbox/outbox/collection 语义;
  • 远端服务器的容错与兼容。

两者可以共享 AccountStatusFollow,但不能复用同一套 serializer。读到 app/serializers/restapp/serializers/activitypub 时,先确认它服务的协议。

六、错误处理的层次

参数错误

例如媒体 ID 不存在、visibility 值非法、投票选项格式不对,应在同步请求中返回 4xx,不入队。

领域冲突

例如不允许引用私密状态、未授权修改别人的状态、提及不符合 allowed_mentions,由 service/policy 抛出可识别异常。

外部依赖错误

远端 HTTP 超时、DNS、5xx 不应让本地发帖失败;这类错误进入 Sidekiq retry 和 delivery failure tracker。

客户端协议错误

Serializer 不能随意改字段名或 null 语义;API 的兼容性本身是一类错误防线。

七、从源码验证一个 API 的方法

以任意 endpoint 为例,按如下顺序做“垂直切片”:

  1. config/routes 找 method/path;
  2. 打开 controller action;
  3. before_action、OAuth scope 和 current account;
  4. 找 service/query/policy;
  5. 找 serializer 与 presenter;
  6. 找对应 request spec;
  7. 再看前端调用处和 reducer;
  8. 如果有副作用,沿 worker 到最后的 adapter/HTTP 请求。

这种切法比从某个大目录开始漫游更快,因为每一步都能用测试和路由验证。

八、源码入口清单

九、小结

Web 层的关键不是 Rails 本身,而是“协议适配器 → 用例 service → 事实模型 → 异步副作用 → 协议 serializer”的边界。下一章进入最有代表性的用例:发帖如何同时触发本地时间线、通知、公共流和远端联邦。