Missing host header in https connections

Can anyone explain some behavior I"m seeing? I cannot tell which level of Plug or Phoenix might be dropping the host header from conn.req_headers.

Steps to Reproduce

From a minimal Phoenix application I added the https config in development per the Phoenix docs

If I send a request with the host header via a browser or via curl, I see two different results when inspecting conn.req_headers:

$ curl -H 'host: localhost:4000' http://localhost:4000

> req headers: [{"accept", "*/*"}, {"host", "localhost:4000"}, {"user-agent", "curl/7.85.0"}]

$ curl --insecure -H 'host: localhost:4001' https://localhost:4001

> req headers: [{"accept", "*/*"}, {"user-agent", "curl/7.85.0"}]

I’ve gone through the app code, all of the Plug source code, and some of the Phoenix source code, and I cannot tell where or why this is occurring. Any pointers?

(This happens with both Cowboy and Bandit adapters on a Phoenix 1.7.1 project, so I don’t think it’s at the level of the web server.)

I think it has to do with cowboy’s support of http/2. What does the complete response look like? you’d be looking for the psuedo-header “:authority”.

Interesting - it is indeed http/2 related.

I see no :authority pseudo-headers in my response (either via Firefox or curl):

$curl --insecure --http2 -I -H 'host: localhost:4001' https://localhost:4001
HTTP/2 200
access-control-allow-credentials: true
access-control-allow-origin: *
access-control-expose-headers: Content-Disposition
cache-control: max-age=0, private, must-revalidate
content-length: 1134
content-type: text/html; charset=utf-8
cross-origin-window-policy: deny
date: Wed, 22 Mar 2023 21:04:52 GMT
server: Cowboy
x-content-type-options: nosniff
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-request-id: F07Zt17IXjk4fPkAAAKB
x-xss-protection: 1; mode=block

If I force the https connection to be http 1.1, I see the host request header again in the conn.req_headers.

curl --insecure --http1.1 -I -H 'host: localhost:4001' https://localhost:4001

req headers: [{"accept", "*/*"}, {"host", "localhost:4001"}, {"user-agent", "curl/7.85.0"}]

Follow up: after tracing through cowboy_http2.erl, I think I know what is happening: an http/2 compliant connection should drop and convert the host: header to the :authority pseudo-header.

So what I’m seeing in conn is correct: no host header appears in req_headers; this isn’t because Cowboy or Plug changes them, but because of the http/2 protocol.

Now I need to find out if Plug.Conn let’s me get at the pseudo-headers at all…

I’m pretty sure that Plug.Conn struct absctracts away the upgrade. You just access “conn.host” and it will pull the host/:authority depending on whats going on with http.

I’m less worried about getting the correct conn.host and more about getting the correct port.

This all arose because of a discussion in Bandit issue #106 (and also related to Ueberauth) where the conn.port may be sourced from any of the following:

  • the port Phoenix is running on
  • the URL config given to web server
  • x-forwarded-for if behind a proxy
  • the host header
  • the :authority pseedo-header
  • a default of 80 or 443 if the host header is missing

All of the above do the right thing in enough circumstances except for local development with both http and https running on non-standard ports.

At this point my solution will be to force everyone to use only the https version locally so that explicit configs can be used in lieu of a missing host header in Ueberauth.

I think you can set the port explicitly under options when you add the listener.