From f8fca7a72fa370f9d03ff89a816db7ad89362dbf Mon Sep 17 00:00:00 2001 From: r266-tech Date: Wed, 25 Mar 2026 05:38:16 +0800 Subject: [PATCH] fix: add __iter__ and __next__ to FileWrapper for PEP 3333 compliance (#3550) * fix: add __iter__ and __next__ to FileWrapper for PEP 3333 compliance The WSGI spec (PEP 3333) requires that wsgi.file_wrapper return an iterable object. Gunicorn's FileWrapper only implemented __getitem__, which technically makes it iterable via old-style iteration but breaks code that explicitly relies on the iterator protocol (e.g., calling iter() or using next()). This adds __iter__ (returning self) and __next__ to make FileWrapper a proper iterator, maintaining backward compatibility with existing __getitem__-based usage. Fixes #3396 * Fix lint: move imports to top of file --------- Co-authored-by: contributor Co-authored-by: Benoit Chesneau --- gunicorn/http/wsgi.py | 9 +++++++++ tests/test_http.py | 26 +++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/gunicorn/http/wsgi.py b/gunicorn/http/wsgi.py index 542636e5..77231a85 100644 --- a/gunicorn/http/wsgi.py +++ b/gunicorn/http/wsgi.py @@ -38,6 +38,15 @@ class FileWrapper: return data raise IndexError + def __iter__(self): + return self + + def __next__(self): + data = self.filelike.read(self.blksize) + if data: + return data + raise StopIteration + class WSGIErrorsWrapper(io.RawIOBase): diff --git a/tests/test_http.py b/tests/test_http.py index 8af0551d..ef9b5ea5 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -9,7 +9,7 @@ from unittest import mock from gunicorn import util from gunicorn.http.body import Body, LengthReader, EOFReader -from gunicorn.http.wsgi import Response +from gunicorn.http.wsgi import FileWrapper, Response from gunicorn.http.unreader import Unreader, IterUnreader, SocketUnreader from gunicorn.http.errors import InvalidHeader, InvalidHeaderName, InvalidHTTPVersion from gunicorn.http.message import TOKEN_RE @@ -253,3 +253,27 @@ def test_eof_reader_read_invalid_size(): def test_invalid_http_version_error(): assert str(InvalidHTTPVersion('foo')) == "Invalid HTTP Version: 'foo'" assert str(InvalidHTTPVersion((2, 1))) == 'Invalid HTTP Version: (2, 1)' + + +def test_file_wrapper_iterable(): + """FileWrapper should support the iterator protocol per PEP 3333.""" + filelike = io.BytesIO(b"hello world") + wrapper = FileWrapper(filelike, blksize=5) + + # Should be iterable + assert hasattr(wrapper, '__iter__') + assert hasattr(wrapper, '__next__') + assert iter(wrapper) is wrapper + + # Should yield chunks via next() + assert next(wrapper) == b"hello" + assert next(wrapper) == b" worl" + assert next(wrapper) == b"d" + with pytest.raises(StopIteration): + next(wrapper) + + # Also works with for loop + filelike2 = io.BytesIO(b"abc") + wrapper2 = FileWrapper(filelike2, blksize=2) + chunks = list(wrapper2) + assert chunks == [b"ab", b"c"]