Closed Kludex closed 3 months ago
The repro that I provided above has a typo (the first request is missing a Host
header).
Here's a better pipeline that actually demonstrates the bug:
printf 'GET / HTTP/1.1\r\nConnection: close\r\nHost: a\r\n\r\nGET / HTTP/1.1\r\n\r\n' | nc localhost 8080
There are two requests in this pipeline; the first is valid and should close the connection. The second is invalid (missing Host
header) and should receive no response at all.
What Uvicorn does is respond to the second request and not the first.
same error
ERROR: Exception in ASGI application Traceback (most recent call last): File "/data/anaconda3/lib/python3.11/site-packages/uvicorn/protocols/http/h11_impl.py", line 404, in run_asgi result = await app( # type: ignore[func-returns-value] ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/anaconda3/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__ return await self.app(scope, receive, send) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/anaconda3/lib/python3.11/site-packages/fastapi/applications.py", line 1054, in __call__ await super().__call__(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/applications.py", line 123, in __call__ await self.middleware_stack(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/middleware/errors.py", line 186, in __call__ raise exc File "/data/anaconda3/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__ await self.app(scope, receive, _send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/middleware/cors.py", line 83, in __call__ await self.app(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 62, in __call__ await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app raise exc File "/data/anaconda3/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app await app(scope, receive, sender) File "/data/anaconda3/lib/python3.11/site-packages/starlette/routing.py", line 762, in __call__ await self.middleware_stack(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/routing.py", line 782, in app await route.handle(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/routing.py", line 297, in handle await self.app(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/routing.py", line 77, in app await wrap_app_handling_exceptions(app, request)(scope, receive, send) File "/data/anaconda3/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app raise exc File "/data/anaconda3/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app await app(scope, receive, sender) File "/data/anaconda3/lib/python3.11/site-packages/starlette/routing.py", line 72, in app response = await func(request) ^^^^^^^^^^^^^^^^^^^ File "/data/anaconda3/lib/python3.11/site-packages/fastapi/routing.py", line 299, in app raise e File "/data/anaconda3/lib/python3.11/site-packages/fastapi/routing.py", line 294, in app raw_response = await run_endpoint_function( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/anaconda3/lib/python3.11/site-packages/fastapi/routing.py", line 191, in run_endpoint_function return await dependant.call(**values) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/data/ChatGLM-6B/visualglm-6b/api.py", line 38, in visual_glm json_post_raw = await request.json() ^^^^^^^^^^^^^^^^^^^^ File "/data/anaconda3/lib/python3.11/site-packages/starlette/requests.py", line 249, in json body = await self.body() ^^^^^^^^^^^^^^^^^ File "/data/anaconda3/lib/python3.11/site-packages/starlette/requests.py", line 242, in body async for chunk in self.stream(): File "/data/anaconda3/lib/python3.11/site-packages/starlette/requests.py", line 236, in stream raise ClientDisconnect() starlette.requests.ClientDisconnect
same error
I can't tell whether you're supporting this issue or trying to poke a hole in it :)
That error isn't necessarily confirmation of the bug; you'd have to look at the server's HTTP response in order to see the bad behavior.
Confirmed. PR welcome.
@Kludex do you have a guess about where's the file that needs to be changed? I was thinking on h11_impl.py
but looking at it
if self.response_complete:
if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive:
self.conn.send(event=h11.ConnectionClosed())
self.transport.close()
self.on_response()
it looks ok to me so I'm bit confused
@kenballus
The repro that I provided above has a typo (the first request is missing a
Host
header).Here's a better pipeline that actually demonstrates the bug:
printf 'GET / HTTP/1.1\r\nConnection: close\r\nHost: a\r\n\r\nGET / HTTP/1.1\r\n\r\n' | nc localhost 8080
There are two requests in this pipeline; the first is valid and should close the connection. The second is invalid (missing
Host
header) and should receive no response at all.What Uvicorn does is respond to the second request and not the first.
I get below in response. has this issue been fixed already?
date: Sun, 18 Feb 2024 17:34:35 GMT
server: uvicorn
content-type: text/plain
connection: close
transfer-encoding: chunked
d
Hello, world!
0
The issue has not been fixed. I just reproduced it again on the current master branch (latest commit is 1e5f1be at the time of writing).
When I send that pipeline, I get the following:
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: close
1e
Invalid HTTP request received.
0
+1 to what @berkio3x said, got below response when I tried to run it.
HTTP/1.1 200 OK
date: Wed, 28 Feb 2024 18:09:27 GMT
server: uvicorn
content-type: text/plain
connection: close
transfer-encoding: chunked
d
Hello, world!
0
We don't need further confirmation.
We don't need further confirmation.
Agreed, though I am curious about why others are not able to reproduce the issue.
I'd be willing to take a shot at fixing this, if nobody else is working on it. I haven't contributed to Uvicorn before though, would there be anything to keep in mind apart from what's in https://www.uvicorn.org/contributing/?
I was able to reproduce the above issue. Also went through the code of uvicorn and h11. I do have RCA for this problem. NGL this bug is kinda funny lol. Because it'll only occur if you have installed uvicon with pip
. It does not occur with manual installation using repo code.
Issue: When passed more than one HTTP requests within one single TCP stream, where first request has Connection: close
header, the server should only respond to first request and close the connection. Instead in current case, server is returning Invalid HTTP request received.
response for second request which is invalid.
Observations: This issue only occurs for pip installed uvicorn. Works totally fine with manually installed version. Unless you explicitly set the http
parameter to h11
while booting your application.
Reason: pip installed uvicorn uses h11
HTTP protocol implementation because httptools
is not on PyPi hence which did not got installed. Now in h11 protocol, h11 sets the client_state
to MUST_CLOSE
after processing first request because of Connection
header which is set to close
(please refer to h11 state machines here). But since the TCP stream contains two requests, while processing the second request h11 checks the state of the client, which is currently set to MUST_CLOSE
, but the request buffer still has data. Which is not expected, and hence it raises LocalProtocolError("Got data when expecting EOF")
.
Possible fix: In the H11Protocol
implementation, we need to check if the client's state is set to MUST_CLOSE
and if some data is still there in the received request buffer then we need to override that remaining buffer with an empty buffer.
Fixed on #2375.
Uvicorn 0.30.4 contains the fix.
Discussed in https://github.com/encode/uvicorn/discussions/2234