requests-wsgi-adapter
requests-wsgi-adapter copied to clipboard
environ['CONTENT_LENGTH'] should be str
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]>