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 <noreply@users.noreply.github.com>
Co-authored-by: Benoit Chesneau <bchesneau@gmail.com>
This commit is contained in:
r266-tech 2026-03-25 05:38:16 +08:00 committed by GitHub
parent 0ad47db800
commit f8fca7a72f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 34 additions and 1 deletions

View File

@ -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):

View File

@ -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"]