Rails 8.1.4.1 (November 03, 2026)¶ ↑
-
Apply
config.action_dispatch.cookies_same_site_protectionto every session store.ActionDispatch::Session::CacheStore,MemCacheStoreand third-party stores built onAbstractStoreor the session mixins sent session cookies with noSameSiteattribute. OnlyCookieStoreapplied the configured default.[GHSA-gmj8-hvj3-49mw]
Jean Boussier
-
Prevent
ActionController::Livestreams from blocking when a full buffer is closed or aborted.[GHSA-rwvh-fw6r-pwvp]
Jeremy Daer
-
Set and extend the
Varyheader for rendered responses according to the request headers used in format negotiation.Vary: Acceptwas only added when the request carried a usableAcceptheader. A response to a request without one, negotiated to HTML or JavaScript, could be stored by a shared cache and served to later clients that requested a different representation.Responses now vary on
AcceptandX-Requested-Withwhenever the format is not given by the URL. XHR requests withoutAcceptalso vary onContent-Type, whichRailsuses as a legacy format fallback. Whenconfig.action_dispatch.ignore_accept_headeris enabled, responses vary only onX-Requested-With.These fields are appended to an existing
Varyvalue without duplicating fields case-insensitively.Vary: *remains unchanged.[GHSA-r9jg-fmfx-2gv6]
Jeremy Daer
-
Skip writing a
CacheStoresession that another request deleted while this one ran.A request that loaded a session from
ActionDispatch::Session::CacheStorewrote it back when it finished, even after a concurrentreset_sessionor a server-side delete had removed it, so the old session ID worked again.[GHSA-2586-8ghr-69fj]
Mike Dalessio
-
Don't write upgraded cookies to a response that skips the session.
Reading a signed or encrypted cookie written under a rotated secret, or in the format a
:hybridserializer migrates from, writes it back in the current format. That write ignoredrequest.session_options[:skip], so a response that skips the session, such as the Active Storage proxy's public, cacheable response, could still carry the session cookie inSet-Cookie. Such a response now leaves the cookie unchanged, and a later request that does not skip the session upgrades it. An application that skips the session on every request no longer upgrades its signed or encrypted cookies. Cookies the application sets are still written.[GHSA-x6mj-8jx9-qhqv]
Mike Dalessio
-
Detect conflicts between the
ForwardedandX-Forwarded-Forheaders inActionDispatch::RemoteIp.When
ip_spoofing_checkis enabled (the default), requests carrying both headers with conflicting client addresses now raiseActionDispatch::RemoteIp::IpSpoofAttackError.[GHSA-2gff-2mmg-m73p]
Gannon McGibbon, Mike Dalessio
-
ActionController::Parameters#each_keyreturnsselfwhen given a block.It returned the internal
ActiveSupport::HashWithIndifferentAccess, which does not respond topermitted?, so passing that return value to mass assignment skipped the Strong Parameters check.each,each_pairandeach_valuealready returnself.Document that
ActionController::Parameters#as_jsonreturns every parameter, permitted or not, in a hash that mass assignment does not check.[GHSA-wm3x-v3qw-4jh3]
Mike Dalessio
Rails 8.1.4 (September 24, 2026)¶ ↑
-
Fix the debug error page to not attempt to read source fragments from directory paths.
Marco Roth
-
Apply source value mappings to dynamic Permissions Policy sources.
Dynamic Permissions Policy sources (procs) now have source mappings applied to their resolved values, the same as static sources.
Kenta Ishizaki
-
Allow
ActionDispatch::Http::URL.path_forwithtrailing_slash: trueand a blank path to accept query params and an anchor.Previously the blank path used a frozen
"/", which raised aFrozenErrorwhen appending the query string.Kenta Ishizaki
-
Avoid mutating the
paramshash passed toActionDispatch::Http::URL.url_for.Kenta Ishizaki
-
Honor falsy values (e.g.
format: false,anchor: false) passed in the deprecated positional-hash form of routing DSL methods.Previously,
falsevalues were silently dropped without emitting a deprecation warning, causing the deprecated hash form to produce different routes than the keyword form.Kenta Ishizaki
-
Honor the deprecated
namespacehash:path,:shallow_path, and:shallow_prefixoptions when passed asfalse.Kenta Ishizaki
-
Fix the deprecated
scopehash:exceptoption to apply toexceptinstead ofonly.Kenta Ishizaki
-
Reject malformed hosts with extra ports in
ActionDispatch::HostAuthorization.When an allowed host is configured with an explicit port, a
Hostheader with an additional port (e.g.www.example.com:80:80) is no longer accepted.Andrii Furmanets
-
Fix
url_forto leave out an optional part of a route when its value is a blank string, the same way it already does fornil.Before, the blank string ended up in the path, producing a URL the route itself cannot match:
get "(/locale/:locale)/products(/:id)", to: "products#show" url_for(controller: "products", action: "show", locale: "", id: 123) # Before: "/locale//products/123" # After: "/products/123"
Blank strings for required parameters behave as before.
Carl Dawson
-
Fix path normalization to handle mixed-case percent-encoding.
Andrii Furmanets
-
Avoid loading
ActionController::Liveearly in initializer, and introduceaction_controller_liveload hook.Adrianna Chang
-
Set the browser binary when preloading the Selenium driver for system tests.
Selenium 4.45 stopped setting it as a side effect of getting the driver path, causing tests to fail with "cannot find Chrome binary".
Titus Fortner
-
Fix
Session::CacheStoreto not generate a new session after dropping the current one.Kenta Ishizaki
-
Parse all three HTTP-date formats in the
If-Modified-Sincerequest header.If-Modified-Sincecarries an HTTP-date, which RFC 9110 allows in any of three formats (IMF-fixdate, RFC 850, or asctime), and requires recipients to accept all three. The header was parsed withTime.rfc2822, which only accepts the IMF-fixdate format, and the parse failure was swallowed — so a conditionalGETusing an RFC 850 or asctime date silently failed to produce a304 Not Modifiedand re-sent the full response. It is now parsed withTime.httpdate, consistent with howLast-ModifiedandDateare read.Kenta Ishizaki
-
Fix response steam buffers compatibility with
IO.copy_stream.IO.copy_stream(src, response.stream)raisedTypeError: no implicit conversion of Array into IntegerbecauseBuffer#writereturned the buffer array instead of anInteger(bytes written). Additionally, the written data could be silently corrupted becauseIO.copy_streamreuses its internal string buffer afterwritereturns — storing a reference withoutdupmeant the buffered content was later mutated.Both
ActionDispatch::Response::Buffer#writeandActionController::Live::Buffer#writeare affected.Gabriel Quaresma
Rails 8.1.3.1 (July 29, 2026)¶ ↑
-
No changes.
Rails 8.1.3 (March 24, 2026)¶ ↑
-
No changes.
Rails 8.1.2.1 (March 23, 2026)¶ ↑
-
No changes.
Rails 8.1.2 (January 08, 2026)¶ ↑
-
Add
config.action_controller.live_streaming_excluded_keysto control execution state sharing inActionController::Live.When using
ActionController::Live, actions are executed in a separate thread that shares state from the parent thread. This new configuration allows applications to opt-out specific state keys that should not be shared.This is useful when streaming inside a
connected_toblock, where you may want the streaming thread to use its own database connection context.# config/application.rb config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]
By default, all keys are shared.
Eileen M. Uchitelle
-
Fix
IpSpoofAttackErrormessage to includeForwardedheader content.Without it, the error message may be misleading.
zzak
Rails 8.1.1 (October 28, 2025)¶ ↑
-
Allow methods starting with underscore to be action methods.
Disallowing methods starting with an underscore from being action methods was an unintended side effect of the performance optimization in 207a254.
Fixes #55985.
Rafael Mendonça França
Rails 8.1.0 (October 22, 2025)¶ ↑
-
Submit test requests using
as: :htmlwithContent-Type: x-www-form-urlencodedSean Doyle
-
Add link-local IP ranges to
ActionDispatch::RemoteIpdefault proxies.Link-local addresses (
169.254.0.0/16for IPv4 andfe80::/10for IPv6) are now included in the default trusted proxy list, similar to private IP ranges.Adam Daniels
-
remote_ipwill no longer ignore IPs in X-Forwarded-For headers if they are accompanied by port information.Duncan Brown, Prevenios Marinos, Masafumi Koba, Adam Daniels
-
Add
action_dispatch.verbose_redirect_logssetting that logs where redirects were called from.Similar to
active_record.verbose_query_logsandactive_job.verbose_enqueue_logs, this adds a line in your logs that shows where a redirect was called from.Example:
Redirected to http://localhost:3000/posts/1 ↳ app/controllers/posts_controller.rb:32:in `block (2 levels) in create'
Dennis Paagman
-
Add engine route filtering and better formatting in
bin/rails routes.Allow engine routes to be filterable in the routing inspector, and improve formatting of engine routing output.
Before:
> bin/rails routes -e engine_only No routes were found for this grep pattern. For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html.
After:
> bin/rails routes -e engine_only Routes for application: No routes were found for this grep pattern. For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html. Routes for Test::Engine: Prefix Verb URI Pattern Controller#Action engine GET /engine_only(.:format) a#b
Dennis Paagman, Gannon McGibbon
-
Add structured events for Action Pack and Action Dispatch:
-
action_dispatch.redirect -
action_controller.request_started -
action_controller.request_completed -
action_controller.callback_halted -
action_controller.rescue_from_handled -
action_controller.file_sent -
action_controller.redirected -
action_controller.data_sent -
action_controller.unpermitted_parameters -
action_controller.fragment_cache
Adrianna Chang
-
-
URL helpers for engines mounted at the application root handle
SCRIPT_NAMEcorrectly.Fixed an issue where
SCRIPT_NAMEis not applied to paths generated for routes in an engine mounted at "/".Mike Dalessio
-
Update
ActionController::Metal::RateLimitingto support passing method names to:byand:withclass SignupsController < ApplicationController rate_limit to: 10, within: 1.minute, with: :redirect_with_flash private def redirect_with_flash redirect_to root_url, alert: "Too many requests!" end end
Sean Doyle
-
Optimize
ActionDispatch::Http::URL.build_host_urlwhen protocol is included in host.When using URL helpers with a host that includes the protocol (e.g.,
{ host: "https://example.com" }), skip unnecessary protocol normalization and string duplication since the extracted protocol is already in the correct format. This eliminates 2 string allocations per URL generation and provides a ~10% performance improvement for this case.Joshua Young, Hartley McGuire
-
Allow
action_controller.loggerto be disabled by setting it tonilorfalseinstead of always defaulting toRails.logger.Roberto Miranda
-
Remove deprecated support to a route to multiple paths.
Rafael Mendonça França
-
Remove deprecated support for using semicolons as a query string separator.
Before:
ActionDispatch::QueryParser.each_pair("foo=bar;baz=quux").to_a # => [["foo", "bar"], ["baz", "quux"]]
After:
ActionDispatch::QueryParser.each_pair("foo=bar;baz=quux").to_a # => [["foo", "bar;baz=quux"]]
Rafael Mendonça França
-
Remove deprecated support to skipping over leading brackets in parameter names in the parameter parser.
Before:
ActionDispatch::ParamBuilder.from_query_string("[foo]=bar") # => { "foo" => "bar" } ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz") # => { "foo" => { "bar" => "baz" } }
After:
ActionDispatch::ParamBuilder.from_query_string("[foo]=bar") # => { "[foo]" => "bar" } ActionDispatch::ParamBuilder.from_query_string("[foo][bar]=baz") # => { "[foo]" => { "bar" => "baz" } }
Rafael Mendonça França
-
Deprecate
Rails.application.config.action_dispatch.ignore_leading_brackets.Rafael Mendonça França
-
Raise
ActionController::TooManyRequestserror fromActionController::RateLimitingRequests that exceed the rate limit raise an
ActionController::TooManyRequestserror. By default, Action Dispatch rescues the error and responds with a429 Too Many Requestsstatus.Sean Doyle
-
Add .md/.markdown as Markdown extensions and add a default
markdown:renderer:class Page def to_markdown body end end class PagesController < ActionController::Base def show @page = Page.find(params[:id]) respond_to do |format| format.html format.md { render markdown: @page } end end end
DHH
-
Add headers to engine routes inspection command
Petrik de Heus
-
Add "Copy as text" button to error pages
Mikkel Malmberg
-
Add
scope:option torate_limitmethod.Previously, it was not possible to share a rate limit count between several controllers, since the count was by default separate for each controller.
Now, the
scope:option solves this problem.class APIController < ActionController::API rate_limit to: 2, within: 2.seconds, scope: "api" end class API::PostsController < APIController # ... end class API::UsersController < APIController # ... end
ArthurPV, Kamil Hanus
-
Add support for
rack.response_finishedcallbacks in ActionDispatch::Executor.The executor middleware now supports deferring completion callbacks to later in the request lifecycle by utilizing Rack’s
rack.response_finishedmechanism, when available. This enables applications to definerack.response_finishedcallbacks that may rely on state that would be cleaned up by the executor’s completion callbacks.Adrianna Chang, Hartley McGuire
-
Produce a log when
rescue_fromis invoked.Steven Webb, Jean Boussier
-
Allow hosts redirects from
hostsRailsconfigurationconfig.action_controller.allowed_redirect_hosts << "example.com"
Kevin Robatel
-
rate_limit.action_controllernotification has additional payloadadditional values: count, to, within, by, name, cache_key
Jonathan Rochkind
-
Add JSON support to the built-in health controller.
The health controller now responds to JSON requests with a structured response containing status and timestamp information. This makes it easier for monitoring tools and load balancers to consume health check data programmatically.
# /up.json { "status": "up", "timestamp": "2025-09-19T12:00:00Z" }
Francesco Loreti, Juan Vásquez
-
Allow to open source file with a crash from the browser.
Igor Kasyanchuk
-
Always check query string keys for valid encoding just like values are checked.
Casper Smits
-
Always return empty body for HEAD requests in
PublicExceptionsandDebugExceptions.This is required by
Rack::Lint(per RFC9110).Hartley McGuire
-
Add comprehensive support for HTTP Cache-Control request directives according to RFC 9111.
Provides a
request.cache_control_directivesobject that gives access to request cache directives:# Boolean directives request.cache_control_directives.only_if_cached? # => true/false request.cache_control_directives.no_cache? # => true/false request.cache_control_directives.no_store? # => true/false request.cache_control_directives.no_transform? # => true/false # Value directives request.cache_control_directives.max_age # => integer or nil request.cache_control_directives.max_stale # => integer or nil (or true for valueless max-stale) request.cache_control_directives.min_fresh # => integer or nil request.cache_control_directives.stale_if_error # => integer or nil # Special helpers for max-stale request.cache_control_directives.max_stale? # => true if max-stale present (with or without value) request.cache_control_directives.max_stale_unlimited? # => true only for valueless max-stale
Example usage:
def show if request.cache_control_directives.only_if_cached? @article = Article.find_cached(params[:id]) return head(:gateway_timeout) if @article.nil? else @article = Article.find(params[:id]) end render :show end
egg528
-
Add assert_in_body/assert_not_in_body as the simplest way to check if a piece of text is in the response body.
DHH
-
Include cookie name when calculating maximum allowed size.
Hartley McGuire
-
Implement
must-understanddirective according to RFC 9111.The
must-understanddirective indicates that a cache must understand the semantics of the response status code, or discard the response. This directive is enforced to be used only withno-storeto ensure proper cache behavior.class ArticlesController < ApplicationController def show @article = Article.find(params[:id]) if @article.special_format? must_understand render status: 203 # Non-Authoritative Information else fresh_when @article end end end
heka1024
-
The JSON renderer doesn’t escape HTML entities or Unicode line separators anymore.
Using
render json:will no longer escape<,>,&,U+2028andU+2029characters that can cause errors when the resulting JSON is embedded in JavaScript, or vulnerabilities when the resulting JSON is embedded in HTML.Since the renderer is used to return a JSON document as
application/json, it’s typically not necessary to escape those characters, and it improves performance.Escaping will still occur when the
:callbackoption is set, since the JSON is used as JavaScript code in this situation (JSONP).You can use the
:escapeoption or setconfig.action_controller.escape_json_responsestotrueto restore the escaping behavior.class PostsController < ApplicationController def index render json: Post.last(30), escape: true end end
Étienne Barrié, Jean Boussier
-
Load lazy route sets before inserting test routes
Without loading lazy route sets early, we miss
after_routes_loadedcallbacks, or risk invoking them with the test routes instead of the real ones if another load is triggered by an engine.Gannon McGibbon
-
Raise
AbstractController::DoubleRenderErrorifheadis called after rendering.After this change, invoking
headwill lead to an error if response body is already set:class PostController < ApplicationController def index render locals: {} head :ok end end
Iaroslav Kurbatov
-
The Cookie Serializer can now serialize an Active Support SafeBuffer when using message pack.
Such code would previously produce an error if an application was using messagepack as its cookie serializer.
class PostController < ApplicationController def index flash.notice = t(:hello_html) # This would try to serialize a SafeBuffer, which was not possible. end end
Edouard Chin
-
Fix
Rails.application.reload_routes!from clearing almost all routes.When calling
Rails.application.reload_routes!inside a middleware of a Rake task, it was possible under certain conditions that all routes would be cleared. If ran inside a middleware, this would result in getting a 404 on most page you visit. This issue was only happening in development.Edouard Chin
-
Add resource name to the
ArgumentErrorthat's raised when invalid:onlyor:exceptoptions are given to resource or resourcesThis makes it easier to locate the source of the problem, especially for routes drawn by gems.
Before:
:only and :except must include only [:index, :create, :new, :show, :update, :destroy, :edit], but also included [:foo, :bar]
After:
Route `resources :products` - :only and :except must include only [:index, :create, :new, :show, :update, :destroy, :edit], but also included [:foo, :bar]
Jeremy Green
-
A route pointing to a non-existing controller now returns a 500 instead of a 404.
A controller not existing isn’t a routing error that should result in a 404, but a programming error that should result in a 500 and be reported.
Until recently, this was hard to untangle because of the support for dynamic
:controllersegment in routes, but since this is deprecated and will be removed inRails8.1, we can now easily not consider missing controllers as routing errors.Jean Boussier
-
Add
check_collisionsoption toActionDispatch::Session::CacheStore.Newly generated session ids use 128 bits of randomness, which is more than enough to ensure collisions can't happen, but if you need to harden sessions even more, you can enable this option to check in the session store that the id is indeed free you can enable that option. This however incurs an extra write on session creation.
Shia
-
In ExceptionWrapper, match backtrace lines with built templates more often, allowing improved highlighting of errors within do-end blocks in templates. Fix for Ruby 3.4 to match new method labels in backtrace.
Martin Emde
-
Allow setting content type with a symbol of the Mime type.
# Before response.content_type = "text/html" # After response.content_type = :html
Petrik de Heus
Please check 8-0-stable for previous changes.