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.
https://hexdocs.pm/bandit/Bandit.html#module-using-bandit-with-plug-applications