# Host Header

> Learn how the Host header specifies the target server domain name and port for HTTP requests. Essential for virtual hosting and routing on shared servers.

Source: https://howhttpworks.com/headers/host
Last reviewed: 2026-10-05

> **TL;DR:** `Host` carries the target hostname and optional port, not a URL. HTTP/1.1 requires exactly one valid Host field; missing, duplicate, or invalid values require 400. HTTP/2 and HTTP/3 normally carry the authority in `:authority`.

`Host` identifies the target authority so servers can distinguish sites sharing an IP address. It is not the client's address and does not change which socket the client connected to. [RFC 9110 §7.2](https://www.rfc-editor.org/rfc/rfc9110#section-7.2) defines the field.

## How Host Works

This illustrative request targets the API site on an already-established connection:

```http
GET /users HTTP/1.1
Host: api.example.com
```

The server uses the target authority for routing. In a request using an absolute URI, an HTTP/1.1 proxy must use the authority from that URI and replace the received Host value when forwarding. See [RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2.2).

## Host Header Format

Illustrative valid values:

```http
Host: example.com
Host: example.com:8080
Host: 192.0.2.10:8080
Host: [2001:db8::1]:8080
```

Do not include a scheme, path, or userinfo. IPv6 literals require brackets. Port omission means the default for the scheme: 80 for `http`, 443 for `https`. An explicitly specified default port is allowed; it is not forbidden just because it is conventional.

## Virtual Hosting

In nginx, use `server_name` for name-based routing. This example belongs inside `http`; the directories must exist:

```nginx
server {
    listen 80;
    server_name blog.example.com;
    root /var/www/blog;
}
server {
    listen 80;
    server_name shop.example.com;
    root /var/www/shop;
}
```

An unmatched name goes to the default server for the listening address and port. `default_server` is a `listen` setting, not a special server name. See [nginx request processing](https://nginx.org/en/docs/http/request_processing.html).

Apache provides the same name-based routing with separate virtual hosts. With `Listen 80` already configured, these blocks route two names:

```apache
<VirtualHost *:80>
    ServerName blog.example.com
    DocumentRoot "/var/www/blog"
</VirtualHost>
<VirtualHost *:80>
    ServerName shop.example.com
    DocumentRoot "/var/www/shop"
</VirtualHost>
```

Create the document roots and configure access to them under your existing Apache policy. For an unmatched name, Apache uses the first matching address-and-port virtual host. `ServerName` does not create DNS records. [Apache's virtual-host examples](https://httpd.apache.org/docs/2.4/vhosts/examples.html) explain matching and ordering.

## Port Handling

Changing `Host: localhost:8080` while connected to port 3000 does not reconnect to 8080. Whether the application rejects a mismatch depends on its routing and validation. The request authority should describe the target URI, not an arbitrary internal backend port.

## Browser Behavior

Browser JavaScript cannot override Host through Fetch: it is a [forbidden request header](https://fetch.spec.whatwg.org/#forbidden-request-header). HTTP/2 uses `:authority` for this role, so seeing that pseudo-header instead of a textual Host line is expected.

## Server-Side Handling

Log the received value for diagnosis, but do not treat it as a configured public origin. Forwarding layers can rewrite it. In Express, proxy trust also affects whether `req.hostname` uses `X-Forwarded-Host`; see its [proxy documentation](https://expressjs.com/en/guide/behind-proxies/).

For diagnosis, this route reports the raw authority separately from Express's hostname interpretation. Add it to an existing Express app:

```javascript
app.get('/request-authority', (req, res) => {
  res.json({
    host: req.get('host'),
    hostname: req.hostname,
    protocol: req.protocol
  })
})
```

`req.hostname` omits the port; it can come from `X-Forwarded-Host` when proxy trust is enabled. Use the raw field to debug what arrived, and the configured proxy policy to decide what to trust. Neither value automatically becomes a safe public URL.

For tenant routing, prefer an explicit mapping of allowed names to tenant records. `company.example.com` can identify a tenant, but a successful hostname lookup is not authorization to access that tenant's data. Reject unknown names rather than falling through to a privileged default.

## Testing Host Headers

For HTTP, change only the routing header:

```bash
curl --http1.1 -i -H 'Host: blog.example.com' http://127.0.0.1/
```

For HTTPS, keep the hostname in the URL and override DNS with curl's [`--resolve`](https://curl.se/docs/manpage.html#--resolve):

```bash
curl --http1.1 --resolve blog.example.com:443:127.0.0.1 https://blog.example.com/
```

That keeps the URL hostname for TLS SNI and certificate verification. Merely changing Host on `https://127.0.0.1/` does not make TLS use `blog.example.com`.

To exercise both virtual hosts locally without editing `/etc/hosts`:

```bash
curl --resolve blog.example.com:80:127.0.0.1 http://blog.example.com/
curl --resolve shop.example.com:80:127.0.0.1 http://shop.example.com/
```

The port in each `--resolve` entry must match the URL's port. For a development server on 3000, use `--resolve blog.example.com:3000:127.0.0.1` and `http://blog.example.com:3000/`. This changes curl's address resolution for the named target; the server still needs a matching virtual-host configuration.

## Getting Host right

[RFC 9112 §3.2](https://www.rfc-editor.org/rfc/rfc9112#section-3.2) requires 400 for an HTTP/1.1 request with no Host field, multiple Host field lines, or an invalid field value. This is a protocol requirement, not optional application advice. It does not mean every syntactically valid hostname belongs to your site.

## Security Implications

Host is request-controlled input. Validate allowed authorities before using them for application routing. Avoid `host.split(':')[0]` as a general hostname parser: IPv6 addresses themselves contain colons.

For a direct HTTP/1.1 Express deployment with a small known set of authorities, an exact allowlist avoids ad hoc IPv6 parsing:

```javascript
const allowedAuthorities = new Set(['localhost:3000', '[::1]:3000'])
app.use((req, res, next) => {
  const authority = req.get('host')
  if (!authority || !allowedAuthorities.has(authority.toLowerCase())) {
    return res.status(400).send('Unrecognized authority')
  }
  next()
})
```

Place the middleware before application routes. Include each authority you intend to serve, including its port form. This deliberately narrow policy rejects aliases and explicit default-port variants unless listed. At a proxy, validate the externally accepted authority there as well; validating only a rewritten internal Host loses the original routing context.

## Host Header Injection Attacks

If password-reset URLs are built from an unchecked request Host, the request can determine where the emailed token goes. Use a configured public origin for those links:

```javascript
const publicOrigin = 'https://accounts.example.com'
const resetUrl = new URL('/reset', publicOrigin)
resetUrl.searchParams.set('token', token)
```

This is a construction snippet; `token` must come from your reset flow. The trust issue is covered by [RFC 9110 §17.1](https://www.rfc-editor.org/rfc/rfc9110#section-17.1).

## Related Headers

- [Origin](https://howhttpworks.com/headers/origin)
- [Forwarded](https://howhttpworks.com/headers/forwarded)
- [X-Forwarded-Proto](https://howhttpworks.com/headers/x-forwarded-proto)
