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.

All tools
Trace a URL's status codes and redirects
The full address, scheme included. It has to answer from the public internet, so point this at staging if the app only runs on your machine.

We make a real request to your host, so this can take a few seconds.

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.

CodeRack/Rails symbolReason phraseIn Rails
1xx Informational
100:continueContinue
101:switching_protocolsSwitching Protocols
102:processingProcessing
103:early_hintsEarly Hints
2xx Success
200:okOK
201:createdCreatedWhat a create answers when it made something. render json: record, status: :created, with a Location header pointing at it.
202:acceptedAccepted
203:non_authoritative_informationNon-Authoritative Information
204:no_contentNo ContentAn answer with no body at all. head :no_content is the whole of a destroy action that has nothing to say.
205:reset_contentReset Content
206:partial_contentPartial Content
207:multi_statusMulti-Status
208:already_reportedAlready Reported
226:im_usedIM Used
3xx Redirection
300:multiple_choicesMultiple Choices
301:moved_permanentlyMoved PermanentlyA permanent move, and what a renamed route should answer. redirect_to path, status: :moved_permanently.
302:foundFoundWhat a bare redirect_to sends. Fine after a form submit, wrong for a route you have renamed for good.
303:see_otherSee OtherWhat 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_modifiedNot Modified
305:use_proxyUse Proxy
307:temporary_redirectTemporary RedirectA temporary move that keeps the method and body intact.
308:permanent_redirectPermanent RedirectA 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_requestBad Request
401:unauthorizedUnauthorizedNot authenticated. Devise answers this for an API request with no valid session; it means sign in, not you may not.
402:payment_requiredPayment Required
403:forbiddenForbiddenAuthenticated and still refused. This is the status a Pundit::NotAuthorizedError should become, via config.action_dispatch.rescue_responses.
404:not_foundNot FoundWhat ActiveRecord::RecordNotFound becomes, with no rescue needed. Also the right answer for a route you want hidden rather than refused.
405:method_not_allowedMethod Not Allowed
406:not_acceptableNot Acceptable
407:proxy_authentication_requiredProxy Authentication Required
408:request_timeoutRequest Timeout
409:conflictConflict
410:goneGone
411:length_requiredLength Required
412:precondition_failedPrecondition Failed
413:content_too_large
formerly :payload_too_large
Content Too Large
414:uri_too_longURI Too Long
415:unsupported_media_typeUnsupported Media Type
416:range_not_satisfiableRange Not Satisfiable
417:expectation_failedExpectation Failed
421:misdirected_requestMisdirected Request
422:unprocessable_content
formerly :unprocessable_entity
Unprocessable ContentA 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:lockedLocked
424:failed_dependencyFailed Dependency
425:too_earlyToo Early
426:upgrade_requiredUpgrade Required
428:precondition_requiredPrecondition Required
429:too_many_requestsToo Many RequestsWhat 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_largeRequest Header Fields Too Large
451:unavailable_for_legal_reasonsUnavailable For Legal Reasons
5xx Server error
500:internal_server_errorInternal Server ErrorAn unhandled exception. Anything that reaches here is a bug rather than a state the client can fix.
501:not_implementedNot Implemented
502:bad_gatewayBad Gateway
503:service_unavailableService UnavailableWhat 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_timeoutGateway Timeout
505:http_version_not_supportedHTTP Version Not Supported
506:variant_also_negotiatesVariant Also Negotiates
507:insufficient_storageInsufficient Storage
508:loop_detectedLoop Detected
511:network_authentication_requiredNetwork Authentication Required

Questions about status codes in a Rails app

Rack 3.1 renamed two symbols to match the current HTTP specification: :unprocessable_entity became :unprocessable_content and :payload_too_large became :content_too_large. The old names still resolve to 422 and 413, so nothing breaks, but Rack logs a deprecation warning each time one is used. The numeric codes never changed. If your app supports several Rails versions, passing the integer - status: 422 - works everywhere and says the same thing.

404 says the server has nothing at that address and does not commit to why; 410 says the resource existed and is deliberately gone. Rails answers 404 for a missing record on its own, so 404 is what you get by default and it is usually right. Reach for 410 when you have soft-deleted something you will never restore and you want crawlers to drop the URL sooner - Google treats a 410 as a stronger signal than a 404 and stops recrawling faster. Either way the record's page should not keep returning 200 with an empty shell, which is the mistake that gets a whole section thin-content flagged.

Turbo replaces a form submit with a fetch, and it only re-renders the page when the response is not a successful one. A failed create that renders the form again with status 200 leaves Turbo with nothing to swap, so the visitor sees no errors and the page appears to do nothing. Rendering the same template with status :unprocessable_content tells Turbo the submit failed and the body is the new state of the form. This is why the Rails scaffold has used 422 on the failure branch since Turbo became the default.

redirect_to defaults to 302, which tells a crawler the move is temporary and to keep asking for the old URL. When you rename a route for good, pass status: :moved_permanently so the old address transfers its signals to the new one. Use 308 instead when the redirected request may carry a body - 301 and 302 historically allowed a client to turn a POST into a GET, and 307 and 308 forbid it. For a GET-only marketing URL 301 is the right answer; for an API endpoint you have moved, 308 is.

Start creating your next app now