Ruby on Rails 与 Rack 深度整合指南:中间件栈的组成、配置与自定义
2026/9/8 16:18:31 网站建设 项目流程

Ruby on Rails 与 Rack 深度整合指南:中间件栈的组成、配置与自定义

【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails

Rails 是构建在 Rack 之上的 Ruby Web 框架:所有 HTTP 请求最终都会以统一约定的方式流经一套由中间件(middleware)串联起来的调用栈。本文以 Rails 官方文档中 "Rails on Rack" 一讲为主体,结合本仓库(Ruby on Rails 完整源码)中actionpackrailties的真实实现,讲清 Rack 的基本模型、Rails.application这一主 Rack 对象、Action Dispatch 内置中间件栈的每个成员及其用途,并给出通过config.middleware增删改排序、编写自定义中间件以及直接在控制器中使用 Rack 底层 API 的完整实战方案。读完你将能独立看懂bin/rails middleware的每一行输出,并在自己的应用里精准地调整请求/响应处理管线。

读完本文你将掌握:

  • Rack 应用与 Rack 中间件的标准协议(call、三元组响应、use);
  • Rails 为什么选择 Rack,以及Rails.application如何成为主 Rack 对象;
  • Action Dispatch 中间件栈与Rack::Builder的异同;
  • config.middleware.use / insert_before / insert_after / swap / move_after / move_before / delete操作中间件栈;
  • Rails 内置中间件栈中每个组件的作用与触发条件;
  • 自定义中间件的正确落地位置(lib/+autoload_lib(ignore:))与注册方式;
  • 在控制器里读取 Rackenv、直接写 Rack 响应、把路由指向 Rack 应用。

Rack 简介:为什么 Rails 需要它

Rack 为用 Ruby 开发 Web 应用提供了一套模块化接口。它把 HTTP 请求与响应"包裹"进一种约定俗成的结构,从而把 Web 服务器、Web 框架以及它们之间各类软件(也就是中间件)的 API 统一成一次方法调用。

这种约定带来的直接好处是:只要符合 Rack 规范的 Web 服务器(例如 Puma 或 Falcon)都可以与任何基于 Rack 的 Web 框架(比如 Rails)互换使用,框架不需要关心底层服务器是哪一个。在深入探讨 Rails 如何与 Rack 集成之前,先看看 Rack 本身。

一个最简 Rack 应用

一个 Rack 应用就是实现了call方法的对象。它接收一个被称作 Rack 环境(Rack environment)的env哈希。下面是一个最朴素的 Rack 应用:

class App def call(env) [200, { "content-type" => "text/plain" }, ["Hello World"]] end end run App.new

当 HTTP 请求到达时,符合 Rack 规范的 Web 服务器会把请求解析成env哈希,然后用env调用应用。call方法必须返回一个恰好包含三个元素的数组,代表 HTTP 响应:

  1. HTTP 响应状态码(上例中的200);
  2. 一个哈希,内含我们想要发送的所有 HTTP 响应头;
  3. 一个可枚举对象,逐段产出字符串形式的响应体。

Rack 应用一般通过 Web 服务器自带的命令行程序启动,应用的入口保存在config.ru文件中:

$ cat > config.ru << APP rack_app = lambda do |env| [200, { "content-type" => "text/plain" }, ["Hello World"]] end run rack_app APP $ gem install puma $ puma

此时应用应该已经可以通过 http://localhost:9292 访问:

$ curl localhost:9292 Hello World

Rack 中间件

Rack 应用可以被称作"中间件"的组件包裹起来。中间件既可以在请求到达主应用之前对它做处理,也可以在应用返回响应之后再次处理它。中间件通常被用来完成日志记录、缓存、鉴权、性能测量等横切任务。

一个 Rack 中间件必须提供new方法,new接收 Rack 应用以及任何用于配置中间件的参数;new的返回值必须是一个能响应call的 Rack 应用。典型情况下,Rack 中间件是类,中间件类的每个实例都包裹着对被关联应用的访问:

class MyMiddleware def initialize(app) @app = app end def call(env) # Operations before the request hits the main application # ------------------------------------------------------- # Propagate the request down the middleware stack status, headers, body = @app.call(env) # --------------------------------------- # Operations after the request comes back # Propagate the response up the middleware stack [status, headers, body] end end

中间的@app.call(env)是关键一环:它把请求继续向下传播;返回值(三元组)再被逐层向上回传,因此每个中间件都能在"请求下行"与"响应上行"两个阶段插入自己的逻辑。

中间件还可以短路(short-circuit)整条栈:完全跳过@app.call,由自己直接返回响应。这意味着请求永远不会到达主应用,也不会触达栈中剩余的其他中间件。一个做请求鉴权的中间件就可以利用这种手法,未通过时直接返回 401:

class AuthenticateRequest def initialize(app) @app = app end def call(env) if authenticated?(env["HTTP_AUTHORIZATION"]) @app.call(env) else [401, { "content-type" => "text/plain" }, ["Authentication failed"]] end end def authenticated?(token) # ... end end

把中间件挂到 Rack 应用上使用的是use

class AuthenticateRequest # ... end class App def call(env) [200, { "content-type" => "text/plain" }, ["Hello World"]] end end use AuthenticateRequest run App.new

上面这套用来组装 Rack 应用的 DSL 由Rack::Builder提供。想进一步了解 Rack,可查阅 Rack 规范(Rack SPEC)与 Rack 官方网站。需要说明的是,Rack 本体不在本仓库源码范围内,属于 Rails 依赖的外部 gem。

Rails on Rack:主 Rack 对象与服务器启动

Rails.application:Rails 应用的主 Rack 对象

Rails.application是 Rails 应用的主 Rack 应用对象(primary Rack application object)。任何符合 Rack 规范的 Web 服务器都应该通过Rails.application来伺服一个 Rails 应用。换句话说,一个 Rails 应用本质上就是一个包着一大堆中间件的 Rack 应用,而最内层run的"最终应用"就是路由分发器(见下文bin/rails middleware输出的最后一行)。

启动 Rails 服务器

Rails 通过子类化Rackup::Server创建了Rails::Server。执行bin/rails server时会实例化一个Rails::Server对象并启动 Web 服务器:

Rails::Server.new.tap do |server| require APP_PATH Dir.chdir(Rails.application.root) server.start end

这段代码在 服务器命令实现 中可以看到完整形态:Rails::Command::ServerCommand#perform先调用Rails::Server.new(server_options),再require APP_PATH加载应用、Dir.chdir(Rails.application.root)切换目录,最后在检测到服务器可运行时print_boot_informationserver.start

从该实现还能读到一些实用的默认值(可用于理解启动参数行为):

  • 默认端口是3000DEFAULT_PORT = 3000,可由-p/ENV["PORT"]覆盖);
  • 默认绑定地址在 development 环境为localhost,其他环境为0.0.0.0(可用-b/ENV["BINDING"]覆盖);
  • development 环境默认 PID 文件是tmp/pids/server.pid
  • Rails::Server#start内部还会create_tmp_directoriestmp/cachetmp/pidstmp/sockets)并处理开发环境缓存开关。

服务器是如何一步步初始化启动的更多细节,可参考 初始化流程指南。

Action Dispatch 中间件栈

ActionDispatch::MiddlewareStack相当于 Rails 版的Rack::Builder。它在满足 Rails 需求的前提下被构建得更加灵活、功能更丰富(其核心实现位于 actionpack/lib/action_dispatch/middleware/stack.rb)。

Rails::Application使用ActionDispatch::MiddlewareStack把内部的与外部的中间件组合起来,最终构建出"用 Rails 组成的一个完整 Rack 应用"。

从源码看,ActionDispatch::MiddlewareStack相比Rack::Builder额外提供了按类名定位、按位置插入/调换/删除的能力:每个入栈条目都被封装成Middleware对象(保存klassargskwargsblock),真正的实例化发生在build阶段——middlewares.freeze.reverse.inject(app) { |a, e| e.build(a) },即从后向前逐层包裹,最终得到洋葱模型。另外它还支持process_middleware.action_dispatch通知事件,当有监听者时会用InstrumentationProxy透明地包装每个中间件以便做性能埋点。

查看中间件栈

运行下面的命令查看完整的中间件栈:

$ bin/rails middleware

该命令的实现见 railties/lib/rails/commands/middleware/middleware_command.rb:它逐条打印use <中间件名>,最后打印一行run <应用名>.routes

下面是一个新生成 Rails 应用的示例输出:

use ActionDispatch::HostAuthorization use Rack::Sendfile use ActionDispatch::Static use Propshaft::Server use ActionDispatch::Executor use ActionDispatch::ServerTiming use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use Rack::MethodOverride use ActionDispatch::RequestId use ActionDispatch::RemoteIp use Propshaft::QuietAssets use Rails::Rack::Logger use ActionDispatch::ShowExceptions use WebConsole::Middleware use ActionDispatch::DebugExceptions use ActionDispatch::ActionableExceptions use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use ActionDispatch::Cookies use ActionDispatch::Session::CookieStore use ActionDispatch::Flash use ActionDispatch::ContentSecurityPolicy::Middleware use Rack::Head use Rack::ConditionalGet use Rack::ETag use Rack::TempfileReaper run MyApp::Application.routes

阅读这段输出时要注意:打印顺序越靠上越"外层"(越先接触到原始请求),越靠下越接近最终路由应用;而run一行才是整条栈的"底",也就是真正处理业务请求的 Rails 路由分发器。

如果你想探究这套默认序列是"怎么被搭出来的",可以阅读 railties/lib/rails/application/default_middleware_stack.rb。其中大量中间件是根据配置条件性加入的,例如:仅当config.hosts非空时才use ActionDispatch::HostAuthorizationconfig.public_file_server.enabled为真才use ActionDispatch::Staticconfig.server_timing开启才加ActionDispatch::ServerTiming;API only 应用不加Rack::MethodOverrideActionDispatch::CookiesActionDispatch::FlashRack::TempfileReaper等。这也解释了为什么"文档里的示例输出"与"你的应用实际输出"可能会略有差别。

配置中间件栈

Rails 通过配置接口config.middleware来增加、删除、修改中间件栈。你可以在application.rb或环境专属配置文件environments/<environment>.rb中使用它。

值得补充的底层机制是:config.middleware实际是Rails::Configuration::MiddlewareStackProxy的实例(见 railties/lib/rails/configuration.rb)。它并不立即改动真正的栈,而是把每次操作记录成一段延迟执行的 lambda(@operations@delete_operations),等到组装时再一次性merge_into真实的ActionDispatch::MiddlewareStack。正因为如此,你在引擎或初始化器里按任意顺序调用这些方法都是安全的,最终都会正确地落到栈中。

添加中间件

向栈中添加新中间件有三种方法:

  • config.middleware.use(new_middleware, args):把新中间件追加到中间件栈的底部(最内层,最接近路由应用的位置);
  • config.middleware.insert_before(existing_middleware, new_middleware, args):把新中间件插到指定的已有中间件之前
  • config.middleware.insert_after(existing_middleware, new_middleware, args):把新中间件插到指定的已有中间件之后

示例:

# config/application.rb # 把 `Rack::BounceFavicon` 追加到栈底 config.middleware.use Rack::BounceFavicon # 在 `ActionDispatch::Executor` 之后添加 `Lifo::Cache`, # 并把 { page_cache: false } 作为参数传给 Lifo::Cache。 config.middleware.insert_after ActionDispatch::Executor, Lifo::Cache, page_cache: false

中间件类构造函数的额外参数都可以在这里透传(位置参数与关键字参数均可),对应到 stack.rb 中的klass.new(app, *args, **kwargs, &block)

替换中间件

使用config.middleware.swap替换中间件:

# config/application.rb # 用 `Lifo::ShowExceptions` 替换 `ActionDispatch::ShowExceptions` config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions

swap在 stack.rb 中的语义是:先定位到目标中间件的位置,把新中间件插进去,再删除同位置多出来的那个旧条目。

移动中间件

使用config.middleware.move_beforeconfig.middleware.move_after移动栈中已有的中间件组件:

# config/application.rb # 把 ActionDispatch::ShowExceptions 移动到 Lifo::ShowExceptions 之前 config.middleware.move_before Lifo::ShowExceptions, ActionDispatch::ShowExceptions
# config/application.rb # 把 ActionDispatch::ShowExceptions 移动到 Lifo::ShowExceptions 之后 config.middleware.move_after Lifo::ShowExceptions, ActionDispatch::ShowExceptions

在 stack.rb 中,move的实现是先从源位置delete_at取出中间件,再插入到目标位置;move_after则额外把目标索引加一。另外move_before/move_afterinsert_before/insert_after一样被MiddlewareStackProxy记录为删除类操作,从而保证先执行完所有添加操作后、再按删除操作调整位置时语义依然正确。

删除中间件

使用config.middleware.delete删除中间件:

# config/application.rb config.middleware.delete Rack::Runtime

如果目标中间件不存在,delete会静默忽略(其实现是对条目做reject!)。而使用delete!时,若该中间件组件不存在则会抛出错误:

# config/application.rb config.middleware.delete! Some::NonExistentMiddleware

delete!在 stack.rb 中若找不到目标会直接抛出RuntimeError: No such middleware to remove。相关行为在 actionpack/test/dispatch/middleware_stack_test.rb 中有对应的单元测试覆盖(例如delete找不到时返回 nil、delete!找不到时抛异常)。

中间件栈的重载时机

中间件栈只被加载一次,并不会被监控自动热更新。因此,每次修改完中间件栈配置后,请重启你的服务器使其生效。

Rails 内置中间件栈

Action Controller 的大量功能正是通过中间件实现的。下面逐一说明上面示例栈中各个组件的用途(其中部分条目只有在满足特定配置条件时才会出现)。

ActionDispatch::ActionableExceptions

若请求来自本机,ActionDispatch::ActionableExceptions提供了一种从 Rails 错误页面直接分发执行对应动作的途径。实现在 actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb。

ActionDispatch::Callbacks

ActionDispatch::Callbacks提供在请求分发前后执行的回调。实现在 actionpack/lib/action_dispatch/middleware/callbacks.rb。

ActionDispatch::ContentSecurityPolicy::Middleware

ActionDispatch::ContentSecurityPolicy::Middleware提供了一套 DSL 用来配置Content-Security-Policy响应头。更多信息参见 Securing Rails Applications(安全指南)。

ActionDispatch::Cookies

ActionDispatch::Cookies负责从请求中读取 cookie 数据,并在响应上写入 cookie 数据。实现在 actionpack/lib/action_dispatch/middleware/cookies.rb。

ActionDispatch::DebugExceptions

ActionDispatch::DebugExceptions负责记录异常日志;当请求来自本机时,展示调试用的错误页面。实现在 actionpack/lib/action_dispatch/middleware/debug_exceptions.rb。

ActionDispatch::Executor

ActionDispatch::Executor在开发环境下确保线程安全的代码重载。实现在 actionpack/lib/action_dispatch/middleware/executor.rb。

ActionDispatch::Flash

ActionDispatch::Flash负责设置 flash 键。仅当配置了会话存储(见 config.session_store 配置项)时它才会出现在栈中。实现在 actionpack/lib/action_dispatch/middleware/flash.rb。

ActionDispatch::HostAuthorization

ActionDispatch::HostAuthorization通过限制请求可以发送到的主机(host),防止 DNS rebinding(DNS 重绑定)攻击。配置方法参见 configuring(配置指南)。实现在 actionpack/lib/action_dispatch/middleware/host_authorization.rb。

ActionDispatch::Reloader

ActionDispatch::Reloader提供 prepare 与 cleanup 回调,目的是在开发环境下辅助代码重载。实现在 actionpack/lib/action_dispatch/middleware/reloader.rb。

ActionDispatch::RemoteIp

ActionDispatch::RemoteIp负责检测 IP 欺骗攻击。实现在 actionpack/lib/action_dispatch/middleware/remote_ip.rb。

ActionDispatch::RequestId

ActionDispatch::RequestId为请求生成唯一的X-Request-Id头,并让ActionDispatch::Request#request_id方法可用。

这个唯一的请求 ID 可以用来端到端地追踪一次请求,通常会成为栈中多个环节日志的一部分。实现在 actionpack/lib/action_dispatch/middleware/request_id.rb。

ActionDispatch::ServerTiming

ActionDispatch::ServerTiming会设置一个包含本次请求性能指标的Server-Timing响应头。实现在 actionpack/lib/action_dispatch/middleware/server_timing.rb。

ActionDispatch::Session::CookieStore

ActionDispatch::Session::CookieStore负责把会话数据存储到 cookie 中。其实现位于 actionpack/lib/action_dispatch/middleware/session/ 目录下。

ActionDispatch::ShowExceptions

ActionDispatch::ShowExceptions会接住应用抛出的任何异常,并调用一个专门的 exceptions app,把它包装成对最终用户友好的响应格式。实现在 actionpack/lib/action_dispatch/middleware/show_exceptions.rb。

ActionDispatch::Static

ActionDispatch::Static负责从public目录伺服静态文件。当config.public_file_server.enabledfalse时它会被禁用。实现在 actionpack/lib/action_dispatch/middleware/static.rb。

ActiveRecord::Migration::CheckPending

ActiveRecord::Migration::CheckPending会检查是否存在待执行的迁移;当config.action_dispatch.x_sendfile_header被设置为:page_load时,若发现有待执行迁移会抛出ActiveRecord::PendingMigrationError

ActiveSupport::Cache::Strategy::LocalCache::Middleware

ActiveSupport::Cache::Strategy::LocalCache::Middleware是内存本地缓存(in-memory local cache)的中间件。该缓存不是线程安全的,只被设计为单个线程的临时内存缓存。

Propshaft::QuietAssets

Propshaft::QuietAssets会抑制(suppress)对静态资源请求的日志输出。它由 Propshaft(Rails 默认的资源管线 gem)提供,因此不出现在本仓库的actionpack/railties源码中。

Rack::ConditionalGet

Rack::ConditionalGet基于if-none-matchif-modified-since支持"条件GET"请求。如果请求的页面没有变化,则返回304 Not Modified和空响应体。

Rack::ETag

Rack::ETag为所有 String 类型的响应体添加ETag头。ETag 被用来校验缓存,从而促成上面提到的"条件GET"请求。参见 Caching with Rails(缓存指南) 中关于条件 GET 的说明。

Rack::Head

Rack::Head对所有HEAD请求返回空响应体,其它请求则原样放行。

Rack::Lock

Rack::Lock会把每个请求锁进一个 mutex,从而让每个请求都被串行同步执行。只有当config.allow_concurrency被显式设置为false时(即应用代码非线程安全)它才会出现在栈中。

Rack::MethodOverride

Rack::MethodOverride允许在params[:_method]被设置时覆写 HTTP 方法。这正是 Rails 支持浏览器原生不具备的PUTPATCHDELETE方法的底层机制(表单通常通过隐藏的_method字段来模拟)。

Rack::Runtime

Rack::Runtime设置X-Runtime响应头,其中包含执行本次请求所消耗的时间(单位:秒)。

Rack::Sendfile

Rack::Sendfile会设置一个服务器专属的X-Sendfile头。如果你使用了类似 Apache 或 Nginx 这样的反向代理服务器,它可以用于加速文件发送——例如对 Apache 可以配置为X-Sendfile。通过config.action_dispatch.x_sendfile_header配置项进行设置。

Rack::TempfileReaper

Rack::TempfileReaper负责清理用于缓冲 multipart 请求的临时文件(tempfile)。

Rails::Rack::Logger

Rails::Rack::Logger会通知日志"请求已开始";请求结束后冲刷(flush)所有日志。实现在 railties/lib/rails/rack/logger.rb。

提示(TIP):以上任何中间件都可以直接复用在你自己搭建的自定义 Rack 栈中。

自定义中间件

你可以编写自己的中间件并把它接入 Rails 应用。

创建中间件

自定义中间件文件应当放在lib/目录下,并手动require,因为中间件不会被自动重载(这也是上文中"修改后必须重启"的另一层原因)。

下面的示例从 URL 参数中读取locale值,将其存入 Rackenv,随后从查询参数中删除它,这样当请求最终进入控制器时,locale不会混入params哈希,保持参数干净:

# lib/middleware/extract_locale.rb module RackMiddleware class ExtractLocale def initialize(app) @app = app end def call(env) request = ActionDispatch::Request.new(env) if request.params["locale"].present? env["myapp.locale"] = env["action_dispatch.request.query_parameters"]["locale"] env["action_dispatch.request.query_parameters"].delete("locale") env["action_dispatch.request.parameters"].delete("locale") end @app.call(env) end end end

Rails 默认并不会创建lib/middleware/目录,所以需要你自己创建。建议把该目录从自动加载(autoload)路径中排除,以避免自动加载带来的问题:

# config/application.rb module MyApp class Application < Rails::Application # ... config.autoload_lib(ignore: %w[assets tasks middleware]) # ... end end

config.autoload_lib(ignore: ...)Rails::Application提供的便捷方法:它把lib目录加入 autoload 路径,同时通过ignore指定其中不希望参与自动加载的子目录(中间件是典型的必须手动require的类型)。

把自定义中间件加入栈

自定义中间件可以在application.rb中添加:

# config/application.rb # ... require_relative "../lib/middleware/extract_locale" module MyApp class Application < Rails::Application # ... config.middleware.use RackMiddleware::ExtractLocale # ... end end

也可以在独立的初始化器(initializer)中添加:

# config/initializers/extract_locale.rb require "#{Rails.root.join("lib", "middleware", "extract_locale")}" Rails.application.config.middleware.use RackMiddleware::ExtractLocale

两条路径等价,最终都会通过MiddlewareStackProxyuse操作应用到真实的中间件栈上。放在独立初始化器里的好处是:你可以把中间件的注册与被加载的应用上下文解耦,便于按环境条件判断后决定是否挂载。

在 Rails 中访问 Rack 底层 API

Rack 的底层 API 可以在 Rails 控制器里直接使用。

读取 Rackenv

在 Rails 控制器中可以通过request.env拿到 Rackenv哈希:

class HomeController def index user_agent = request.env["HTTP_USER_AGENT"] # ... end end

常见的做法是:在控制器里读取由前置中间件(例如上面自定义的ExtractLocale)写入env的自定义键,实现跨层数据传递。

直接编写 Rack 响应

你也可以在 Rails 控制器中直接写出一个 Rack 响应:

class HomeController def index self.response = Rack::Response[200, {}, ["I'm Home!"]] end end

注意这里使用的是 Rack 3 风格的Rack::Response[status, headers, body]类方法构造形式。这样写相当于绕过视图渲染,把响应主体完全交给自定义三元组。当然,Rails 主推的仍然是正常的render机制,这种写法适合对响应做最底层控制的少数场景。

把路由指向 Rack 应用

你可以在config/routes.rb中把请求路由到任意 Rack 应用。详细说明参见 路由指南 中关于把路由指向 Rack 应用(routing to Rack applications)的小节。这也是 Rails 与 Rack 生态(诸如挂载 WebSocket 端点、Rack 中间件型独立服务)衔接的标准入口。

小结

Rails 与 Rack 的关系可以浓缩成三句话:Rails.application是一个可以被任何 Rack 服务器驱动的 Rack 应用;这个应用由ActionDispatch::MiddlewareStack(相对Rack::Builder功能更强)组装而成;整条栈可以用bin/rails middleware一览无余,并通过config.middleware系列 API 精确调整。当默认的 20 多个内置中间件无法满足需求时,把自定义中间件放进lib/并手动require,再以use或精确位置插入的方式注册,即可在保证请求处理逻辑可复用、可测试的前提下扩展 Rails 的能力边界。文中所涉及的核心实现均可直接在仓库源码中继续研读:中间件栈引擎见 actionpack/lib/action_dispatch/middleware/stack.rb,默认栈的组装逻辑见 railties/lib/rails/application/default_middleware_stack.rb,配置代理见 railties/lib/rails/configuration.rb,对应的行为测试见 actionpack/test/dispatch/middleware_stack_test.rb。

【免费下载链接】railsRuby on Rails项目地址: https://gitcode.com/GitHub_Trending/rai/rails

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询