HTTP Host Header and Virtual Hosting: Why IP Addresses Alone Fail to Load Websites


1 views

When you enter 93.184.216.34 directly into your browser instead of example.com, the server doesn't know which website to serve. This happens because modern web servers use name-based virtual hosting, where multiple domains can share the same IP address.

Here's what happens in the HTTP protocol:

GET / HTTP/1.1
Host: example.com

Without the Host header (which browsers automatically include when you use a domain name), the server can't determine which virtual host configuration to use.

You can demonstrate this behavior using cURL:

# This works (includes Host header)
curl -v https://example.com

# This fails (no Host header)
curl -v http://93.184.216.34

Here's a typical Nginx virtual host configuration:

server {
    listen 80;
    server_name example.com;
    
    location / {
        root /var/www/example.com;
        index index.html;
    }
}

For development purposes, you can modify your /etc/hosts file:

93.184.216.34 example.com

Or configure your web server to handle direct IP access:

server {
    listen 80 default_server;
    return 444; # Special Nginx code to close connection
}

Blocking direct IP access is actually good security practice:

  • Prevents host header injection attacks
  • Blocks malicious scanners looking for default server responses
  • Improves HTTPS certificate validation

When you run host example.com and get an IP like 93.184.216.34, but entering that IP directly in your browser fails, there are several technical reasons behind this behavior:

> curl -I http://93.184.216.34
HTTP/1.1 404 Not Found

Modern web hosting relies heavily on the HTTP Host header. When you access a site via domain name:

GET / HTTP/1.1
Host: example.com

But with direct IP access, browsers typically send:

GET / HTTP/1.1
Host: 93.184.216.34

Most web servers host multiple sites on a single IP using virtual hosting. Here's a simplified Node.js example:

const http = require('http');

http.createServer((req, res) => {
  if(req.headers.host === 'example.com') {
    res.end('Welcome to example.com');
  } else {
    res.writeHead(404);
    res.end('Host not found');
  }
}).listen(80);

HTTPS adds another layer of complexity. Certificates are issued for domain names, not IPs:

> openssl s_client -connect 93.184.216.34:443
...
subject=CN = example.com
...

For testing purposes, you can override the Host header:

curl -H "Host: example.com" http://93.184.216.34

Or modify your local hosts file:

93.184.216.34 example.com

This behavior stems from HTTP/1.1 specifications (RFC 2616) that made the Host header mandatory. Prior to HTTP/1.1, IP-based access was more common.