首页
/ Rails 8.2 Action Pack 新特性实战指南:QUERY 方法、头式 CSRF、Parameters 增强与限流进阶

Rails 8.2 Action Pack 新特性实战指南:QUERY 方法、头式 CSRF、Parameters 增强与限流进阶

2026-09-06 11:22:31作者:袁立春Spencer

本文基于 Rails 8.2.0.alpha(见 RAILS_VERSION)中 actionpack/CHANGELOG.md 的完整变更记录,结合当前仓库源码逐一验证实现细节。读完后你将掌握 Rails 8.2 在 HTTP 协议(RFC 10008 QUERY 方法)、CSRF 安全模型、ActionController::Parameters API、渲染与缓存、限流以及测试工具链上的全部变化,并能据此完成既有应用的升级与迁移。

一、变更概览:Action Pack 在 8.2 改了什么

本次变更日志覆盖 5 条主线:

  1. HTTP 协议层:新增对 RFC 10008 定义的 QUERY 方法的支持,并新增 safe_method? / unsafe_method? / bearer_token 等请求工具方法;
  2. 安全模型:引入基于 Sec-Fetch-Site 请求头的现代 CSRF 防护策略、token 认证的 scheme: 白名单、permissions_policy 特性列表更新;
  3. ActionController::Parameters:新增 deep_transform_valuesfetch_valuesdeconstruct_keys(模式匹配),merge 支持多参数与冲突块;
  4. 渲染、缓存与限流:renderable 支持 options/block、svg: 渲染器、http_cache_foreverlast_modified: 参数、rate_limit 的动态参数与 cache_key 作用域;
  5. 配置、中间件与测试工具链:一批新配置项、若干内部行为修复、多个弃用声明,以及集成测试请求助手的 query: / body: 关键字参数。

下面按主题展开,每个条目都给出源码级佐证。

二、HTTP 协议层:QUERY 方法与请求工具方法

2.1 支持 RFC 10008 的 QUERY 方法

变更日志引入了对 HTTP QUERY 方法(RFC 10008)的完整支持。QUERY 是一种安全(safe)且幂等(idempotent)的 HTTP 方法,查询条件放在请求体中而非 URL 查询串里,适合查询过大或结构复杂、无法塞进 URL 的场景。

路由端用法(来自变更日志):

# config/routes.rb
query "search", to: "search#index"
match "filter", to: "search#filter", via: :query

请求处理端:

request.query?                # => true
request.request_method_symbol # => :query

集成测试端:

query "/search", params: { filters: { status: "active" } }, as: :json

源码中的实现证据:

CSRF 豁免边界(变更日志特别强调,源码同样写得很清楚):与 GET、HEAD 一样,QUERY 请求豁免伪造保护,因为 HTML 表单无法发出 QUERY 请求,且跨源 QUERY 请求必然触发 CORS 预检。但豁免仅适用于真正以 QUERY 方法到达的请求:通过表单 POST 隧道传递的 _method=query 覆写会被当作普通 POST 一样验证。该逻辑实现在 verified_query_request?

# QUERY requests are exempt from forgery protection like GET and HEAD,
# but only when the request actually arrived with the QUERY method.
def verified_query_request? # :doc:
  request.query? && request.method == "QUERY"
end

注意 request.method == "QUERY" 这一半检查:query? 会读取可能被 Rack::MethodOverride 覆写后的方法,而 request.method 取的是原始 REQUEST_METHOD,两者同时为真才认定是"原生 QUERY"。

另外两点行为变化:

  • via: :all 绘制(或自定义约束未检查请求方法)的路由,现在能收到 QUERY 请求;此前这类请求在进入路由之前就会被 405 拒绝;
  • 应用服务器也必须接受该方法。例如 Puma 默认只接受八个标准 HTTP 方法,需要通过其 supported_http_methods 选项显式启用 QUERY——这是部署时最容易漏掉的一环。

2.2 safe_method?unsafe_method?

ActionDispatch::Request 新增两个方法:

request.safe_method?   # => true for GET, HEAD, OPTIONS, TRACE
request.unsafe_method? # => true for POST, PUT, PATCH, DELETE

源码实现为 get? || head? || query? || options? || trace?——可以注意到 QUERY 也被判定为安全方法,这与 RFC 9110 §9.2.1 的安全方法定义(GET/HEAD/OPTIONS/TRACE)加 RFC 10008 的 QUERY 相吻合,注释中明确写了这一依据。unsafe_method? 是它的逻辑取反。

2.3 Request#bearer_token

ActionDispatch::Request#bearer_token 用于从 Authorization 头中提取 Bearer token:

def bearer_token
  authorization.to_s[/\ABearer (.+)\z/i, 1]
end

变更日志说明 Bearer token 常用于 API 与 MCP 请求。这是一个只读提取器,配合下文的 token 认证 scheme: 选项使用非常顺手。

三、安全模型:现代 CSRF 防护与 HTTP 认证增强

3.1 基于请求头的现代 CSRF 防护

这是本次安全变更的核心。现代浏览器会发送 Sec-Fetch-Site 头来表示请求发起方与目标源的关系,Rails 现在利用它来验证同源请求而无需 authenticity token。通过 protect_from_forgery using: 提供两种策略:

  • :header_only —— 只使用 Sec-Fetch-Site 头,缺少有效头的请求直接拒绝。Rails 8.2 新建应用的默认值
  • :header_or_legacy_token —— 有 Sec-Fetch-Site 头时优先使用,否则回退到传统 authenticity token 校验,以兼容旧浏览器。

对于合法的跨站请求(OAuth 回调、第三方嵌入等),用 trusted_origins: 配置受信来源:

protect_from_forgery trusted_origins: %w[ https://accounts.google.com ]

同时,InvalidAuthenticityToken 被弃用,由 InvalidCrossOriginRequest 取代——语义从"token 无效"升级为"跨源请求被拒"。策略枚举见 request_forgery_protection.rb 中对 :header_only:header_or_legacy_token 的策略说明注释。

本地开发环境兼容修复:在不使用 HTTPS 的本地安装中(应用部署在局域网内访问),浏览器不会发送 Sec-Fetch-Site 头,header_only 策略会把所有非 GET 请求都拒掉。修复后的规则是:当请求经过 HTTP应用未配置强制 SSL 时,允许缺少 Sec-Fetch-Site 头的请求通过;Origin 检查则无论如何都会执行。

弃用无策略的 protect_from_forgery 调用:不带 :with 选项调用时目前默认 :null_session,这与 config.action_controller.default_protect_from_forgery 使用的 :exception 不一致,因此:

  • 新增配置项 config.action_controller.default_protect_from_forgery_with 允许应用指定默认策略,当前默认 :null_session(向后兼容),未来版本将改为 :exception
  • 应用可现在就用 config.action_controller.default_protect_from_forgery_with = :exception 提前切换;
  • 若想消除弃用警告而不改行为,显式传策略:protect_from_forgery with: :null_session

CSRF 事件通知:CSRF 警告从直接写日志改为事件驱动,新增三个 Active Support 通知,便于监控系统订阅:

  • csrf_token_fallback.action_controller
  • csrf_request_blocked.action_controller
  • csrf_javascript_blocked.action_controller

3.2 HTTP Token 认证:scheme: 白名单

authenticate_or_request_with_http_token 及其他 token 认证方法新增 scheme: 参数,可传单个方案名或方案名数组,用于限定接受的认证方案并原样用于 WWW-Authenticate 挑战:

authenticate_or_request_with_http_token(scheme: ["Bearer", "DPoP"]) do |token, options, scheme|
  # ...
end

源码佐证(http_authentication.rb):

  • 不指定 scheme: 时接受 SCHEMES = ["Token", "Bearer", "DPoP"]
  • 任意方案名都可以被限定;请求方的方案以小写符号形式作为可选第三个参数 yield 给认证块;
  • 多个方案在挑战头中输出为独立的多个挑战(见 Token.authentication_requestschemes.map { |s| %(#{s} realm="...") }.join(", "))。

3.3 认证 401 响应的 content_type 参数

request_http_basic_authenticationrequest_http_digest_authenticationrequest_http_token_authentication 现在接受 content_type 参数,控制 401 响应的 Content-Type,默认行为不变:

http_basic_authenticate_with(
  name: "admin", password: "secret",
  message: '{"error":"Access denied"}',
  content_type: "application/json"
)

这对需要返回 JSON 错误体的 API 很有用。

3.4 permissions_policy 特性列表更新

permissions_policy 帮助方法补充了 28 个已标准化的浏览器特性:attribution-reportingbatterybluetoothch-ua(及 ch-ua-archch-ua-bitnessch-ua-full-versionch-ua-full-version-listch-ua-high-entropy-valuesch-ua-mobilech-ua-modelch-ua-platformch-ua-platform-versionch-ua-wow64)、compute-pressurecross-origin-isolateddirect-socketsexecution-while-not-renderedexecution-while-out-of-viewportidentity-credentials-getmediasessionnavigation-overrideotp-credentialspublickey-credentials-getstorage-accesswindow-managementxr-spatial-tracking

四、ActionController::Parameters:API 全面增强

本次 Parameters 类获得了一组补齐 Hash 惯用法的方法,实现都在 actionpack/lib/action_controller/metal/strong_parameters.rb

4.1 deep_transform_values / deep_transform_values!

与已有的 deep_transform_keys 配对,对齐 ActiveSupport 的 Hash#deep_transform_values。块只对叶子值求值,嵌套 hash、数组和 Parameters 实例自动遍历;返回实例继承接收者的 permitted? 状态(deep_transform_values 内部使用 new_instance_with_inherited_permitted_status):

params = ActionController::Parameters.new(
  user: { email: "  ALICE@EXAMPLE.COM  ", profile: { bio: "  Hello world  " } }
)
params.deep_transform_values { |v| v.is_a?(String) ? v.strip.downcase : v }
# => #<ActionController::Parameters {"user"=>{"email"=>"alice@example.com", "profile"=>{"bio"=>"hello world"}}} permitted: false>

此前做同样的事必须跳出强参数护栏(params.to_unsafe_h.deep_transform_values { ... });现在转换结果仍停留在 Parameters 内,批量赋值前仍须经过 permit / expect

4.2 fetch_valuesfetch 块透传键

params = ActionController::Parameters.new(name: "Francesco", age: 22)
name, age = params.fetch_values(:name, :age)
# => ["Francesco", 22]

# With default values via block
name, email = params.fetch_values(:name, :email) { |key| "default_#{key}" }
# => ["Francesco", "default_email"]

fetch_values 的一个细节:块收到的 key还原为调用时的原始键(符号或字符串),并且缺键时的值会经 convert_value_to_parameters 转换——嵌套 hash 仍返回 Parameters。与之配套,Parameters#fetch 的块现在也会 yield 缺失的键:

key = params.fetch(:missing) { |missing_key| missing_key }
# => :missing

4.3 merge / merge! 多参数与冲突块

两个方法现在都接受多个 hash,与 Ruby 的 Hash#merge 行为一致:

params1 = ActionController::Parameters.new(a: 1)
params2 = ActionController::Parameters.new(b: 2)
params1.merge(params2, { c: 3 })
# => #<ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false>

merge 还新增冲突解决块支持(与 Hash#mergeParameters#merge! 一致),实现见 merge

params1 = ActionController::Parameters.new(a: 1, b: 2)
params2 = ActionController::Parameters.new(b: 3, c: 4)
params1.merge(params2) { |key, old_val, new_val| old_val + new_val }
# => #<ActionController::Parameters {"a"=>1, "b"=>5, "c"=>4} permitted: false>

4.4 deconstruct_keys:模式匹配支持

deconstruct_keys 的加入让 params 可以参与 Ruby 模式匹配(传入键名时先 slice,键转符号后构造纯 Hash):

if params in { search:, page: }
  Article.search(search).limit(page)
else
  # ...
end

case (value = params[:string_or_hash_with_nested_key])
in String
  # do something with a String `value`…
in { nested_key: }
  # do something with `nested_key` or `value`
else
  # …
end

五、渲染、缓存与响应头

5.1 :renderable 支持渲染 options 与 block

:renderable 协议的 render_in(view_context, **) 现在可以接收渲染 options 与 block,并透传给内层 render(变更日志中的完整示例):

class Greeting
  def render_in(view_context, **)
    if block_given?
      view_context.render(html: yield)
    else
      view_context.render(inline: <<~ERB.strip, **)
        Hello, <%= local_assigns[:name] || "World" %>
      ERB
    end
  end
end

ApplicationController.render(Greeting.new)                                        # => "Hello, World"
ApplicationController.render(Greeting.new) { "Hello, Block" }                     # => "Hello, Block"
ApplicationController.render(renderable: Greeting.new)                            # => "Hello, World"
ApplicationController.render(renderable: Greeting.new, locals: { name: "Local" }) # => "Hello, Local"

5.2 新增 svg: 渲染器

class Page
  def to_svg
    body
  end
end

class PagesController < ActionController::Base
  def show
    @page = Page.find(params[:id])

    respond_to do |format|
      format.html
      format.svg { render svg: @page }
    end
  end
end

5.3 http_cache_forever 支持 last_modified:

http_cache_forever 新增可选 last_modified: 关键字参数,默认仍为 2011 年 1 月 1 日;当资源有真实的相关时间时可以替换它。

5.4 config.action_dispatch.strict_accept_header

新增配置用于停止把含 */* 通配符的 Accept 头一律当作浏览器而强制 HTML 响应。启用后,Accept: application/json, */* 的请求会正确返回 JSON。默认 false;新建应用通过 load_defaults 8.2 启用。

源码链路:ActionDispatch::Railtie 声明 config.action_dispatch.strict_accept_header = false,经 on_load(:action_dispatch_request) 钩子写入 Request 类属性(mime_negotiation.rb#L19-L22),最终作用于 respond_to 的判定逻辑(mime_negotiation.rb#L231-L239strict_accept_header || !accept.match?(BROWSER_LIKE_ACCEPTS))。

5.5 静态 CSS/HTML 响应附带 charset=utf-8

ActionDispatch::Static 服务的静态 CSS 与 HTML 文件,其 Content-Type 响应头现在附带 ; charset=utf-8,修复含非 ASCII 字符的 CSS 文件在浏览器中的编码问题。

5.6 translate / tscope: 支持点号相对解析

translate(即 t)的 scope: 选项现在支持与 key 参数相同的点号相对解析:以 . 开头的 scope 会相对于当前控制器与 action 解析。从 PostsController#index 调用 translate("bar", scope: ".foo") 现在等价于 translate("bar", scope: "posts.index.foo")

实现见 AbstractController::Translation:key 以 . 开头时优先按 controller_action_scope 展开,且若 key 本身以点号开头,则 scope: 的前导点号保持原样不展开,保留了 translate(".bar", scope: ".foo") 这类调用的既有行为。

六、限流:rate_limit 的三处增强

限流类方法的实现在 actionpack/lib/action_controller/metal/rate_limiting.rb,本次有三项增强,源码中互相印证:

  1. by: 支持 cache_key。当 by: 返回的对象响应 cache_key 时直接采用其值(rate_limiting 内 by = by.cache_key if by.respond_to?(:cache_key)):

    class CommentsController < ApplicationController
      # Cache key in the store would be `rate-limit:comments:user/1`
      rate_limit to: 2, within: 2.seconds, by: -> { current_user }
    end
    

    缓存键格式为 ["rate-limit", scope, name, by].compact.join(":")L98),与日志中的 rate-limit:comments:user/1 一致。

  2. to: / within: 支持动态值。两个选项除静态值外现在接受可调用对象(lambda/proc,在控制器实例上下文 instance_exec)或方法名符号(L95-L96):

    class APIController < ApplicationController
      rate_limit to: :max_requests, within: :time_window, by: -> { current_user.id }
    
      private
        def max_requests
          current_user.premium? ? 1000 : 100
        end
    
        def time_window
          current_user.premium? ? 1.hour : 1.minute
        end
    end
    
  3. 限流仍依赖 ActiveSupport::Cache 后端,默认 config.action_controller.cache_store(进而默认全局 config.cache_store),可用 store: 指定独立存储;超限抛出 ActionController::TooManyRequests,由 Action Dispatch 默认 rescue 为 429,并可观测 rate_limit.action_controller 通知。

七、异常处理、日志与开发体验

7.1 config.action_dispatch.silent_exceptionswrapper_exceptions

新增两个 ActionDispatch::Railtie 配置:

  • config.action_dispatch.silent_exceptions:对应 ActionDispatch::ExceptionWrapper.silent_exceptions,列表中的异常在没有应用层回溯时不再回退到框架级回溯(即不打印冗长的内部栈);
  • config.action_dispatch.wrapper_exceptions:对应 wrapper_exceptions,列表中的异常会被中间件"拆包",报告其 cause 而非包装异常本身。

7.2 action_on_open_redirect: :notify 输出结构化事件

action_on_open_redirect 设为 :notify 时,除了既有的 Active Support 通知外,现在还会发出结构化事件,便于统一的事件管道消费。

7.3 rescue_from_handled 通知携带完整回溯

config.action_controller.rescue_from_event_backtrace:array 时,rescue_from_handled.action_controller(同样影响 action_controller.rescue_from_handled 事件)通知中的 event_backtrace 属性现在携带完整回溯数组。

7.4 DebugExceptions 支持 text/markdown

通过 Accept 头请求 text/markdown 时,错误响应以 Content-Type: text/markdown 返回而非 HTML。复用既有的 text 模板生成 Markdown 输出,让 CLI 工具等客户端能拿到字节高效的结构化错误信息。

7.5 RAILS_HOST_APP_PATH:容器内开发时的编辑器链接

Rails 运行在容器内时,错误页展示的文件路径是容器内部路径,宿主机上并不存在。设置 RAILS_HOST_APP_PATH 为宿主机应用路径后,"open in editor" 功能可将容器路径正确翻译为宿主路径。.devcontainer/devcontainer.json 示例:

{
  "containerEnv": {
    "EDITOR": "code",
    "RAILS_HOST_APP_PATH": "${localWorkspaceFolder}"
  }
}

7.6 路由检查器显示 action 源码位置

rails routes --expanded 新增 "Action Location" 字段,显示每个 action 方法定义所在的文件与行号;在路由错误页面上,当设置了 RAILS_EDITOREDITOR 时,每个 Controller#Action 旁出现可点击的 ✏️ 图标,可直接在编辑器中打开该 action。

八、中间件与内部机制:ActionController::Live 和 Executor 的修复

8.1 Live 流在客户端断开时不再挂起

ActionController::Live::Buffer#abort 此前只清空流式队列、从未入队 each_chunk 退出所需的终止符,导致阻塞在 SizedQueue#pop 的读线程永远不会被唤醒,请求线程无限挂起。修复后 #abort 会入队终止符,与 #close 行为对齐。

8.2 config.action_controller.live_streaming_excluded_keys

ActionController::Live 的 action 运行在与父线程共享状态的独立线程中。新增配置允许应用排除特定不应共享的状态键,典型场景是在 connected_to 块内流式响应时,让流式线程使用自己的数据库连接上下文:

# config/application.rb
config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]

默认所有键都共享。

8.3 推迟加载 ActionController::Live,新增 action_controller_live 加载钩子

ActionController::Live 不再在 initializer 阶段被提前加载,改为通过新的 action_controller_live load hook 按需加载,减小启动时的加载面。

8.4 ActionDispatch::Executor 在 rack hijack 时立即释放执行器状态

执行器此前通过响应体的 close 回调(或可用时的 rack.response_finished)完成状态。对 WebSocket 升级与完整 rack hijack,响应体是长生命周期流式连接,close 要等套接字关闭才触发。在 Puma 下这一问题被掩盖(Action Cable 的 hijack 会脱离到工作池),但在纤程调度型服务器(如 Falcon)下请求纤程保持内联,reloader 的 share 会一直被持有直到客户端断开,从而阻塞后续所有 reload。修复后,执行器识别被 hijack 的响应(HTTP 101 升级与 rack.hijack_io),立即完成状态而非等待 body close。

8.5 路由集重绘后识别数据不再残留(回归修复)

Journey::Routes#clear 此前没有使记忆化的识别数据(ast / simulator)失效,只有 add_route 会。RouteSet#draw 先清空路由集,若 draw 块没有添加任何路由,旧的识别数据仍在,已被删除的路由仍能被识别。现在 clear 会正确失效。

8.6 其他内部修复

  • action_dispatch_request 提前加载钩子在构建应用中间件栈时的调用得到修复;
  • Rails.application.middleware 条目将关键字参数从 #args 中剥离(详见下节弃用清单);
  • 集成测试以 as: :html 提交的请求现在使用 Content-Type: x-www-form-urlencoded,更贴近真实表单行为。

九、测试工具链:集成测试 query: / body: 关键字参数

params: 对"GET 请求 + as: :json"的组合存在歧义——参数到底进查询串还是请求体?这导致排除了 Rack::MethodOverride 的纯 API 应用测试失败(对应上游 issue #57131)。

新增两个关键字参数,语义明确且可组合:

get  "/search", query: { q: "rails" }, as: :json
post "/search", query: { page: 1 }, body: { filters: {} }, as: :json
  • query: 显式把参数放进 URL 查询串(对任意 HTTP 方法);
  • body: 发送编码后的请求体;
  • params: 保留既有行为:GET → 查询串,其他方法 → 请求体。

十、弃用与迁移清单(升级必读)

以下条目是升级时真正需要动手的部分,按影响面排序:

  1. Rails.application.middleware 条目的 #args 拆分:中间件条目新增 #kwargs 访问器暴露关键字参数。#args 此前通过 Hash.ruby2_keywords_hash 把 kwargs 作为尾随 hash 混在位置参数数组里,现在只返回位置参数。直接检查 middleware.args 的代码需要同时读取 middleware.kwargs

  2. 初始化后注册/注销 MIME 类型被弃用Mime::Type.registerMime::Type.register_aliasMime::Type.unregister 在下一个 Rails 版本中于应用初始化之后调用将抛出 FrozenError。请在初始化阶段操作,如 config/initializers/mime_types.rb 或 Railtie 的 initializer 块。

  3. Mime::Type.register_callback 弃用:它本就不是公共 API,且无替代方案,直接移除调用。

  4. Mime::SETMime::LOOKUPMime::EXTENSION_LOOKUP 常量弃用:分别改用 Mime.symbolsMime::Type.lookupMime::Type.lookup_by_extension。同时新增 Mime.extensions 枚举所有已注册扩展名(含同义词),替代 Mime::EXTENSION_LOOKUP.map(&:first)

  5. ActionController::Renderers::RENDERERS 常量弃用:该常量是内部用途但曾带文档且未标记私有/:nodoc:。增删渲染器请改用公共 API:

    ActionController.add_renderer(:rtf) do
    end
    
    ActionController.remove_renderer(:rtf)
    

    需要查看渲染器列表的 gem/应用改用冻结只读接口:

    ActionController::Renderers.all.include?(:csv)
    
  6. ActionDispatch::Cookies::HTTP_HEADER 弃用:改用 Rack::SET_COOKIE

  7. InvalidAuthenticityToken 弃用:改用 InvalidCrossOriginRequest(配合第 3.1 节的头式 CSRF 防护)。

  8. 无策略的 protect_from_forgery 调用弃用:见第 3.1 节,显式传 with: 或配置 default_protect_from_forgery_with

十一、小结

本次 Action Pack 变更的共同指向很清晰:让 Rails 更贴近现代 HTTP 语义。QUERY 方法补齐了"大查询体走 body"的标准路径且完整打通了路由、CSRF、测试三侧;头式 CSRF 把防护重心从 token 移到浏览器原生请求头,同时保留旧浏览器回退;Parameters 的 API 补齐使强参数不再是需要"绕出去再回来"的孤岛。对升级者而言,真正需要代码改动的集中在第十节的弃用清单——中间件 #kwargs 拆分、MIME 类型注册时机、渲染器常量与 Cookies 头常量——建议先按此清单做全库搜索,再启用 load_defaults 8.2 带来的 strict_accept_header 等默认值变化。

登录后查看全文
热门项目推荐
相关项目推荐