Geonode logo
Geonode Team

Geonode Team

Updated: September 2, 2026

Published: 2026-09-02

How to Show Response Headers with curl

curl hides response headers by default. Four options reveal them, and they differ in ways that matter more than the documentation suggests. One of them sends a different request than the one you are trying to debug, which is a fine way to spend an hour chasing a discrepancy that does not exist. This guide covers all four, plus the JSON output most people do not know about and how the picture changes through a proxy.

Our reason for caring: we are Geonode and we sell proxies, and response headers are the fastest way to answer the question customers ask us most — is this a proxy problem or not? A block page, a rate limit and a genuine error look identical in a browser and completely different in the headers. A 429 with Retry-After means you are going too fast and no proxy fixes that. A 403 with a security-vendor header means the target identified you. A 407 means the proxy wants credentials. Reading headers before changing anything saves a great deal of guessing, and curl has a flag for the specific case — %{proxy_used}, added in 8.7.0, which returns 1 if the transfer went through a proxy. Useful when you are not certain your configuration took effect.

The Four Options at a Glance

OptionShowsSendsBest for
-iResponse headers + bodyYour actual requestEveryday inspection
-IResponse headers onlyA HEAD requestQuick checks, with a caveat
-D fileResponse headers to a fileYour actual requestScripting, separating streams
-vRequest and response headersYour actual requestDebugging what you sent

The critical row is the second one, and it is the source of most confusion in this area. Everything else is a matter of where the output goes.

-i: Headers With the Body

The everyday option. The curl manual documents it as -i, --show-headers: "Show response headers in the output... This option makes the response headers get saved in the same stream/output as the data."

curl -i https://example.com
HTTP/2 200
content-type: text/html; charset=UTF-8
content-length: 1256
cache-control: max-age=604800
date: Wed, 02 Sep 2026 10:14:22 GMT

<!doctype html>...

Headers, a blank line, then the body — the same structure as the wire format.

A naming note that catches people reading older material: the long form is now --show-headers. It used to be --include, and both work, but current documentation uses the newer name.

Two details worth knowing. When output goes to a terminal, curl may style header names in bold and mark Location: URLs, which is helpful interactively and unwanted in a pipeline — --no-styled-output disables it. And because headers and body share a stream, -i with -o file writes both into the file, which is almost never what you want. Use -D for that case.

-I: Headers Only, and Why It Can Mislead

-I is documented as: "Fetch the headers only. HTTP-servers feature the command HEAD which this uses to get nothing but the header of a document."

Read that carefully. It does not fetch the response and discard the body. It sends a different HTTP method.

curl -I https://example.com

That is a HEAD request, and the consequences are real:

Some servers handle HEAD differently. A HEAD may return different headers, a different status code, or be rejected entirely with a 405 Method Not Allowed — while the equivalent GET works perfectly.

Some frameworks do not compute the body for HEAD, so Content-Length, ETag and Content-Type may be absent or wrong.

CDNs and caches frequently treat HEAD as a distinct cache key, so cache headers can differ from what a real request would see.

Anti-bot systems may respond differently. A HEAD from an unusual client is itself a signal, and the challenge you get may not be the challenge a GET would produce.

So -I is excellent for quick checks — is this URL alive, what does it redirect to, how big is the file — and unreliable for debugging why a GET behaves oddly. When you are diagnosing a real request, use the real method:

curl -sS -o /dev/null -D - https://example.com

That performs a normal GET, discards the body to /dev/null, and dumps headers to standard output. It gives you what -I appears to give you, without changing the request.

If you need headers from a POST specifically, the same pattern applies:

curl -sS -o /dev/null -D - -X POST -H "Content-Type: application/json" \
     -d '{"a":1}' https://api.example.com/items

-D and -v: Separating Streams and Seeing Requests

-D writes headers to a separate destination. The manual: "Write the received protocol headers to the specified file... Specify '-' as filename (a single minus) to have it written to stdout." It also notes that if no headers are received, the option "creates an empty file" — which is itself diagnostic information.

curl -D headers.txt -o body.html https://example.com

Clean separation, which is what you want in scripts. -D - sends headers to stdout while the body goes wherever -o points, and that combination is the basis of the pattern above.

-v shows the request as well, which is frequently the half you actually need. The manual explains the prefixes precisely:

Verbose output lines are prefixed with letters: > header sent by curl, < header received by curl, } data sent by curl, { data received by curl, * additional info provided by curl.

curl -v https://example.com 2>&1 | grep '^>'

That gives you exactly what curl transmitted — which is often not what you configured, because libraries, defaults and .curlrc files all add and override headers. A great many "the server is ignoring my header" problems resolve here.

Note that verbose output goes to stderr, which is why the 2>&1 is needed before piping. That is deliberate: it keeps the body clean on stdout.

The manual also flags that since curl 8.10, repeating -v increases the trace level. For genuinely low-level work, --trace-ascii gives "a full trace dump of all incoming and outgoing data, including descriptive information", with the hex omitted so it stays readable.

One warning the manual gives that is worth repeating: trace and verbose output "might contain sensitive data, including usernames, credentials or secret data content. Be aware and be careful when sharing trace logs with others." Proxy credentials passed in a URL appear in verbose output. Redact before pasting into an issue tracker.

Machine-Readable Headers with %{header_json}

The option most people have never seen, added in curl 7.83.0, and the correct answer whenever you were about to write a regular expression against header text.

The manual describes it as "A JSON object with all HTTP response headers from the recent transfer. Values are provided as arrays, since in the case of multiple headers there can be multiple values." Header names come through "in lowercase, listed in order of appearance over the wire", with duplicates "grouped on the first occurrence of that header, each value is presented in the JSON array".

curl -s -o /dev/null -w '%{header_json}' https://example.com | jq
{
  "content-type": ["text/html; charset=UTF-8"],
  "cache-control": ["max-age=604800"],
  "set-cookie": ["a=1; Path=/", "b=2; Path=/"]
}

Three things this fixes at once. Names are normalised to lowercase, so no case-insensitive matching. Repeated headers such as Set-Cookie come through as arrays rather than being silently collapsed. And the output is parseable without writing a parser.

Extracting a single header becomes trivial:

curl -s -o /dev/null -w '%{header_json}' "$URL" | jq -r '.["retry-after"][0] // "none"'

Other -w variables that pair well with it:

curl -s -o /dev/null -w 'status=%{response_code} redirects=%{num_redirects} proxy=%{proxy_used} ip=%{remote_ip}\n' "$URL"

response_code is the status of the last transfer, num_redirects counts redirects followed, redirect_url shows where a redirect would have gone when you did not use -L, remote_ip is the address actually connected to, and proxy_used returns 1 if a proxy was involved. That last one is genuinely useful when a NO_PROXY pattern may have quietly excluded your host.

Following Redirect Chains

Without -L, curl stops at the first redirect and you see only that response. With -L, curl shows the headers of every response in the chain:

curl -sSL -o /dev/null -D - https://example.com
HTTP/2 301
location: https://www.example.com/

HTTP/2 200
content-type: text/html

Each block is one hop. This is how you find out that a URL redirects three times, that one hop drops to plain HTTP, or that a redirect loses a cookie.

Two patterns worth keeping:

curl -sSL -o /dev/null -w '%{num_redirects} hops -> %{url_effective}\n' "$URL"

The count and the final destination in one line. And when you want to see where a redirect points without following it:

curl -s -o /dev/null -w '%{redirect_url}\n' "$URL"

Redirect chains are worth inspecting more often than people do. Each hop is a round trip, a chain of four adds real latency, and an unexpected hop through a different host is frequently the explanation for a cookie or CORS problem.

Headers Through a Proxy

Two additions to the picture, both of which cause confusion the first time.

CONNECT responses appear in verbose output. For HTTPS through an HTTP proxy, curl first issues a CONNECT to establish a tunnel, and that exchange has its own headers:

curl -v -x http://proxy.example.com:8080 https://example.com

You will see the CONNECT, a HTTP/1.1 200 Connection established from the proxy, and only then the real request. That first block is the proxy talking, not the target. Confusing them is a common early mistake. --suppress-connect-headers removes them from the output when you only care about the target's response.

%{http_connect} reports the proxy's response code to the CONNECT specifically, separately from the target's status. That distinction is precisely what you need when something fails and you do not know which hop refused:

curl -s -o /dev/null -x "$PROXY" \
  -w 'connect=%{http_connect} status=%{response_code} proxy=%{proxy_used}\n' \
  https://example.com

connect=200 status=403 means the proxy worked and the target refused you. connect=407 means the proxy wanted credentials and never reached the target. Those two situations have completely different fixes, and without this distinction they look identical from the application's point of view.

Also note that for HTTPS through a tunnel, the proxy cannot add or read headers — it is relaying encrypted bytes. If you see unexpected headers on an HTTPS response, they came from the target or from a CDN in front of it, not from the proxy.

What the Headers Actually Tell You

The point of all this. Reading them well turns guessing into diagnosis.

Status line first. 200 succeeded. 301/302 redirected. 403 refused. 429 rate-limited. 407 proxy authentication. 502/503 upstream trouble.

Retry-After appears with 429 and 503 and tells you exactly how long to wait. Honouring it is both correct and the fastest route back to working. Ignoring it and retrying immediately is how a temporary limit becomes a longer one.

Content-Type tells you what you actually received. text/html on an API endpoint means you got an error page, not JSON, and it is the answer to a large share of parse failures.

Content-Length versus what arrived. A short body with a large declared length means truncation.

Cache headersCache-Control, ETag, Last-Modified — tell you whether you can avoid re-fetching. If-None-Match and If-Modified-Since on subsequent requests turn a full transfer into a 304, which on metered bandwidth is a direct saving.

Set-Cookie shows what session state the server is establishing, and its absence where you expected one explains a lot of authentication puzzles.

Server and vendor-specific headers identify what is in front of the origin. A response bearing a security vendor's headers with a 403 tells you the block came from a protection layer rather than the application — a different problem with a different response.

Non-standard headers. Rate-limit budgets, request identifiers, and API-specific metadata often appear as x-prefixed headers, and they are frequently the most useful thing in the response. A request ID is what support will ask for.

People Also Ask

How do I see response headers with curl?

curl -i URL shows headers followed by the body. curl -D - URL writes headers to stdout separately. curl -v URL shows both the request and the response headers. Avoid -I when debugging a real request, because it sends a HEAD rather than a GET.

What is the difference between -i and -I in curl?

-i includes response headers alongside the body of your actual request. -I sends a HEAD request instead, so it is a different request with potentially different results. Use -i or -o /dev/null -D - when you need headers from the request you are actually debugging.

How do I see only the headers without the body?

curl -sS -o /dev/null -D - URL. This performs a normal GET, discards the body, and prints the headers. It gives you what -I appears to give you without changing the HTTP method, which matters because some servers respond to HEAD differently or reject it outright.

How do I see the request headers curl sends?

curl -v URL and look for lines beginning with >, which are headers curl sent. Verbose output goes to stderr, so pipe with 2>&1 if you want to filter it. This is how you confirm that the header you configured is actually on the wire.

How do I get curl headers as JSON?

curl -s -o /dev/null -w '%{header_json}' URL. Added in curl 7.83.0, it emits all response headers as a JSON object with lowercase names and array values, so repeated headers such as Set-Cookie are preserved rather than collapsed. Pipe to jq to extract fields.

Why do I see two sets of headers when using a proxy?

For HTTPS through an HTTP proxy, curl first sends a CONNECT to open a tunnel, and the proxy's response to that appears before the target's. Use --suppress-connect-headers to hide it, or %{http_connect} to read the proxy's status code separately from the target's.

How do I see headers for each redirect?

Add -L so curl follows redirects, and use -D - or -i — curl prints the headers of every response in the chain, one block per hop. %{num_redirects} and %{url_effective} give you the count and the final URL in a single line.

Do response headers reveal whether I am being blocked?

Often, yes, and more reliably than the body does. A 429 with Retry-After is a rate limit. A 403 carrying a security vendor's headers is a protection layer. A 200 with Content-Type: text/html on an API endpoint is a challenge or login page. Each implies a different fix, and only the headers distinguish them.

Wrapping Up

Four options and one common trap. -i for everyday inspection, -D - when you want headers separate from the body, -v when you need to see what you sent as well as what came back, and -I only for quick liveness checks — because it sends a HEAD, and servers are entitled to answer a HEAD differently from a GET.

The option worth adopting if you take one thing from this is %{header_json}. Any script currently parsing header text with a regular expression should be using it instead: lowercase names, arrays for repeated headers, and output jq can read. Paired with %{response_code}, %{num_redirects} and %{proxy_used}, it turns header inspection into something you can assert on rather than eyeball.

And when a request goes wrong, read the headers before changing anything. The status code, Retry-After, Content-Type and any vendor headers between them usually name the problem outright — which beats swapping settings until something works, and takes about ten seconds.

Show Response Headers with curl: -i -I -D -v and header_json | Geonode