How HTTP Works

Request and response header

< Via: 1.1 varnish, 1.1 envoy

Via Header

Learn how the Via header tracks the path of HTTP requests through proxies and gateways. Debug routing issues and understand your network infrastructure.

Reviewed 6 min readintermediate4 sourcesTry itMarkdown
Direction
Request and response
Category
Proxies
Spec
RFC 9110 §7.6.3
JS can set it
No: forbidden header name
On this page

TL;DR: Via is a list of the proxies and gateways that forwarded an HTTP message, in order, like Via: 1.1 proxy-a, 1.1 proxy-b. Each entry gives the HTTP version that hop received and its hostname or a pseudonym. Requests and responses build separate lists. It doesn’t carry the client’s IP; that’s what Forwarded is for.

What is Via?

Every proxy has to add a Via entry to each message it forwards; RFC 9110 §7.6.3 makes that a requirement. Gateways (reverse proxies) must add one to requests but only may add one to responses. That’s why the response you see at the client can be missing intermediaries that handled the request.

How Via Works

Each intermediary appends its own entry to the end of the list. The list describes the path of this one message, not a full round trip.

For a request that goes client → proxy-a → proxy-b → origin, the origin might receive this (examples on this page are trimmed):

Via: 1.1 proxy-a, 1.1 proxy-b

On the way back, the client might see:

Via: 1.1 proxy-b, 1.1 proxy-a

The order flips because the response builds its own list. The origin doesn’t copy the request’s Via into the response; each proxy appends as the response passes back through.

Syntax

Each entry has the protocol the hop received, an identifier for the hop, and an optional comment. For HTTP the protocol name can be left off, so 1.1 means HTTP/1.1. Some valid values:

Via: 1.1 proxy-a
Via: HTTP/1.1 proxy-a:8080 (gateway)
Via: 1.1 proxy-a, 1.1 proxy-b

The identifier (received-by) is a host with an optional port, or a pseudonym. The comment is free text; you don’t have to put your software version there.

Proxy Implementation

In Apache, the ProxyVia directive controls this. With mod_proxy and mod_proxy_http loaded, add it to a virtual host:

ProxyVia On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/

The default is ProxyVia Off, which passes existing Via headers through untouched but adds no entry of its own. On adds an entry to every request and reply. Full does the same and puts the Apache version in a comment. Block strips Via from proxied requests and adds nothing. Since the RFC requires proxies to add an entry, On or Full is what matches the spec.

To see what Apache does, send a request with your own first entry:

curl -sS -D - -o /dev/null \
  -H 'Via: 1.1 client-test' http://localhost/api/data

With ProxyVia On, the origin’s request log should show client-test followed by Apache’s entry. The response’s Via describes the return trip, so it won’t start with client-test unless your application copied the header over. And ProxyPassReverse only rewrites redirect headers like Location; it has nothing to do with Via.

Setting a fixed Via value with a header directive is a tempting shortcut that gets it wrong twice. It replaces the upstream entries instead of appending, and a hard-coded 1.1 is wrong whenever the proxy received a different HTTP version. Use the proxy’s built-in Via support if it has one, and check both directions.

Analyzing Via Headers

Splitting Via on commas breaks as soon as a comment contains one. If you need structured entries, use a parser that understands HTTP comment syntax. And don’t read the entry count as a hop count; intermediaries that skip or merge entries make it unreliable.

For example, this is one entry, not two:

Via: 1.1 edge (regional gateway, west)

Log the raw header too, so a parser bug doesn’t lose the evidence. In an Express origin, log the incoming request’s Via, and check responses separately:

app.use((req, res, next) => {
  console.log(JSON.stringify({
    method: req.method,
    path: req.path,
    receivedVia: req.get('Via') ?? null,
  }));
  next();
});

req.get() reads the header case-insensitively. The log shows what arrived, and anyone can send any Via value, so it’s a record of claims, not of which proxies really handled the request. Writing it as JSON stops a crafted value from breaking your log format.

Privacy Considerations

If your internal hostnames are sensitive, the RFC lets you use pseudonyms instead. It also lets an organization merge entries with the same protocol, under specific conditions, for hops behind its firewall. What it doesn’t do is let any proxy throw away the list whenever it likes.

A pseudonym keeps the forwarding record without exposing an internal DNS name. Here’s a request’s Via before and after an edge appends its chosen name:

Via: 1.1 branch-gateway
Via: 1.1 branch-gateway, 1.1 public-edge

Pick a stable name your ops team can map back to a real system. A random name per request makes logs impossible to compare, and a private hostname gives away your topology for no reason. Never base access control on it, since any caller can send the same name.

Getting Via right

The version in each entry is the protocol that hop received, not the one it uses for the next connection. A proxy that gets HTTP/1.1 and forwards over HTTP/2 writes 1.1.

Debugging with Via

Check requests in your origin logs and responses at the client; they’re different lists. Swap in your own proxied endpoint:

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

Via has no timestamps or per-hop timings. It tells you which route to look at, but for latency you need actual measurements.

When one request works and a similar one fails, compare the origin logs and the client-side headers for both. A different intermediary in one of them is a good lead. The same list in both doesn’t mean they hit the same backend or saw the same state, though. Combine Via with your own request IDs, server logs, and timings.

Via is about intermediaries, not about who the client is. The two often appear together:

Via: 1.1 public-edge
Forwarded: for=192.0.2.10;proto=https;host=example.com

Via names the intermediary and the protocol it received. Forwarded carries the client’s address and original scheme. Whether you can trust Forwarded depends on which proxy set it and whether it strips values sent by clients; a Via entry doesn’t vouch for it. See RFC 7239, the specification for Forwarded.

Security Considerations

Via is how proxies spot loops: if a request already contains your own hostname or pseudonym, you may be about to forward it to yourself. RFC 9110 §7.6 says a proxy must not forward to itself unless it has some protection against infinite loops, but it doesn’t set an entry-count limit. Remember that clients can send any Via they like, so treat a repeated name as a signal to check, not as proof.

When you investigate a suspected loop, look at where the proxy forwards next, not only at the header it received. Your own pseudonym in a request is worth checking, but a caller could have typed it. And a loop through a gateway that adds no response entry won’t show up as a repeat at the client. In the end, your forwarding configuration is what tells you whether a destination points back to the same service.

A throwaway test endpoint helps here. Send one request with Via: 1.1 client-test and one without, and compare what the origin receives. If both arrive with only the edge’s entry, something on the path replaces the list. If client-test survives with the edge appended, the path preserves it.

HTTP/2 Considerations

Via means the same thing in HTTP/2 and HTTP/3. Each entry records what one intermediary received; there’s no single end-to-end version. Browser JavaScript can’t set Via on a fetch request, because it’s a forbidden request header.

A request that crossed protocols might carry:

Via: 2.0 edge, 1.1 internal-gateway

The edge received HTTP/2 and the internal gateway received HTTP/1.1. It says nothing about the protocol between the gateway and the origin. To look at the response over HTTP/2 from the client, use a curl build with HTTP/2 support:

curl --http2 -v -o /dev/null https://example.com/api/data

curl shows the protocol of your own connection; Via shows what each intermediary received. They answer different questions, so keep both when you trace protocol conversion through a gateway.

Frequently asked questions

What is the Via header?

Via lists the proxies and gateways that forwarded an HTTP message, in order. Requests and responses each build their own list, so the response you see may not show every hop the request took.

What information does Via contain?

Each entry contains the received protocol and version, a host with an optional port or a pseudonym, and optionally a comment.

Why is Via important?

It shows which intermediaries handled a message and which HTTP version each one received. That helps you debug routing and lets proxies detect forwarding loops. It has no timing data, so it cannot tell you where latency comes from.

Should I strip Via headers?

Usually no. RFC 9110 requires proxies to add Via to messages they forward. If an internal hostname is sensitive, replace it with a pseudonym; the RFC also allows merging some entries behind a firewall.

Sources

  1. MDN Web Docs: Viadeveloper.mozilla.org
  2. RFC 9110: Viarfc-editor.org
  3. Apache httpd: ProxyViahttpd.apache.org
  4. Fetch Standard: Forbidden request headersfetch.spec.whatwg.org

Keep going

Browse /search