From 880231713892de668b3a95456902c74f4d06a833 Mon Sep 17 00:00:00 2001 From: gaweng Date: Thu, 9 Apr 2026 12:57:05 +0200 Subject: [PATCH] gh-58857: Add error messages to bare assertions in wsgiref.validate Add descriptive error messages to all assertions in the wsgiref validator middleware that were previously missing them. This makes it much easier to diagnose WSGI compliance issues when using the validation middleware. The following assertions now include messages: - InputWrapper.read(): argument count and return type checks - InputWrapper.readline(): argument count and return type checks - InputWrapper.readlines(): argument count and return type checks - ErrorWrapper.write(): argument type check - WriteWrapper.__call__(): argument type check - check_headers(): header tuple length check --- Lib/test/test_wsgiref.py | 45 ++++++++++++++++++- Lib/wsgiref/validate.py | 34 +++++++++----- ...6-04-09-12-56-34.gh-issue-58857.Kw9mPx.rst | 3 ++ 3 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-09-12-56-34.gh-issue-58857.Kw9mPx.rst diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py index 32ef0ccf4e638d..c64cf0bb76c454 100644 --- a/Lib/test/test_wsgiref.py +++ b/Lib/test/test_wsgiref.py @@ -189,9 +189,52 @@ def bad_app(e,s): b"A server error occurred. Please contact the administrator." ) self.assertEqual( - err.splitlines()[-2], "AssertionError" + err.splitlines()[-2], + "AssertionError: wsgi.input.read() takes exactly one argument" ) + def test_wsgi_input_readline_type(self): + def bad_app(e, s): + # Monkey-patch the underlying input to return wrong type + class BadInput: + def read(self, size): return b"" + def readline(self, *args): return "not bytes" + def readlines(self, *args): return [] + def __iter__(self): return self + e["wsgi.input"].input = BadInput() + e["wsgi.input"].readline() + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return [b"data"] + out, err = run_amock(validator(bad_app)) + self.assertIn("wsgi.input.readline() must return bytes", + err.splitlines()[-2]) + + def test_wsgi_errors_write_type(self): + def bad_app(e, s): + e["wsgi.errors"].write(b"not a string") + s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + return [b"data"] + out, err = run_amock(validator(bad_app)) + self.assertIn("wsgi.errors.write() requires a str argument", + err.splitlines()[-2]) + + def test_wsgi_write_wrapper_type(self): + def bad_app(e, s): + write = s("200 OK", [("Content-Type", "text/plain; charset=utf-8")]) + write("not bytes") + return [b"data"] + out, err = run_amock(validator(bad_app)) + self.assertIn("write() argument must be a bytes instance", + err.splitlines()[-2]) + + def test_headers_tuple_length(self): + def bad_app(e, s): + s("200 OK", [("Content-Type",)]) + return [b"data"] + out, err = run_amock(validator(bad_app)) + self.assertIn("Individual headers must be 2-item tuples", + err.splitlines()[-2]) + @force_not_colorized def test_bytes_validation(self): def app(e, s): diff --git a/Lib/wsgiref/validate.py b/Lib/wsgiref/validate.py index 1a1853cd63a0d2..8dbbc0f05da717 100644 --- a/Lib/wsgiref/validate.py +++ b/Lib/wsgiref/validate.py @@ -194,23 +194,32 @@ def __init__(self, wsgi_input): self.input = wsgi_input def read(self, *args): - assert_(len(args) == 1) + assert_(len(args) == 1, + "wsgi.input.read() takes exactly one argument") v = self.input.read(*args) - assert_(type(v) is bytes) + assert_(type(v) is bytes, + "wsgi.input.read() must return bytes, got %s" % type(v)) return v def readline(self, *args): - assert_(len(args) <= 1) + assert_(len(args) <= 1, + "wsgi.input.readline() takes at most one argument") v = self.input.readline(*args) - assert_(type(v) is bytes) + assert_(type(v) is bytes, + "wsgi.input.readline() must return bytes, got %s" % type(v)) return v def readlines(self, *args): - assert_(len(args) <= 1) + assert_(len(args) <= 1, + "wsgi.input.readlines() takes at most one argument") lines = self.input.readlines(*args) - assert_(type(lines) is list) + assert_(type(lines) is list, + "wsgi.input.readlines() must return a list, got %s" + % type(lines)) for line in lines: - assert_(type(line) is bytes) + assert_(type(line) is bytes, + "wsgi.input.readlines() must yield bytes, got %s" + % type(line)) return lines def __iter__(self): @@ -226,7 +235,9 @@ def __init__(self, wsgi_errors): self.errors = wsgi_errors def write(self, s): - assert_(type(s) is str) + assert_(type(s) is str, + "wsgi.errors.write() requires a str argument, got %s" + % type(s)) self.errors.write(s) def flush(self): @@ -245,7 +256,9 @@ def __init__(self, wsgi_writer): self.writer = wsgi_writer def __call__(self, s): - assert_(type(s) is bytes) + assert_(type(s) is bytes, + "write() argument must be a bytes instance, got %s" + % type(s)) self.writer(s) class PartialIteratorWrapper: @@ -391,7 +404,8 @@ def check_headers(headers): assert_(type(item) is tuple, "Individual headers (%r) must be of type tuple: %r" % (item, type(item))) - assert_(len(item) == 2) + assert_(len(item) == 2, + "Individual headers must be 2-item tuples, got %r" % (item,)) name, value = item name = check_string_type(name, "Header name") value = check_string_type(value, "Header value") diff --git a/Misc/NEWS.d/next/Library/2026-04-09-12-56-34.gh-issue-58857.Kw9mPx.rst b/Misc/NEWS.d/next/Library/2026-04-09-12-56-34.gh-issue-58857.Kw9mPx.rst new file mode 100644 index 00000000000000..fe46851ce7e5a6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-09-12-56-34.gh-issue-58857.Kw9mPx.rst @@ -0,0 +1,3 @@ +Add descriptive error messages to all bare assertions in +:mod:`wsgiref.validate` middleware, making it easier to diagnose WSGI +compliance issues.