Skip to content

Commit 0f04815

Browse files
committed
Fix blocking reads from wsgi.input in wsgiref.simple_server
1 parent 3fd61b7 commit 0f04815

File tree

5 files changed

+139
-12
lines changed

5 files changed

+139
-12
lines changed

Doc/library/wsgiref.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,14 @@ request. (E.g., using the :func:`shift_path_info` function from
371371
:pep:`3333`.
372372

373373

374+
.. method:: WSGIRequestHandler.get_stdin()
375+
376+
Return the object that should be used as the ``wsgi.input`` stream. If the
377+
request provides a ``Content-Length`` header, the default implementation returns
378+
a wrapper around :attr:`rfile` that limits reads to that many bytes. Otherwise,
379+
:attr:`rfile` is returned unchanged.
380+
381+
374382
.. method:: WSGIRequestHandler.get_stderr()
375383

376384
Return the object that should be used as the ``wsgi.errors`` stream. The default

Lib/test/test_wsgiref.py

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,18 +179,92 @@ def bad_app(environ, start_response):
179179
)
180180
self.assertEqual(err.splitlines()[-2], exc_message)
181181

182-
def test_wsgi_input(self):
183-
def bad_app(e,s):
182+
@force_not_colorized
183+
def test_wsgi_input_validation(self):
184+
def app(e, s):
184185
e["wsgi.input"].read()
185186
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
186187
return [b"data"]
187-
out, err = run_amock(validator(bad_app))
188-
self.assertEndsWith(out,
189-
b"A server error occurred. Please contact the administrator."
188+
out, err = run_amock(validator(app))
189+
self.assertEqual(out.splitlines()[-1], b"data")
190+
self.assertEndsWith(err, '"GET / HTTP/1.0" 200 4\n')
191+
192+
@force_not_colorized
193+
def test_wsgi_input_read(self):
194+
def app(e, s):
195+
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
196+
return [e["wsgi.input"].read(3), b"-", e["wsgi.input"].read()]
197+
request = (
198+
b"POST / HTTP/1.0\n"
199+
b"Content-Length: 6\n\n"
200+
b"foobarEXTRA"
190201
)
191-
self.assertEqual(
192-
err.splitlines()[-2], "AssertionError"
202+
out, err = run_amock(app, request)
203+
self.assertEqual(out.splitlines()[-1], b"foo-bar")
204+
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 7\n')
205+
206+
@force_not_colorized
207+
def test_wsgi_input_readline(self):
208+
def app(e, s):
209+
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
210+
return [
211+
e["wsgi.input"].readline(3),
212+
b"-",
213+
e["wsgi.input"].readline(),
214+
e["wsgi.input"].readline(),
215+
]
216+
request = (
217+
b"POST / HTTP/1.0\n"
218+
b"Content-Length: 10\n\n"
219+
b"foobar\n"
220+
b"bazEXTRA"
221+
)
222+
out, err = run_amock(app, request)
223+
self.assertEqual(out.splitlines()[-2], b"foo-bar")
224+
self.assertEqual(out.splitlines()[-1], b"baz")
225+
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 11\n')
226+
227+
@force_not_colorized
228+
def test_wsgi_input_readlines(self):
229+
def app(e, s):
230+
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
231+
return (
232+
e["wsgi.input"].readlines(3)
233+
+ [b"-"]
234+
+ e["wsgi.input"].readlines()
235+
)
236+
request = (
237+
b"POST / HTTP/1.0\n"
238+
b"Content-Length: 17\n\n"
239+
b"foobar\n"
240+
b"baz\n"
241+
b"hello\n"
242+
b"EXTRA"
243+
)
244+
out, err = run_amock(app, request)
245+
self.assertEqual(out.splitlines()[-3], b"foobar")
246+
self.assertEqual(out.splitlines()[-2], b"-baz")
247+
self.assertEqual(out.splitlines()[-1], b"hello")
248+
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 18\n')
249+
250+
@force_not_colorized
251+
def test_wsgi_input_iter(self):
252+
def app(e, s):
253+
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
254+
return e["wsgi.input"]
255+
request = (
256+
b"POST / HTTP/1.0\n"
257+
b"Content-Length: 17\n\n"
258+
b"foobar\n"
259+
b"baz\n"
260+
b"hello\n"
261+
b"EXTRA"
193262
)
263+
out, err = run_amock(app, request)
264+
self.assertEqual(out.splitlines()[-3], b"foobar")
265+
self.assertEqual(out.splitlines()[-2], b"baz")
266+
self.assertEqual(out.splitlines()[-1], b"hello")
267+
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 17\n')
194268

195269
@force_not_colorized
196270
def test_bytes_validation(self):

Lib/wsgiref/simple_server.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,42 @@ def set_app(self,application):
6666
self.application = application
6767

6868

69+
class InputWrapper:
70+
71+
def __init__(self, stream, remaining):
72+
self.stream = stream
73+
self.remaining = remaining
74+
75+
def read(self, size=-1, /):
76+
readable = min(size, self.remaining) if size >= 0 else self.remaining
77+
if readable == 0:
78+
return b''
79+
data = self.stream.read(readable)
80+
self.remaining -= readable
81+
return data
82+
83+
def readline(self, size=-1, /):
84+
readable = min(size, self.remaining) if size >= 0 else self.remaining
85+
if readable == 0:
86+
return b''
87+
line = self.stream.readline(readable)
88+
self.remaining -= len(line)
89+
return line
90+
91+
def readlines(self, hint=-1, /):
92+
lines = []
93+
read = 0
94+
while line := self.readline():
95+
lines.append(line)
96+
read += len(line)
97+
if hint > 0 and read >= hint:
98+
break
99+
return lines
100+
101+
def __iter__(self):
102+
while line := self.readline():
103+
yield line
104+
69105

70106
class WSGIRequestHandler(BaseHTTPRequestHandler):
71107

@@ -104,6 +140,13 @@ def get_environ(self):
104140
env['HTTP_'+k] = v
105141
return env
106142

143+
def get_stdin(self):
144+
length = self.headers.get('content-length')
145+
if length:
146+
return InputWrapper(self.rfile, int(length))
147+
else:
148+
return self.rfile
149+
107150
def get_stderr(self):
108151
return sys.stderr
109152

@@ -122,8 +165,8 @@ def handle(self):
122165
return
123166

124167
handler = ServerHandler(
125-
self.rfile, self.wfile, self.get_stderr(), self.get_environ(),
126-
multithread=False,
168+
self.get_stdin(), self.wfile, self.get_stderr(),
169+
self.get_environ(), multithread=False,
127170
)
128171
handler.request_handler = self # backpointer for logging
129172
handler.run(self.server.get_app())

Lib/wsgiref/validate.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@
7777
7878
* That wsgi.input is used properly:
7979
80-
- .read() is called with exactly one argument
81-
8280
- That it returns a string
8381
8482
- That readline, readlines, and __iter__ return strings
@@ -194,7 +192,7 @@ def __init__(self, wsgi_input):
194192
self.input = wsgi_input
195193

196194
def read(self, *args):
197-
assert_(len(args) == 1)
195+
assert_(len(args) <= 1)
198196
v = self.input.read(*args)
199197
assert_(type(v) is bytes)
200198
return v
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :mod:`wsgiref.simple_server` blocking when a WSGI application reads past
2+
the request body from ``wsgi.input``. Reads are now limited to the number of
3+
bytes declared by the ``Content-Length`` header and an end-of-file condition
4+
is simulated once that limit is reached, as required by :pep:`3333`.

0 commit comments

Comments
 (0)