Skip to content

User-provided Content-Length header is missing from streaming requests #1168

Closed

Description

@vaultah

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

User-provided Content-Length header is missing from streaming requests

To reproduce

>>> import httpx
>>> client = httpx.AsyncClient()
>>> async def f():
...     yield b'abcd'
... 
>>> r = await client.post('https://httpbin.org/post', data=f(), headers={'content-length': '4'})
>>> r.json()
{'args': {}, 'data': 'abcd', 'files': {}, 'form': {}, 'headers': {'Accept': '*/*', 'Accept-Encoding': 'gzip, deflate', 'Host': 'httpbin.org', 'Transfer-Encoding': 'chunked', 'User-Agent': 'python-httpx/0.14.1', 'X-Amzn-Trace-Id': 'Root=1-5f3283ee-d5e67484ee0db0346804ce72'}, 'json': None, 'origin': 'x.x.x.x', 'url': 'https://httpbin.org/post'}
>>> 'Content-Length' in r.json()['headers']
False

Expected behavior

User-provided Content-Length header is not missing from streaming requests

Actual behavior

User-provided Content-Length header is missing from streaming requests

Debugging material

Environment

  • OS: Linux/Windows/macOS.
  • Python version: 3.8.5
  • HTTPX version: 0.14.1
  • Async environment: asyncio
  • HTTP proxy: no
  • Custom certificates: no

Additional context

Activity

  1. lovelydinosaur commented on Aug 11, 2020

    @lovelydinosaur
    Member

    Thanks for the report.

    You can see what's happening a bit more clearly with this...

    >>> c = httpx.Client()
    >>> c.build_request("POST", "https://www.example.org", headers={'Content-Length': '5'}, data=(i for i in b'abcde')).headers
    Headers({'host': 'www.example.org', 'user-agent': 'python-httpx/0.14.0', 'accept': '*/*', 'accept-encoding': 'gzip, deflate, br', 'connection': 'keep-alive', 'content-length': '5', 'transfer-encoding': 'chunked'})

    Our .build_request is including both the Transfer-Encoding: chunked that it expects from streaming data...

    return {"Transfer-Encoding": "chunked"}

    While also including the content-length that you've set explicitly.

    Presumably the h11 library is then discounting the Content-Length. (Since it's invalid to set both Content-Length and Transfer-Encoding.)

    I think what we probably want is an explicit ignore case for setting Transfer-Encoding from the stream, if the Content-Length has been set explicitly. Probably something like changing this piece of code:

    for key, value in self.stream.get_headers().items():

    From...

    for key, value in self.stream.get_headers().items():
        self.headers.setdefault(key, value)

    To this...

    for key, value in self.stream.get_headers().items():
        if key.lower() == "transfer-encoding" and "content-length" in self.headers:
            continue
        self.headers.setdefault(key, value)

    Along with a test case to verify that a .build_request() operation like this... c.build_request("POST", "https://www.example.org", headers={'Content-Length': '5'}, data=(i for i in b'abcde')) results in the correct headers. (Ie. just the Content-Length one.)

  2. lovelydinosaur commented on Aug 11, 2020

    @lovelydinosaur
    Member

    Labelling this as a good first issue, since there's hopefully enough into in the comment for someone to be able to resolve this.

  3. j178 commented on Aug 11, 2020

    @j178
    Contributor

    Run your scripts and running Wireshark, httpx actually contains the content-length header:
    image

  4. lovelydinosaur commented on Aug 11, 2020

    @lovelydinosaur
    Member

    @j178 Okay. It's possible that an intermediary could be stripping it before it gets to the server.
    Either way around the issue is valid, since we shouldn't be setting both headers.

  5. j178 commented on Aug 11, 2020

    @j178
    Contributor

    Yes, I'll open a PR for this.

  6. vaultah commented on Aug 11, 2020

    @vaultah
    Author

    To clarify, I noticed this while trying to make requests to an API that checks the presence of the Content-Length header (B2 API, to be precise), so it's definitely not limited to httpbin. For example, if data is a bytes object, httpbin registers the header correctly.

    >>> r = await client.post('https://httpbin.org/post', data=b''.join([x async for x in f()]), headers={'content-length': '4'})
    >>> 'Content-Length' in r.json()['headers']
    True
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggood first issueGood for newcomers

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions