requests-wsgi-adapter icon indicating copy to clipboard operation
requests-wsgi-adapter copied to clipboard

environ['CONTENT_LENGTH'] should be str

Open lemon24 opened this issue 1 year ago • 0 comments

environ['CONTENT_LENGTH'] should be str, because PEP 3333 says:

It is a violation of this specification for any CGI variable’s value to be of any type other than str.

WSGIAdapter sets it to an int, which causes recent versions of Werkzeug to fail (example below), likely because of stricter number parsing introduced in 2.3.5:

When parsing numbers in HTTP request headers such as Content-Length, only ASCII digits are accepted rather than any format that Python’s int and float accept.

repro.py:

from flask import Flask, request
import requests
from wsgiadapter import WSGIAdapter

app = Flask(__name__)
app.config['DEBUG'] = True

@app.route("/", methods=['POST'])
def hello_world():
    print('in hello_world')
    request.form['key']
    return 'ok'
    
session = requests.Session()
session.mount('http://app/', WSGIAdapter(app))

print(session.post('http://app/', data={'key': 'value'}))

Environment:

$ python -V
Python 3.11.1
$ pip install flask==2.3.2 werkzeug==2.3.5 requests==2.31.0 requests-wsgi-adapter==0.4.1
...
Output (click to expand):
$ python repro.py 
in hello_world
Traceback (most recent call last):
  File ".../repro.py", line 17, in <module>
    print(session.post('http://app/', data={'key': 'value'}))
  File ".../.venv/lib/python3.11/site-packages/requests/sessions.py", line 637, in post
    return self.request("POST", url, data=data, json=json, **kwargs)
  File ".../.venv/lib/python3.11/site-packages/requests/sessions.py", line 589, in request
    resp = self.send(prep, **send_kwargs)
  File ".../.venv/lib/python3.11/site-packages/requests/sessions.py", line 703, in send
    r = adapter.send(request, **kwargs)
  File ".../.venv/lib/python3.11/site-packages/wsgiadapter.py", line 163, in send
    response.raw = Content(b''.join(self.app(environ, start_response)))
  File ".../.venv/lib/python3.11/site-packages/flask/app.py", line 2213, in __call__
    return self.wsgi_app(environ, start_response)
  File ".../.venv/lib/python3.11/site-packages/flask/app.py", line 2193, in wsgi_app
    response = self.handle_exception(e)
  File ".../.venv/lib/python3.11/site-packages/flask/app.py", line 2190, in wsgi_app
    response = self.full_dispatch_request()
  File ".../.venv/lib/python3.11/site-packages/flask/app.py", line 1486, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File ".../.venv/lib/python3.11/site-packages/flask/app.py", line 1484, in full_dispatch_request
    rv = self.dispatch_request()
  File ".../.venv/lib/python3.11/site-packages/flask/app.py", line 1469, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
  File ".../repro.py", line 11, in hello_world
    request.form['key']
  File ".../.venv/lib/python3.11/site-packages/werkzeug/utils.py", line 106, in __get__
    value = self.fget(obj)  # type: ignore
  File ".../.venv/lib/python3.11/site-packages/werkzeug/wrappers/request.py", line 450, in form
    self._load_form_data()
  File ".../.venv/lib/python3.11/site-packages/flask/wrappers.py", line 114, in _load_form_data
    super()._load_form_data()
  File ".../.venv/lib/python3.11/site-packages/werkzeug/wrappers/request.py", line 275, in _load_form_data
    self._get_stream_for_parsing(),
  File ".../.venv/lib/python3.11/site-packages/werkzeug/wrappers/request.py", line 302, in _get_stream_for_parsing
    return self.stream
  File ".../.venv/lib/python3.11/site-packages/werkzeug/utils.py", line 106, in __get__
    value = self.fget(obj)  # type: ignore
  File ".../.venv/lib/python3.11/site-packages/werkzeug/wrappers/request.py", line 354, in stream
    return get_input_stream(
  File ".../.venv/lib/python3.11/site-packages/werkzeug/wsgi.py", line 175, in get_input_stream
    content_length = get_content_length(environ)
  File ".../.venv/lib/python3.11/site-packages/werkzeug/wsgi.py", line 129, in get_content_length
    return _sansio_utils.get_content_length(
  File ".../.venv/lib/python3.11/site-packages/werkzeug/sansio/utils.py", line 157, in get_content_length
    return max(0, _plain_int(http_content_length))
  File ".../.venv/lib/python3.11/site-packages/werkzeug/_internal.py", line 325, in _plain_int
    if _plain_int_re.fullmatch(value) is None:
TypeError: expected string or bytes-like object, got 'int'

This workaround shows that CONTENT_LENGTH being set to a string fixes it:

class WSGIAdapter(WSGIAdapter):
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        original_app = self.app
        
        def new_app(environ, start_response):
            environ['CONTENT_LENGTH'] = str(environ['CONTENT_LENGTH'])
            return original_app(environ, start_response)
        
        self.app = new_app
$ python repro.py
in hello_world
<Response [200]>

lemon24 avatar Jun 08 '23 21:06 lemon24