twisted / treq

Python requests like API built on top of Twisted's HTTP client.
Other
585 stars 137 forks source link

Inconsistent handling of data sequence values with/without files #360

Open twm opened 1 year ago

twm commented 1 year ago
import os

from twisted.internet.task import react

import treq

async def main(reactor):
    print("Without files:")
    response = await treq.post(
        "https://httpbin.org/post",
        data={"form": ["1", "2"]},
    )
    print(await response.text())

    print("With files:")
    response = await treq.post(
        "https://httpbin.org/post",
        data={"form": ["1", "2"]},
        files={"foo": (os.devnull, open(os.devnull, "rb"))},
    )
    print(await response.text())

react(main, [])

When run:

Without files:
{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "form": [
      "1", 
      "2"
    ]
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "13", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "X-Amzn-Trace-Id": "Root=1-63a26dee-580ab8e77fdd228c1eb84266"
  }, 
  "json": null, 
  "origin": "99.187.228.202", 
  "url": "https://httpbin.org/post"
}

With files:
main function encountered error
Traceback (most recent call last):
  File ".../twisted/internet/defer.py", line 696, in callback
    self._startRunCallbacks(result)
  File ".../twisted/internet/defer.py", line 798, in _startRunCallbacks
    self._runCallbacks()
  File ".../twisted/internet/defer.py", line 892, in _runCallbacks
    current.result = callback(  # type: ignore[misc]
  File ".../twisted/internet/defer.py", line 1792, in gotResult
    _inlineCallbacks(r, gen, status, context)
--- <exception caught here> ---
  File ".../twisted/internet/defer.py", line 1697, in _inlineCallbacks
    result = context.run(gen.send, result)
  File ".../demo.py", line 17, in main
    response = await treq.post(
  File ".../src/treq/api.py", line 32, in post
    return _client(kwargs).post(url, data=data, _stacklevel=4, **kwargs)
  File ".../src/treq/client.py", line 182, in post
    return self.request('POST', url, data=data, **kwargs)
  File ".../src/treq/client.py", line 244, in request
    bodyProducer, contentType = self._request_body(data, files, json,
  File ".../src/treq/client.py", line 379, in _request_body
    multipart.MultiPartProducer(data + files, boundary=boundary),
  File ".../src/treq/multipart.py", line 53, in __init__
    self._fields = list(_sorted_by_type(_converted(fields)))
  File ".../src/treq/multipart.py", line 350, in _sorted_by_type
    return sorted(fields, key=key)
  File ".../src/treq/multipart.py", line 260, in _converted
    raise ValueError(
builtins.ValueError: Expected tuple: (filename, content type, producer)

When only data is passed we URL-encode the request body with urllencode(..., doseq=True), whereas when both data and files are passed we combine them and pass the result to MultiPartProducer without unboxing sequences of values.

glyph commented 1 year ago

Thanks for filing this.