diff --git a/Doc/library/wsgiref.rst b/Doc/library/wsgiref.rst index 2af54dc2a7e632..c621127aa354f4 100644 --- a/Doc/library/wsgiref.rst +++ b/Doc/library/wsgiref.rst @@ -371,6 +371,14 @@ request. (E.g., using the :func:`shift_path_info` function from :pep:`3333`. + .. method:: WSGIRequestHandler.get_stdin() + + Return the object that should be used as the ``wsgi.input`` stream. If the + request provides a ``Content-Length`` header, the default implementation returns + a wrapper around :attr:`rfile` that limits reads to that many bytes. Otherwise, + :attr:`rfile` is returned unchanged. + + .. method:: WSGIRequestHandler.get_stderr() Return the object that should be used as the ``wsgi.errors`` stream. The default diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 32ef0ccf4e638d..5c524315f354f2 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -179,18 +179,92 @@ def bad_app(environ, start_response): ) self.assertEqual(err.splitlines()[-2], exc_message) - def test_wsgi_input(self): - def bad_app(e,s): + @force_not_colorized + def test_wsgi_input_validation(self): + def app(e, s): e["wsgi.input"].read() s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) return [b"data"] - out, err = run_amock(validator(bad_app)) - self.assertEndsWith(out, - b"A server error occurred. Please contact the administrator." + out, err = run_amock(validator(app)) + self.assertEqual(out.splitlines()[-1], b"data") + self.assertEndsWith(err, '"GET / HTTP/1.0" 200 4\n') + + @force_not_colorized + def test_wsgi_input_read(self): + def app(e, s): + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return [e["wsgi.input"].read(3), b"-", e["wsgi.input"].read()] + request = ( + b"POST / HTTP/1.0\n" + b"Content-Length: 6\n\n" + b"foobarEXTRA" ) - self.assertEqual( - err.splitlines()[-2], "AssertionError" + out, err = run_amock(app, request) + self.assertEqual(out.splitlines()[-1], b"foo-bar") + self.assertEndsWith(err, '"POST / HTTP/1.0" 200 7\n') + + @force_not_colorized + def test_wsgi_input_readline(self): + def app(e, s): + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return [ + e["wsgi.input"].readline(3), + b"-", + e["wsgi.input"].readline(), + e["wsgi.input"].readline(), + ] + request = ( + b"POST / HTTP/1.0\n" + b"Content-Length: 10\n\n" + b"foobar\n" + b"bazEXTRA" + ) + out, err = run_amock(app, request) + self.assertEqual(out.splitlines()[-2], b"foo-bar") + self.assertEqual(out.splitlines()[-1], b"baz") + self.assertEndsWith(err, '"POST / HTTP/1.0" 200 11\n') + + @force_not_colorized + def test_wsgi_input_readlines(self): + def app(e, s): + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return ( + e["wsgi.input"].readlines(3) + + [b"-"] + + e["wsgi.input"].readlines() + ) + request = ( + b"POST / HTTP/1.0\n" + b"Content-Length: 17\n\n" + b"foobar\n" + b"baz\n" + b"hello\n" + b"EXTRA" + ) + out, err = run_amock(app, request) + self.assertEqual(out.splitlines()[-3], b"foobar") + self.assertEqual(out.splitlines()[-2], b"-baz") + self.assertEqual(out.splitlines()[-1], b"hello") + self.assertEndsWith(err, '"POST / HTTP/1.0" 200 18\n') + + @force_not_colorized + def test_wsgi_input_iter(self): + def app(e, s): + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return e["wsgi.input"] + request = ( + b"POST / HTTP/1.0\n" + b"Content-Length: 17\n\n" + b"foobar\n" + b"baz\n" + b"hello\n" + b"EXTRA" ) + out, err = run_amock(app, request) + self.assertEqual(out.splitlines()[-3], b"foobar") + self.assertEqual(out.splitlines()[-2], b"baz") + self.assertEqual(out.splitlines()[-1], b"hello") + self.assertEndsWith(err, '"POST / HTTP/1.0" 200 17\n') @force_not_colorized def test_bytes_validation(self): diff --git a/Lib/wsgiref/simple_server.py b/Lib/wsgiref/simple_server.py index 31efd8c9baea0d..2835e18d9f35a2 100644 --- a/Lib/wsgiref/simple_server.py +++ b/Lib/wsgiref/simple_server.py @@ -66,6 +66,42 @@ def set_app(self,application): self.application = application +class InputWrapper: + + def __init__(self, stream, remaining): + self.stream = stream + self.remaining = remaining + + def read(self, size=-1, /): + readable = min(size, self.remaining) if size >= 0 else self.remaining + if readable == 0: + return b'' + data = self.stream.read(readable) + self.remaining -= readable + return data + + def readline(self, size=-1, /): + readable = min(size, self.remaining) if size >= 0 else self.remaining + if readable == 0: + return b'' + line = self.stream.readline(readable) + self.remaining -= len(line) + return line + + def readlines(self, hint=-1, /): + lines = [] + read = 0 + while line := self.readline(): + lines.append(line) + read += len(line) + if hint > 0 and read >= hint: + break + return lines + + def __iter__(self): + while line := self.readline(): + yield line + class WSGIRequestHandler(BaseHTTPRequestHandler): @@ -104,6 +140,13 @@ def get_environ(self): env['HTTP_'+k] = v return env + def get_stdin(self): + length = self.headers.get('content-length') + if length: + return InputWrapper(self.rfile, int(length)) + else: + return self.rfile + def get_stderr(self): return sys.stderr @@ -122,8 +165,8 @@ def handle(self): return handler = ServerHandler( - self.rfile, self.wfile, self.get_stderr(), self.get_environ(), - multithread=False, + self.get_stdin(), self.wfile, self.get_stderr(), + self.get_environ(), multithread=False, ) handler.request_handler = self # backpointer for logging handler.run(self.server.get_app()) diff --git a/Lib/wsgiref/validate.py b/Lib/wsgiref/validate.py index 1a1853cd63a0d2..43b1ba14f50102 100644 --- a/Lib/wsgiref/validate.py +++ b/Lib/wsgiref/validate.py @@ -77,8 +77,6 @@ * That wsgi.input is used properly: - - .read() is called with exactly one argument - - That it returns a string - That readline, readlines, and __iter__ return strings @@ -194,7 +192,7 @@ def __init__(self, wsgi_input): self.input = wsgi_input def read(self, *args): - assert_(len(args) == 1) + assert_(len(args) <= 1) v = self.input.read(*args) assert_(type(v) is bytes) return v diff --git a/Misc/NEWS.d/next/Library/2026-04-21-01-58-47.gh-issue-66077.dvcX2i.rst b/Misc/NEWS.d/next/Library/2026-04-21-01-58-47.gh-issue-66077.dvcX2i.rst new file mode 100644 index 00000000000000..77db067328adb2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-21-01-58-47.gh-issue-66077.dvcX2i.rst @@ -0,0 +1,4 @@ +Fix :mod:`wsgiref.simple_server` blocking when a WSGI application reads past +the request body from ``wsgi.input``. Reads are now limited to the number of +bytes declared by the ``Content-Length`` header and an end-of-file condition +is simulated once that limit is reached, as required by :pep:`3333`.