Rails 8.1.4.1 (November 03, 2026) ↑

  • Apply config.action_dispatch.cookies_same_site_protection to every session store.

    ActionDispatch::Session::CacheStore, MemCacheStore and third-party stores built on AbstractStore or the session mixins sent session cookies with no SameSite attribute. Only CookieStore applied the configured default.

    [GHSA-gmj8-hvj3-49mw]

    Jean Boussier

  • Prevent ActionController::Live streams from blocking when a full buffer is closed or aborted.

    [GHSA-rwvh-fw6r-pwvp]

    Jeremy Daer

  • Set and extend the Vary header for rendered responses according to the request headers used in format negotiation.

    Vary: Accept was only added when the request carried a usable Accept header. 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 Accept and X-Requested-With whenever the format is not given by the URL. XHR requests without Accept also vary on Content-Type, which Rails uses as a legacy format fallback. When config.action_dispatch.ignore_accept_header is enabled, responses vary only on X-Requested-With.

    These fields are appended to an existing Vary value without duplicating fields case-insensitively. Vary: * remains unchanged.

    [GHSA-r9jg-fmfx-2gv6]

    Jeremy Daer

  • Skip writing a CacheStore session that another request deleted while this one ran.

    A request that loaded a session from ActionDispatch::Session::CacheStore wrote it back when it finished, even after a concurrent reset_session or 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 :hybrid serializer migrates from, writes it back in the current format. That write ignored request.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 in Set-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 Forwarded and X-Forwarded-For headers in ActionDispatch::RemoteIp.

    When ip_spoofing_check is enabled (the default), requests carrying both headers with conflicting client addresses now raise ActionDispatch::RemoteIp::IpSpoofAttackError.

    [GHSA-2gff-2mmg-m73p]

    Gannon McGibbon, Mike Dalessio

  • ActionController::Parameters#each_key returns self when given a block.

    It returned the internal ActiveSupport::HashWithIndifferentAccess, which does not respond to permitted?, so passing that return value to mass assignment skipped the Strong Parameters check. each, each_pair and each_value already return self.

    Document that ActionController::Parameters#as_json returns 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_for with trailing_slash: true and a blank path to accept query params and an anchor.

    Previously the blank path used a frozen "/", which raised a FrozenError when appending the query string.

    Kenta Ishizaki

  • Avoid mutating the params hash passed to ActionDispatch::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, false values 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 namespace hash :path, :shallow_path, and :shallow_prefix options when passed as false.

    Kenta Ishizaki

  • Fix the deprecated scope hash :except option to apply to except instead of only.

    Kenta Ishizaki

  • Reject malformed hosts with extra ports in ActionDispatch::HostAuthorization.

    When an allowed host is configured with an explicit port, a Host header with an additional port (e.g. www.example.com:80:80) is no longer accepted.

    Andrii Furmanets

  • Fix url_for to leave out an optional part of a route when its value is a blank string, the same way it already does for nil.

    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::Live early in initializer, and introduce action_controller_live load 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::CacheStore to not generate a new session after dropping the current one.

    Kenta Ishizaki

  • Parse all three HTTP-date formats in the If-Modified-Since request header.

    If-Modified-Since carries 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 with Time.rfc2822, which only accepts the IMF-fixdate format, and the parse failure was swallowed — so a conditional GET using an RFC 850 or asctime date silently failed to produce a 304 Not Modified and re-sent the full response. It is now parsed with Time.httpdate, consistent with how Last-Modified and Date are read.

    Kenta Ishizaki

  • Fix response steam buffers compatibility with IO.copy_stream.

    IO.copy_stream(src, response.stream) raised TypeError: no implicit conversion of Array into Integer because Buffer#write returned the buffer array instead of an Integer (bytes written). Additionally, the written data could be silently corrupted because IO.copy_stream reuses its internal string buffer after write returns — storing a reference without dup meant the buffered content was later mutated.

    Both ActionDispatch::Response::Buffer#write and ActionController::Live::Buffer#write are 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_keys to control execution state sharing in ActionController::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_to block, 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 IpSpoofAttackError message to include Forwarded header 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: :html with Content-Type: x-www-form-urlencoded

    Sean Doyle

  • Add link-local IP ranges to ActionDispatch::RemoteIp default proxies.

    Link-local addresses (169.254.0.0/16 for IPv4 and fe80::/10 for IPv6) are now included in the default trusted proxy list, similar to private IP ranges.

    Adam Daniels

  • remote_ip will 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_logs setting that logs where redirects were called from.

    Similar to active_record.verbose_query_logs and active_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_NAME correctly.

    Fixed an issue where SCRIPT_NAME is not applied to paths generated for routes in an engine mounted at "/".

    Mike Dalessio

  • Update ActionController::Metal::RateLimiting to support passing method names to :by and :with

    class 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_url when 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.logger to be disabled by setting it to nil or false instead of always defaulting to Rails.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::TooManyRequests error from ActionController::RateLimiting

    Requests that exceed the rate limit raise an ActionController::TooManyRequests error. By default, Action Dispatch rescues the error and responds with a 429 Too Many Requests status.

    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 to rate_limit method.

    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_finished callbacks in ActionDispatch::Executor.

    The executor middleware now supports deferring completion callbacks to later in the request lifecycle by utilizing Rack’s rack.response_finished mechanism, when available. This enables applications to define rack.response_finished callbacks 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_from is invoked.

    Steven Webb, Jean Boussier

  • Allow hosts redirects from hosts Rails configuration

    config.action_controller.allowed_redirect_hosts << "example.com"
    

    Kevin Robatel

  • rate_limit.action_controller notification has additional payload

    additional 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 PublicExceptions and DebugExceptions.

    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_directives object 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-understand directive according to RFC 9111.

    The must-understand directive 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 with no-store to 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+2028 and U+2029 characters 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 :callback option is set, since the JSON is used as JavaScript code in this situation (JSONP).

    You can use the :escape option or set config.action_controller.escape_json_responses to true to 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_loaded callbacks, 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::DoubleRenderError if head is called after rendering.

    After this change, invoking head will 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 ArgumentError that's raised when invalid :only or :except options are given to resource or resources

    This 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 :controller segment in routes, but since this is deprecated and will be removed in Rails 8.1, we can now easily not consider missing controllers as routing errors.

    Jean Boussier

  • Add check_collisions option to ActionDispatch::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.