DEV Community

Cover image for "Rails Magic" Isn't Magic. How a Request Actually Travels Through Rails.

"Rails Magic" Isn't Magic. How a Request Actually Travels Through Rails.

S M Tahosin on October 19, 2026

Ask any developer who has worked with modern web frameworks what they think of Ruby on Rails, and you will almost certainly hear the same word with...
Collapse
 
kaj_ilands profile image
Kaj •

The CSRF check is the one stop on this trip that compares instead of routes: verify_authenticity_token runs as a before_action, reads the session's _csrf_token, unmasks the token from the form, and compares. It's session-bound, not page-bound, which is why the same masked token keeps working for the life of the session.

I lost a week to the flip side of that. I was scripting a POST and reading the token straight out of the raw HTML, and the comment form's authenticity_token is the literal string NOTHING until JS fills it in. So I was posting the word NOTHING and getting 422 Invalid authenticity token back, like my session was broken. It never was. The token has to come from the same session you POST with, and from a field that isn't a placeholder.

Wrote the whole trap up here, in case it saves someone else the week: dev.to/kaj_ilands/your-scripted-po...

Collapse
 
smtahosin profile image
S M Tahosin •

Thanks Kaj, good addition. CSRF is the one stop in the request path that compares instead of routes, and it is session-bound, so a placeholder token in the HTML will always fail. Useful tip for anyone scripting POST requests.

Collapse
 
kaj_ilands profile image
Kaj •

the tripwire is usually the order you do it in, not the token itself. scrape the form, sign in, then post, and the token is already dead: sign-in rotates the session (devise and most apps do it deliberately, for session fixation), and the _csrf_token lives in the session you just dropped. fetch the page after you're logged in, out of the same jar.

two more that hand you a 422 with a token that looks perfect:

  • per_form_csrf_tokens (on in plenty of upgraded apps): the token is scoped to that form's action and method, so the one from /articles/new is not the one for PATCH /articles/:id.
  • a token carried over from a jar you didn't keep sending. same string, different session, fails.

and before rewriting the scrape, read the server log for Can't verify CSRF token authenticity. The 422 body is empty by default, so it reads like a routing problem. The log line tells you which of the three you actually have.

Collapse
 
emilywoodsnyc profile image
Emily Woods •

Your request walkthrough would make a good debugging exercise: send one request that routing rejects, one that a controller rejects, and one that fails during the database query. Then trace how each response gets back out. I'd pair the first step with prachub's HTTP-router question. Explaining those three failures would tell me more than memorizing the names of every middleware layer.

Collapse
 
smtahosin profile image
S M Tahosin •

Great idea, Emily. That would make a good exercise. Routing rejects raise RoutingError, a controller rejects with something like ParameterMissing (400), and a failed query raises RecordNotFound (404) or StatementInvalid (500). All three travel back out through the same middleware (ShowExceptions) and become the response. Tracing those paths teaches more than memorizing the middleware list. I'll consider adding it as a follow-up.

Collapse
 
emilywoodsnyc profile image
Emily Woods •

Thanks for spelling out the three paths. I’d finish the exercise with a small table showing where the exception started and which response the client actually saw. That gives me something concrete to compare with my prediction, instead of only copying a middleware diagram.

Collapse
 
alexcodebytes profile image
Oleksandr •

The view_assigns explanation is the best one I've read. I've explained Rails to juniors for years and always stumbled on "how does @article reach the view" — now I have the exact code path: instance_variable_set("@#{key}", value).

One thing that took me years to appreciate: the GeneratedAttributeMethods module isn't just clever — it's why article.title doesn't override your custom def title; "custom #{super}"; end. Most ORMs generate methods on the class itself, which means super doesn't work. Rails puts them in an included module specifically so super still finds the database column. That design choice is invisible until you need it, then it saves you.

Curious about your production story — have you ever hit the ActiveRecord::ConnectionTimeoutError due to a pool mismatch? Asking because that's the one most teams I've seen get wrong on first deploy.

Collapse
 
smtahosin profile image
S M Tahosin •

Thanks Oleksandr, and great point about super. Because the generated methods live in an included module, you can override title and call super to reach the real column value. As for ConnectionTimeoutError, a pool smaller than Puma's max threads is one of the most common first-deploy mistakes. Keeping pool equal to RAILS_MAX_THREADS avoids it.

Collapse
 
alexcodebytes profile image
Oleksandr •

I'd add one caveat: overriding a generated method with super works, but it can break if the underlying column name changes. It's a powerful escape hatch, but it couples your override to the schema. Worth mentioning as a trade-off.

Collapse
 
joel_maniego_665deba00877 profile image
Joel Maniego •

Thank you. It was really helpful for ROR developer.

Collapse
 
smtahosin profile image
S M Tahosin •

Thanks Joel, glad it helped!

Collapse
 
joel_maniego_665deba00877 profile image
Joel Maniego •

SM Tahosin. This is my individual question.
How do you think of the US IT marketplace? Are you fine working as CEO in Bangladesh?