Rails HTTP Status Codes: Checker and Rack Symbol Reference
Give it a URL and it follows every redirect from our server, then names each status with the Rack symbol Rails knows it by. Below the tool is the full code to symbol table, so you can look up what to pass to render, head or redirect_to without leaving the page.
The request goes out from our server, not from your browser, and nothing that could change data on your host is ever sent.
The symbol is the API, the number is the wire
In a Rails controller the status is a symbol, and Rack owns the list. render json: invoice, status: :created, head :no_content and redirect_to url, status: :see_other all pass their symbol through Rack::Utils.status_code, which looks it up in SYMBOL_TO_STATUS_CODE and hands the integer to the server. The symbol is the API you write against; the number is what goes over the wire. Two of those symbols were renamed in Rack 3.1 to match the current specification: :unprocessable_entity became :unprocessable_content and :payload_too_large became :content_too_large. The old names still work and still resolve to 422 and 413, but Rack now emits a deprecation warning when you use one, so a log full of them is a rename waiting to happen rather than a bug. The table below shows the canonical symbol for every code Rack knows, and the former name alongside it where there is one.
The statuses Rails picks for you
Plenty of statuses are chosen for you. ActionDispatch::ExceptionWrapper keeps a rescue_responses map that turns an uncaught exception into a status: ActiveRecord::RecordNotFound becomes 404, ActionController::ParameterMissing becomes 400, and ActiveRecord::RecordInvalid and ActionController::InvalidAuthenticityToken both become 422. So a controller that calls Invoice.find(params[:id]) already answers 404 for a missing row without a rescue anywhere, and one that calls update! already answers 422 on a validation failure. You can add your own with config.action_dispatch.rescue_responses["MyApp::Forbidden"] = :forbidden, which is the cleanest way to keep an authorisation failure out of every controller. The one status worth choosing by hand is the failure branch of a form: Turbo only swaps the page when the response is not successful, so a create that re-renders the form must answer :unprocessable_content rather than 200, or the visitor sees the submit do nothing at all.
What a redirect chain costs, and where a loop comes from
A redirect chain costs a request per hop, and every hop is a round trip the visitor waits through before anything renders. 301 and 308 say the move is permanent and let a crawler transfer the old URL's signals to the new one; 302, 303 and 307 say it is temporary and keep the old address in the index. The pairs differ in one more way: a client may turn a redirected POST into a GET on 301, 302 and 303, and must not on 307 and 308, which is why an API endpoint that has moved wants 308 rather than 301. In a controller that is redirect_to new_path, status: :permanent_redirect. Chains grow by accident - a http to https rule, then a bare domain to www rule, then a trailing-slash rule, each added by somebody who could not see the other two - and each one multiplies. A loop is the same mistake with the last hop pointing back at the first, which this tool marks where it happens. Collapse the chain to a single hop where you can: the tool stops following at 5 hops and a crawler gives up at about the same depth.
Every HTTP status code and its Rack symbol
The symbol in the second column is what you pass to render, head or redirect_to. It comes straight from the Rack your app is running, so it is the list your Rails version actually accepts rather than a copy that has drifted. Where a symbol was renamed, the former name is shown underneath it.
| Code | Rack/Rails symbol | Reason phrase | In Rails |
|---|---|---|---|
| 1xx Informational | |||
| 100 | :continue | Continue | |
| 101 | :switching_protocols | Switching Protocols | |
| 102 | :processing | Processing | |
| 103 | :early_hints | Early Hints | |
| 2xx Success | |||
| 200 | :ok | OK | |
| 201 | :created | Created | What a create answers when it made something. render json: record, status: :created, with a Location header pointing at it. |
| 202 | :accepted | Accepted | |
| 203 | :non_authoritative_information | Non-Authoritative Information | |
| 204 | :no_content | No Content | An answer with no body at all. head :no_content is the whole of a destroy action that has nothing to say. |
| 205 | :reset_content | Reset Content | |
| 206 | :partial_content | Partial Content | |
| 207 | :multi_status | Multi-Status | |
| 208 | :already_reported | Already Reported | |
| 226 | :im_used | IM Used | |
| 3xx Redirection | |||
| 300 | :multiple_choices | Multiple Choices | |
| 301 | :moved_permanently | Moved Permanently | A permanent move, and what a renamed route should answer. redirect_to path, status: :moved_permanently. |
| 302 | :found | Found | What a bare redirect_to sends. Fine after a form submit, wrong for a route you have renamed for good. |
| 303 | :see_other | See Other | What the scaffold passes after a Turbo-driven DELETE - redirect_to path, status: :see_other - so the follow-up request is a GET rather than another DELETE. |
| 304 | :not_modified | Not Modified | |
| 305 | :use_proxy | Use Proxy | |
| 307 | :temporary_redirect | Temporary Redirect | A temporary move that keeps the method and body intact. |
| 308 | :permanent_redirect | Permanent Redirect | A permanent move that keeps the method and body intact - the one to use when an API endpoint moves. status: :permanent_redirect. |
| 4xx Client error | |||
| 400 | :bad_request | Bad Request | |
| 401 | :unauthorized | Unauthorized | Not authenticated. Devise answers this for an API request with no valid session; it means sign in, not you may not. |
| 402 | :payment_required | Payment Required | |
| 403 | :forbidden | Forbidden | Authenticated and still refused. This is the status a Pundit::NotAuthorizedError should become, via config.action_dispatch.rescue_responses. |
| 404 | :not_found | Not Found | What ActiveRecord::RecordNotFound becomes, with no rescue needed. Also the right answer for a route you want hidden rather than refused. |
| 405 | :method_not_allowed | Method Not Allowed | |
| 406 | :not_acceptable | Not Acceptable | |
| 407 | :proxy_authentication_required | Proxy Authentication Required | |
| 408 | :request_timeout | Request Timeout | |
| 409 | :conflict | Conflict | |
| 410 | :gone | Gone | |
| 411 | :length_required | Length Required | |
| 412 | :precondition_failed | Precondition Failed | |
| 413 | :content_too_largeformerly :payload_too_large | Content Too Large | |
| 414 | :uri_too_long | URI Too Long | |
| 415 | :unsupported_media_type | Unsupported Media Type | |
| 416 | :range_not_satisfiable | Range Not Satisfiable | |
| 417 | :expectation_failed | Expectation Failed | |
| 421 | :misdirected_request | Misdirected Request | |
| 422 | :unprocessable_contentformerly :unprocessable_entity | Unprocessable Content | A validation failure. Rails maps ActiveRecord::RecordInvalid here, and Turbo needs this status to re-render a form with its errors. Canonically :unprocessable_content since Rack 3.1. |
| 423 | :locked | Locked | |
| 424 | :failed_dependency | Failed Dependency | |
| 425 | :too_early | Too Early | |
| 426 | :upgrade_required | Upgrade Required | |
| 428 | :precondition_required | Precondition Required | |
| 429 | :too_many_requests | Too Many Requests | What Rails 8's rate_limit answers when a client goes over the limit. Send a Retry-After header with it. |
| 431 | :request_header_fields_too_large | Request Header Fields Too Large | |
| 451 | :unavailable_for_legal_reasons | Unavailable For Legal Reasons | |
| 5xx Server error | |||
| 500 | :internal_server_error | Internal Server Error | An unhandled exception. Anything that reaches here is a bug rather than a state the client can fix. |
| 501 | :not_implemented | Not Implemented | |
| 502 | :bad_gateway | Bad Gateway | |
| 503 | :service_unavailable | Service Unavailable | What a maintenance page answers, and what a queue backed up beyond its timeout should answer, so a crawler comes back rather than dropping the URL. |
| 504 | :gateway_timeout | Gateway Timeout | |
| 505 | :http_version_not_supported | HTTP Version Not Supported | |
| 506 | :variant_also_negotiates | Variant Also Negotiates | |
| 507 | :insufficient_storage | Insufficient Storage | |
| 508 | :loop_detected | Loop Detected | |
| 511 | :network_authentication_required | Network Authentication Required | |