emmett-framework / granian

A Rust HTTP server for Python applications
BSD 3-Clause "New" or "Revised" License
2.67k stars 79 forks source link

Missing "host" header in ASGI scope with HTTP/2 #188

Closed kramar11 closed 8 months ago

kramar11 commented 8 months ago

Tried granian today with --interface asgi and a starlette application but I'm missing the "host" header in the asgi scope.

gi0baro commented 8 months ago

Headers are sent by the client, Granian is not removing nor adding anything to request headers.

Tested with the following code and can't reproduce your issue:

from starlette.applications import Starlette
from starlette.responses import PlainTextResponse
from starlette.routing import Route

def homepage(request):
    print('scope', request.scope)
    print('headers', request.headers)
    print('host', request.headers.get('host'))
    return PlainTextResponse('Hello, world!')

routes = [
    Route('/', homepage),
]

app = Starlette(debug=True, routes=routes)

Request:

 ❯ curl -v http://localhost:8000
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.1.2
> Accept: */*
> 
< HTTP/1.1 200 OK
< server: granian
< content-length: 13
< content-type: text/plain; charset=utf-8
< date: Thu, 25 Jan 2024 09:48:51 GMT
< 
* Connection #0 to host localhost left intact
Hello, world!

Server logs:

 ❯ granian --interface asgi t:app
[INFO] Starting granian (main PID: 71370)
[INFO] Listening at: 127.0.0.1:8000
[INFO] Spawning worker-1 with pid: 71372
[INFO] Started worker-1
[INFO] Started worker-1 runtime-1
scope {'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'extensions': {'http.response.pathsend': {}}, 'type': 'http', 'http_version': '1.1', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 55309), 'scheme': 'http', 'method': 'GET', 'root_path': '', 'path': '/', 'raw_path': b'/', 'query_string': b'', 'headers': [(b'host', b'localhost:8000'), (b'user-agent', b'curl/8.1.2'), (b'accept', b'*/*')], 'state': {}, 'app': <starlette.applications.Starlette object at 0x1026237d0>, 'starlette.exception_handlers': ({<class 'starlette.exceptions.HTTPException'>: <bound method ExceptionMiddleware.http_exception of <starlette.middleware.exceptions.ExceptionMiddleware object at 0x10273b920>>, <class 'starlette.exceptions.WebSocketException'>: <bound method ExceptionMiddleware.websocket_exception of <starlette.middleware.exceptions.ExceptionMiddleware object at 0x10273b920>>}, {}), 'router': <starlette.routing.Router object at 0x10273b6b0>, 'endpoint': <function homepage at 0x10262ca40>, 'path_params': {}}
headers Headers({'host': 'localhost:8000', 'user-agent': 'curl/8.1.2', 'accept': '*/*'})
host localhost:8000
kramar11 commented 8 months ago

Thanks for your extra fast reply! I did some further investigation, and your example works with http, but as soon as i'm using https, the host header disappears from the scope.

> granian --interface asgi --ssl-certificate ssl-cert.pem --ssl-keyfile ssl-key.pem t:app
...
scope {'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'extensions': {'http.response.pathsend': {}}, 'type': 'http', 'http_version': '2', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 21342), 'scheme': 'https', 'method': 'GET', 'root_path': '', 'path': '/', 'raw_path': b'/', 'query_string': b'', 'headers': [(b'user-agent', b'curl/7.76.1'), (b'accept', b'*/*')], 'state': {}, 'app': <starlette.applications.Starlette object at 0x7fd984583bd0>, 'router': <starlette.routing.Router object at 0x7fd984116110>, 'endpoint': <function homepage at 0x7fd984562480>, 'path_params': {}}
headers Headers({'user-agent': 'curl/7.76.1', 'accept': '*/*'})
host None

Am I doing sth wrong? With hypercorn I get the host header as expected.

gi0baro commented 8 months ago

@kramar11 thank you for the additional details, I'll investigate this asap.

gi0baro commented 8 months ago

@kramar11 after re-checking your comment, I noticed this is HTTP/2. Host header is not allowed on HTTP/2, thus is not present in the scope's headers.

Now the actual issue here is that Granian doesn't allow access to pseudo-header :authority which would contain the relative information. I see Hypercorn automatically translate such pseudo-header content into the Host header on HTTP/2, as per ASGI spec. Now I don't like this, but now Granian is out of spec for ASGI, so I gonna do the change. But a the same time I need to figure out something for RSGI and WSGI which can incur in the same issue.