API Gateway Header Forwarding: Why Your Backend Sees the Wrong Host

2 min readCloud Infrastructure

API Gateway rewrites the Host header to the integration endpoint by default. If your backend depends on the original hostname for routing or virtual hosting, you need to forward it explicitly — and x-forwarded-for has restrictions that will trip you up.

awsapi-gatewayalb

The problem: API Gateway rewrites the Host header

When API Gateway forwards a request to an HTTP integration (ALB, EC2, custom HTTP endpoint), it sets the Host header to the integration URL — not the original hostname the client sent.

Client sends:
  GET /api/status HTTP/1.1
  Host: api.example.com

API Gateway forwards to ALB:
  GET /api/status HTTP/1.1
  Host: my-alb-1234567890.us-east-1.elb.amazonaws.com  ← rewritten

If your backend uses virtual hosting — different nginx server blocks for different hostnames, or application-level routing on the Host header — it sees the ALB hostname instead of api.example.com and routes incorrectly or returns 404.

What API Gateway does and doesn't forward

GotchaAWS API Gateway

API Gateway transforms requests before sending them to integrations. Some headers are passed through, some are rewritten, and some (x-forwarded-for on HTTP APIs) are restricted. The behavior differs between REST API and HTTP API types.

Prerequisites

  • HTTP headers
  • virtual hosting
  • API Gateway integration types

Key Points

  • Host header is rewritten to the integration endpoint URL by default.
  • API Gateway adds its own headers: x-amzn-trace-id, x-forwarded-for, x-forwarded-proto, x-forwarded-port.
  • x-forwarded-for is restricted — you cannot directly set or modify it in mapping templates on HTTP APIs.
  • Original client IP is in x-forwarded-for. Original hostname is in the forwarded header as host=.

How to forward the original hostname

HTTP API (recommended): in the integration settings, enable "Use HTTP proxy integration" or configure a parameter mapping to forward the original host as a custom header.

# In AWS CDK or Terraform — HTTP API with parameter mapping
integrationUri: http://${alb_dns}
parameterMapping:
  overwrite:header.x-original-host: "$request.header.host"

Your backend then reads X-Original-Host instead of Host.

REST API (integration request): use a mapping template to set a header containing the original hostname.

In the API Gateway console → Integration Request → HTTP Headers:

Name: X-Original-Host
Mapped from: method.request.header.Host

Or in Terraform:

resource "aws_api_gateway_integration" "alb_integration" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  resource_id = aws_api_gateway_resource.proxy.id
  http_method = "ANY"
  type        = "HTTP_PROXY"
  uri         = "http://${aws_lb.internal.dns_name}/{proxy}"

  request_parameters = {
    "integration.request.header.X-Original-Host" = "method.request.header.Host"
  }
}

The x-forwarded-for restriction

On HTTP APIs, you cannot set or overwrite x-forwarded-for in a parameter mapping. API Gateway manages this header to track the client IP chain. Attempting to map it produces:

Invalid mapping expression specified: Validation Result: warnings : [], errors : [Operations on header x-forwarded-for are restricted]

The actual client IP is available in the forwarded header that API Gateway injects:

forwarded: for=207.81.250.185;host=api.example.com;proto=https

This header contains both the original client IP (for=) and the original hostname (host=). If your backend needs the original hostname for routing, parse it from the forwarded header rather than trying to override x-forwarded-for.

# Extract original host from API Gateway's forwarded header
def get_original_host(request):
    forwarded = request.headers.get('forwarded', '')
    for part in forwarded.split(';'):
        if part.strip().startswith('host='):
            return part.strip()[5:]
    # fallback to x-original-host if explicitly forwarded
    return request.headers.get('x-original-host', '')

The full header picture in a CloudFront → API Gateway → ALB chain

When traffic flows through CloudFront → API Gateway → ALB → ECS:

Client IP: 207.81.250.185
Client Host: api.example.com

Headers at ALB (as seen by backend):
  host:              my-alb-1234.us-east-1.elb.amazonaws.com  ← rewritten
  x-forwarded-for:   207.81.250.185, 10.0.1.70               ← client IP + API GW IP
  x-forwarded-proto: https
  x-forwarded-port:  443
  forwarded:         for=207.81.250.185;host=api.example.com;proto=https
  x-amzn-trace-id:   Self=1-abc;Root=1-def
  via:               HTTP/1.1 AmazonAPIGateway

x-forwarded-for contains the original client IP. The API Gateway's internal IP appears as a second entry. The original Host is preserved in the forwarded header.

📝NLB in front of private ALB: what's possible

If you need a public entry point for a private ALB (ALB with no direct internet access), you can put a public NLB in front of it. However, AWS only supports TCP passthrough from NLB to ALB — TLS termination must happen at the ALB.

Internet → NLB (TCP:443, no TLS termination) → ALB (HTTPS listener, terminates TLS) → ECS

You cannot configure NLB with a TLS listener pointing to an ALB target group because ALB targets require TCP protocol, and NLB TLS listeners cannot send unencrypted traffic to ALBs.

For most use cases, API Gateway HTTP integration directly to a private ALB (using a VPC link) is cleaner — no NLB needed, and API Gateway handles TLS termination.

# VPC link gives API Gateway access to private ALB
resource "aws_apigatewayv2_vpc_link" "main" {
  name               = "api-vpc-link"
  security_group_ids = [aws_security_group.api_gw.id]
  subnet_ids         = aws_subnet.private[*].id
}

An nginx server behind API Gateway uses server_name to route requests to different upstreams based on hostname. After adding API Gateway in front, all requests are routed to the default server block. What should you check first?

easy

Requests were working before API Gateway was added. The API Gateway uses an HTTP_PROXY integration to the ALB. nginx has multiple server blocks with different server_name values.

  • AThe ALB's listener rules need to match on the original hostname
    Incorrect.ALB listener rules can match on the Host header. But the Host header nginx sees is the ALB's own DNS name (set by API Gateway), so ALB listener rules already receiving the request is not the issue — nginx's internal routing is.
  • BAPI Gateway rewrites the Host header to the ALB DNS name — nginx matches this against server_name blocks and falls through to the default
    Correct!nginx server_name matching compares the Host header value against each server block's server_name. API Gateway sets Host to the ALB DNS name, which matches no nginx virtual host — nginx falls through to the default_server block. Fix: forward the original hostname as X-Original-Host and configure nginx to use it (using the $http_x_original_host variable in server_name matching, or switch to routing on a different mechanism).
  • Cnginx requires SNI (TLS server name indication) for virtual hosting, which API Gateway does not support
    Incorrect.This scenario uses HTTP (not HTTPS) between API Gateway and the ALB, so SNI is not relevant. nginx HTTP virtual hosting uses the Host header.
  • DThe ALB needs to strip the forwarded header before passing requests to nginx
    Incorrect.Stripping headers would remove information, not fix the routing. The problem is the Host header value, not extra headers.

Hint:What does nginx use for server_name matching, and what value does that header have after API Gateway rewrites it?