API Gateway Header Forwarding: Why Your Backend Sees the Wrong Host
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.
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 GatewayAPI 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?
easyRequests 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?