Repository navigation
Closed
Labels
enhancementNew feature or requestNew feature or requesthttp/1.1Issues and PRs related to HTTP/1.1Issues and PRs related to HTTP/1.1httpcoreIssues related to HTTPCore (core HTTP networking layer) - https://github.com/encode/httpcoreIssues related to HTTPCore (core HTTP networking layer) - https://github.com/encode/httpcore
Description
Checklist
- The bug is reproducible against the latest release and/or
master. - There are no similar issues or pull requests to fix it yet.
Describe the bug
HTTPX Session encounters parsing error (RemoteProtocolError: malformed data) when an endpoint returns content when status code is 204 (HTTP No Content). Other Python http clients handle this scenario.. I'm not sure if httpx should actually handle this case, since this is a weird case, but maybe it's best to handle it?
To reproduce
setup FastAPI server with this code:
from typing import Optional
from fastapi import FastAPI
from starlette.status import HTTP_204_NO_CONTENT
app = FastAPI()
@app.get("/", status_code=HTTP_204_NO_CONTENT)
def read_root():
print("d")
Run it using uvicorn. In another terminal run the following snippet:
import httpx
client = httpx.Client()
client.get('http://localhost:8000/")
# The second one will get an error
client.get('http://localhost:8000/")
Expected behavior
No errors?
Actual behavior
malformed data
Debugging material
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_client.py in get(self, url, params, headers, cookies, auth, allow_redirects, timeout)
905 **Parameters**: See `httpx.request`.
906 """
--> 907 return self.request(
908 "GET",
909 url,
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_client.py in request(self, method, url, content, data, files, json, params, headers, cookies, auth, allow_redirects, timeout)
731 cookies=cookies,
732 )
--> 733 return self.send(
734 request, auth=auth, allow_redirects=allow_redirects, timeout=timeout
735 )
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_client.py in send(self, request, stream, auth, allow_redirects, timeout)
765 auth = self._build_request_auth(request, auth)
766
--> 767 response = self._send_handling_auth(
768 request,
769 auth=auth,
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_client.py in _send_handling_auth(self, request, auth, timeout, allow_redirects, history)
803
804 while True:
--> 805 response = self._send_handling_redirects(
806 request,
807 timeout=timeout,
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_client.py in _send_handling_redirects(self, request, timeout, allow_redirects, history)
835 )
836
--> 837 response = self._send_single_request(request, timeout)
838 response.history = list(history)
839
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_client.py in _send_single_request(self, request, timeout)
859
860 with map_exceptions(HTTPCORE_EXC_MAP, request=request):
--> 861 (status_code, headers, stream, ext) = transport.request(
862 request.method.encode(),
863 request.url.raw,
~/.pyenv/versions/3.8.2/lib/python3.8/contextlib.py in __exit__(self, type, value, traceback)
129 value = type()
130 try:
--> 131 self.gen.throw(type, value, traceback)
132 except StopIteration as exc:
133 # Suppress StopIteration *unless* it's the same exception that
~/.pyenv/versions/3.8.2/lib/python3.8/site-packages/httpx/_exceptions.py in map_exceptions(mapping, **kwargs)
341
342 message = str(exc)
--> 343 raise mapped_exc(message, **kwargs) from exc # type: ignore
344
345
RemoteProtocolError: malformed data
Environment
- OS: macOS
- Python version: 3.8.2
- HTTPX version: 0.16.1
- Async environment: Both (asyncio, blocking)
- HTTP proxy: no
- Custom certificates: no
Reacted by Oleksandr Danshyn
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or requesthttp/1.1Issues and PRs related to HTTP/1.1Issues and PRs related to HTTP/1.1httpcoreIssues related to HTTPCore (core HTTP networking layer) - https://github.com/encode/httpcoreIssues related to HTTPCore (core HTTP networking layer) - https://github.com/encode/httpcore
Activity
florimondmanca commentedon Feb 18, 2021
Hello @aviramha, that's a pretty interesting one, thanks.
I was able to reproduce against this script:
App:
Client:
Trace output:
This does NOT reproduce if:
appso that it returns no body.The exception comes from a call to
h11, here:https://github.com/encode/httpcore/blob/12a25fdb311cc3f0feb2b7ca7f0afab46989fe19/httpcore/_sync/http11.py#L168-L169
The fact that it only occurs upon 2nd request makes me think that, despite calling
conn.start_next_cycle(),h11might have kept some internal state/buffer and processes that first, then it gets confused having received "malformed data".But that's only a supposition. It's not clear to me whether we have a bug, or
h11does. I know thath11is rather strict, so it's not really a surprise that it raises aRemoteProtocolErrorin this case (after all, the server is violating the HTTP protocol), but what surprises me is that it raises it on the second request-response cycle.What could help would be to simulate sending the request through
h11only (see tutorial). 😃lovelydinosaur commentedon Feb 18, 2021
Okay. So, not super surprising that
h11is correctly indicating this as a protocol violation, since it is...And yes,
h11tends to be protocol strict rather than some more lenient parsers, simply by virtue of it having been so carefully and comprehensively designed. (thanks to Nathaniel J. Smith)In terms of ecosystem here, what would be neater would be if either the server or the framework was indicating an error. (More likely the server, since the framework probably? shouldn't have that level of detail about the HTTP semantics built-in.)
Having said that, it's feasible that there's scope for more lenient "parse it if you can at all" for various edge cases in
h11. (?) There's a couple of similar style issues on the tracker - eg. python-hyper/h11#113, or supporting 6xx and other invalid codes as briefly mentioned in python-hyper/h11#95.lovelydinosaur commentedon Feb 18, 2021
Having switched from bug to u-x, it's actually slightly less clear to me, as it'd be preferable if the first request was the point at which this is handled. Perhaps there are gracefulnesses here such as force-closing the connection on responses that include invalid content bodies.
aviramha commentedon Feb 18, 2021
I did some digging and also came with the h11 strictness. I opened a PR to fastapi to overcome this but maybe this should also be enforced via starlette?
lovelydinosaur commentedon Feb 18, 2021
Well, I'd probably suggest at the server level, rather than each individual framework. So perhaps in Uvicorn. It'd be interesting to know how Gunicorn handles this case.
aviramha commentedon Feb 19, 2021
I wouldn't put my trust into Gunicorn as it's very lightweight and permissive. I did the test anyway which showed it allows the same issue to occur:
curl output:
lovelydinosaur commentedon Mar 24, 2021
Enforced by
h11, here... https://github.com/python-hyper/h11/blob/4746b0a0fff341b70aaa4032b7fa49f613a1b586/h11/_connection.py#L73-L81Would be useful to know if
requests/urllib3raises an error in this case on subsequent requests or not.One sensible tactic here might be to close the connection if we get a
204/304/HEAD/SuccessfulCONNECTthat includes a non-zeroContent-Length. That way we'll at least end up with graceful behaviour.[-]httpx session error on consecutive requests to an endpoint that returns 204 with actual data.[/-][+]Graceful handling of 204 responses that incorrectly a include non-zero Content-Length.[/+]dalazx commentedon Oct 20, 2021
Bumped into this issue too. The part of the first response's body that was not fully read is remaining in the underlying buffer. The second response reuses that buffer and hence is considered malformed.
In this ^ particular case the previous response was 204, but its body had
null.Clearly this is a bug and has nothing to do with servers not strictly conforming to the RFC.
lovelydinosaur commentedon Oct 21, 2021
That would not be my conclusion - it looks very much like a valid behaviour in response to malformed server behaviour.
But I do think we should be robust to it, and handle it gracefully.
dalazx commentedon Oct 21, 2021
@tomchristie thank you for your reply. let me share some more reasoning:
httpxhas a way of disabling HTTP pipelining explicitly, a response should be read in its entirety before sending the next request. In such a case the first response would be considered malformed, not the second one (howhttpxcurrently handles it).httpxwould be constantly raising protocol exceptions on subsequent responses from unrelated services after getting a response from that malformed one.dalazx commentedon Oct 21, 2021
meanwhile a pretty-bad looking workaround:
stale commentedon Feb 20, 2022
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
lovelydinosaur commentedon Feb 21, 2022
Still valid thx, @Stale bot.
stale commentedon Mar 25, 2022
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
florimondmanca commentedon Jun 28, 2022
As far as the original example code is concerned here, FastAPI is at fault here (or at least, users returning
Nonewithout specifyingresponse_class=Response). I got bitten by it myself just now, and there are a number of open issues on the FastAPI issue tracker and elsewhere about this:tiangolo commentedon Sep 3, 2022
Yep, FastAPI was at fault. This was solved in fastapi/fastapi#5145, available since FastAPI
0.79.0. 🎉