Example: Downloading Behind a Login Cookie
curl -L -b 'session=abc123' -o invoice.pdf \
'https://portal.example.com/billing/invoice/9812/download'
-bsends a cookie read-style;-cwrites the jar. For flows that need two hops (login then download), use-c jar.txtthen-b jar.txt.-Lmatters more here than anywhere: download endpoints are usually one redirect from the page URL you copied.- The gotcha: if the server re-issues
Set-Cookieduring the redirect and you didn't use a jar, hop two is unauthenticated and you silently download the login page named invoice.pdf. Converted code needs a session/cookie-jar object for the same behavior —requests.Session()in Python, aCookieJarin Node fetch is manual.
Check what you actually downloaded before opening it: file invoice.pdf will tell you it's an HTML document in one keystroke.
Cookie flags, precisely
-b 'name=value'sends that cookie inline.-b jar.txtreads a Netscape cookie file. Same flag, two shapes — curl checks whether the argument contains=.-c jar.txtwrites the jar at the end of the transfer, including cookies received mid-redirect.-b jar.txt -c jar.txtin one command is the two-hop pattern: seed the jar, then update it.-cwritesHttpOnlyandSecureattributes as comments in the jar; a hand-rolled cookie string copied from DevTools silently dropsHttpOnly, which is fine for curl (it isn't a browser) but matters when you later reproduce the flow in a browser script.- Domain and path scoping is real: a cookie set for
/billingwill not be sent to/api, even from the same host. If your converted Node code sends the cookie everywhere and works, it's working by accident until an endpoint rejects it.
The login-then-download flow, end to end
# 1. authenticate, capture the re-issued session cookie
curl -sS -c jar.txt -o /dev/null -w '%{http_code}\n' \
-X POST 'https://portal.example.com/login' \
--data-urlencode 'email=me@example.com' \
--data-urlencode 'password=<password>'
# 2. download with the jar; -L because the download URL redirects
curl -sS -b jar.txt -L -o invoice.pdf -w '%{http_code} %{size_download}\n' \
'https://portal.example.com/billing/invoice/9812/download'
# 3. prove it
test "$(file -b --mime-type invoice.pdf)" = application/pdf || \
{ echo 'not a PDF — likely the login page'; head -c 200 invoice.pdf; exit 1; }
--data-urlencodefor credentials, not-d: a password containing&,+, or%is silently mis-encoded by-dand you get a login failure that looks like a wrong password. This is the same class of damage as the newline-eating in the flags guide.- Many portals CSRF-protect the login form. The flow then needs a GET first to pick up the CSRF token from the form, and a jar across both hops. If step 1 returns
403while the browser works, that's why. %{size_download}in step 2 is a cheap sanity check: a login page is a few KB, an invoice is not. Combined withfile, it catches the silent-success bug without opening the PDF.
Why -L is load-bearing here and not elsewhere
On a download endpoint, the redirect is usually the app bouncing you from the page URL to the signed storage URL (/download → /s3/invoice-9812?sig=...). Without -L you save the redirect stub — a ~200-byte HTML page named invoice.pdf. With -L but no jar, the second hop is unauthenticated and the app bounces you to login, which 200s. Both failures produce a file with the right name and the wrong bytes; only the mime check separates them.
FAQ
How do I find the right cookie to copy? DevTools → Application → Cookies, or run curl -i on the page and read the Set-Cookie headers. Send only the session cookie, not analytics noise.
Why did -o save an HTML login page instead of the PDF? You hit the redirect without the re-issued cookie. Use a jar: curl -c jar.txt -b jar.txt -L ... so both hops share cookie state.
Is exporting cookies from DevTools safe? The value is your session — treat it like a password. Paste it only where you need it and rotate the session afterwards if the machine isn't yours.