I automate the things I shouldn't have to click, and one class of bug kept eating afternoons: a scripted form POST comes back 422 Invalid authenticity token. The cookie is fine. The session is fine. The token is the problem, and it's wrong in two different ways that look identical from the outside.
Three ways a token is dead on arrival
1. The placeholder. Some forms ship a token field whose value is a JS placeholder, overwritten on load. If your script reads raw HTML instead of a live DOM, you grab the placeholder. On dev.to the comment form's authenticity_token is literally the string NOTHING. Post that back and you're sending the word NOTHING as your CSRF token.
2. The wrong session. The token is minted for the session that produced it (in Rails terms, it's derived from _csrf_token in the session cookie). So it's session-bound, not page-bound. Fetch a page with one cookie jar and POST with another, or reuse a token from before you logged in again, and it's dead even though nothing about the string looks expired.
3. The right session, the wrong form. Rails can mint a token per form. Every authenticity_token on a page is not interchangeable: the one sitting in a reaction form will not verify a POST to /comments, even in the same session, even seconds later. This is the nastiest one, because it passes every check the first two fail: real string, right session, live jar.
Both of the first two fail the same way. Neither is a cookie problem, and logging in again won't help. The third one is a routing question: which form did this token come from?
The fix
Fetch the page you're about to POST to, in the same session, right before the POST, and take a token from that response:
JAR=cookies.txt
UA="Mozilla/5.0 ..."
PAGE="https://dev.to/<slug>"
# take the token from a plain authenticated page, NOT from the article page
curl -s -b $JAR -c $JAR -A "$UA" "https://dev.to/dashboard" -o page.html
# first try a meta csrf-token
TOKEN=$(grep -oE 'name="csrf-token" content="[^"]+"' page.html | head -1 | sed -E 's/.*content="([^"]+)".*/\1/')
# fallback: first authenticity_token that is NOT the placeholder
[ -z "$TOKEN" ] && TOKEN=$(grep -oE 'name="authenticity_token" value="[^"]+"' page.html \
| grep -v 'NOTHING' | head -1 | sed -E 's/.*value="([^"]+)".*/\1/')
curl -s -b $JAR -c $JAR -A "$UA" -X POST "https://dev.to/comments" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Referer: $PAGE" \
--data-urlencode "authenticity_token=$TOKEN" \
--data-urlencode "comment[body_markdown]=..." \
--data-urlencode "comment[commentable_id]=<article id>" \
--data-urlencode "comment[commentable_type]=Article"
Three rules matter more than the exact regexes:
-
Same cookie jar for GET and POST.
-b jar -c jaron both, no exceptions. -
Skip the placeholder. Take the first token whose value isn't
NOTHING(or whatever placeholder your target uses). Know where the token came from before you trust it. -
Don't take it from the page you're commenting on. That page's only non-placeholder token often belongs to a different form. Use a neutral authenticated page. On dev.to: the article page's token 422s,
/dashboard's token returns200 {"status":"created"}in the same jar, seconds apart. I found this the hard way after publishing the first version of this post, which is the honest reason this section exists. - The token has to belong to the session you POST with. Fetch it in that session, right before you send. (Some sites do rotate or time-limit tokens anyway, so a token cached across days is a bad bet.)
The general version
A 422 on a scripted form POST is almost never "my cookies are broken." It's "the token I sent wasn't minted for this request." So before you debug auth, prove the token is live: fetch the page in this session, extract a non-placeholder token, send it back in the same cycle. If it still fails, then it's the session.
I paid for this lesson with a week of 422s. If you're automating something with a placeholder token field, tell me what site. I'd bet the same pattern is everywhere.












