How HTTP Works

Request header

> Host: www.example.com

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.

Reviewed 4 min readbeginner2 sourcesTry itMarkdown
Direction
Request
Category
Routing
Spec
RFC 9110 §7.2
JS can set it
No: forbidden header name
On this page

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 defines the field.

How Host Works

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

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.

Host Header Format

Illustrative valid values:

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:

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.

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

<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 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. 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.

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

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:

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:

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:

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 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:

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:

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.

Frequently asked questions

What is the Host header?

Host specifies the domain name and port of the server being requested. It is required in HTTP/1.1 and enables virtual hosting where one IP serves multiple domains.

Why is Host required?

HTTP/1.1 requires Host to identify the target authority, including for name-based virtual hosting. A server may have a default site, but that does not waive the protocol requirement.

What happens if Host is missing?

An HTTP/1.1 server must return 400 for a missing Host field, duplicate Host field lines, or an invalid Host value, as specified in RFC 9112 section 3.2.

Does Host include the port?

Host can include a port. A non-default port must be represented when it is part of the target authority; omission implies the default port for the URI scheme.

Sources

  1. MDN Web Docs: Hostdeveloper.mozilla.org
  2. RFC 9110: Hostrfc-editor.org

Keep going

Browse /search