Skip to content

Commit e7b87ac

Browse files
committed
Add buf_read_size setting for configurable request body buffering
- Introduced `buf_read_size` setting to control buffer size for reading request data from the socket. - Updated `Body` class to utilize `buf_read_size` for reading operations. - Added validation for `buf_read_size` to ensure it is a positive integer. - Enhanced tests to cover the new setting and its validation.
1 parent 9aa5470 commit e7b87ac

8 files changed

Lines changed: 111 additions & 8 deletions

File tree

docs/content/reference/settings.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,25 @@ If not set, the default temporary directory will be used.
12861286
See [blocking-os-fchmod](#blocking_os_fchmod) for more detailed information
12871287
and a solution for avoiding this problem.
12881288

1289+
### `buf_read_size`
1290+
1291+
**Command line:** `--buf-read-size INT`
1292+
1293+
**Default:** `1024`
1294+
1295+
Buffer size for reading request data from the socket.
1296+
1297+
This controls the block size used while buffering request bodies for
1298+
``wsgi.input``. Larger values can reduce Python loop overhead for large
1299+
requests, while smaller values keep per-request buffering tighter.
1300+
1301+
!!! note
1302+
Benchmarks show that, with WSGI workers, increased values up to
1303+
64 kB can improve bandwidth performance when transferring
1304+
large bodies, typically larger than 5 MB.
1305+
1306+
The value must be greater than zero.
1307+
12891308
### `user`
12901309

12911310
**Command line:** `-u USER`, `--user USER`

gunicorn/config.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,17 @@ def validate_pos_int(val):
389389
return val
390390

391391

392+
def validate_strict_pos_int(val):
393+
if not isinstance(val, int):
394+
val = int(val, 0)
395+
else:
396+
# Booleans are ints!
397+
val = int(val)
398+
if val <= 0:
399+
raise ValueError("Value must be greater than zero: %s" % val)
400+
return val
401+
402+
392403
def validate_http2_frame_size(val):
393404
"""Validate HTTP/2 max frame size per RFC 7540."""
394405
if not isinstance(val, int):
@@ -1192,6 +1203,30 @@ class WorkerTmpDir(Setting):
11921203
"""
11931204

11941205

1206+
class BufReadSize(Setting):
1207+
name = "buf_read_size"
1208+
section = "Server Mechanics"
1209+
cli = ["--buf-read-size"]
1210+
meta = "INT"
1211+
validator = validate_strict_pos_int
1212+
type = int
1213+
default = 1024
1214+
desc = """\
1215+
Buffer size for reading request data from the socket.
1216+
1217+
This controls the block size used while buffering request bodies for
1218+
``wsgi.input``. Larger values can reduce Python loop overhead for large
1219+
requests, while smaller values keep per-request buffering tighter.
1220+
1221+
.. note::
1222+
Benchmarks show that, with WSGI workers, increased values up to
1223+
64 kB can improve bandwidth performance when transferring
1224+
large bodies, typically larger than 5 MB.
1225+
1226+
The value must be greater than zero.
1227+
"""
1228+
1229+
11951230
class User(Setting):
11961231
name = "user"
11971232
section = "Server Mechanics"

gunicorn/http/body.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,14 @@ def read(self, size):
184184

185185

186186
class Body:
187-
def __init__(self, reader):
187+
def __init__(self, reader, buf_read_size=1024):
188+
if not isinstance(buf_read_size, int):
189+
raise TypeError("buf_read_size must be an integral type")
190+
if buf_read_size <= 0:
191+
raise ValueError("buf_read_size must be greater than zero")
192+
188193
self.reader = reader
194+
self.buf_read_size = buf_read_size
189195
self.buf = io.BytesIO()
190196

191197
def __iter__(self):
@@ -221,7 +227,7 @@ def read(self, size=None):
221227
return ret
222228

223229
while size > self.buf.tell():
224-
data = self.reader.read(1024)
230+
data = self.reader.read(self.buf_read_size)
225231
if not data:
226232
break
227233
self.buf.write(data)
@@ -251,7 +257,7 @@ def readline(self, size=None):
251257

252258
ret.append(data)
253259
size -= len(data)
254-
data = self.reader.read(min(1024, size))
260+
data = self.reader.read(min(self.buf_read_size, size))
255261
if not data:
256262
break
257263

gunicorn/http/message.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ def set_body_reader(self):
348348
# we cannot be certain the message framing we understood matches proxy intent
349349
# -> whatever happens next, remaining input must not be trusted
350350
raise InvalidHeader("CONTENT-LENGTH", req=self)
351-
self.body = Body(ChunkedReader(self, self.unreader))
351+
self.body = Body(ChunkedReader(self, self.unreader), self.cfg.buf_read_size)
352352
elif content_length is not None:
353353
try:
354354
if str(content_length).isnumeric():
@@ -361,9 +361,9 @@ def set_body_reader(self):
361361
if content_length < 0:
362362
raise InvalidHeader("CONTENT-LENGTH", req=self)
363363

364-
self.body = Body(LengthReader(self.unreader, content_length))
364+
self.body = Body(LengthReader(self.unreader, content_length), self.cfg.buf_read_size)
365365
else:
366-
self.body = Body(EOFReader(self.unreader))
366+
self.body = Body(EOFReader(self.unreader), self.cfg.buf_read_size)
367367

368368
def should_close(self):
369369
if self.must_close:
@@ -828,4 +828,4 @@ def parse_request_line(self, line_bytes):
828828
def set_body_reader(self):
829829
super().set_body_reader()
830830
if isinstance(self.body.reader, EOFReader):
831-
self.body = Body(LengthReader(self.unreader, 0))
831+
self.body = Body(LengthReader(self.unreader, 0), self.cfg.buf_read_size)

gunicorn/uwsgi/message.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def set_body_reader(self):
222222
except ValueError:
223223
content_length = 0
224224

225-
self.body = Body(LengthReader(self.unreader, content_length))
225+
self.body = Body(LengthReader(self.unreader, content_length), self.cfg.buf_read_size)
226226

227227
def should_close(self):
228228
"""Determine if the connection should be closed after this request."""

tests/test_config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ def test_pos_int_validation():
157157
pytest.raises(TypeError, c.set, "workers", c)
158158

159159

160+
def test_strict_pos_int_validation():
161+
c = config.Config()
162+
assert c.buf_read_size == 1024
163+
c.set("buf_read_size", 4096)
164+
assert c.buf_read_size == 4096
165+
c.set("buf_read_size", "8192")
166+
assert c.buf_read_size == 8192
167+
pytest.raises(ValueError, c.set, "buf_read_size", 0)
168+
pytest.raises(ValueError, c.set, "buf_read_size", -1)
169+
pytest.raises(TypeError, c.set, "buf_read_size", c)
170+
171+
160172
def test_str_validation():
161173
c = config.Config()
162174
assert c.proc_name == "gunicorn"
@@ -252,6 +264,9 @@ def test_cmd_line():
252264
with AltArgs(["prog_name", "-w", "3"]):
253265
app = NoConfigApp()
254266
assert app.cfg.workers == 3
267+
with AltArgs(["prog_name", "--buf-read-size", "4096"]):
268+
app = NoConfigApp()
269+
assert app.cfg.buf_read_size == 4096
255270
with AltArgs(["prog_name", "--preload"]):
256271
app = NoConfigApp()
257272
assert app.cfg.preload_app

tests/test_http.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,33 @@ def test_readline_buffer_loaded_with_size():
8181
assert body.readline(2) == b"f"
8282

8383

84+
def test_body_read_uses_configured_buf_read_size():
85+
reader = mock.MagicMock()
86+
reader.read.side_effect = [b"abcd", b"ef", b""]
87+
88+
body = Body(reader, buf_read_size=4)
89+
90+
assert body.read(6) == b"abcdef"
91+
assert reader.read.call_args_list == [mock.call(4), mock.call(4)]
92+
93+
94+
def test_body_readline_uses_configured_buf_read_size():
95+
reader = mock.MagicMock()
96+
reader.read.side_effect = [b"abcd", b"ef\n", b""]
97+
98+
body = Body(reader, buf_read_size=4)
99+
100+
assert body.readline() == b"abcdef\n"
101+
assert reader.read.call_args_list == [mock.call(4), mock.call(4)]
102+
103+
104+
def test_body_invalid_buf_read_size():
105+
with pytest.raises(TypeError):
106+
Body(io.BytesIO(b""), buf_read_size="1024")
107+
with pytest.raises(ValueError):
108+
Body(io.BytesIO(b""), buf_read_size=0)
109+
110+
84111
def test_http_header_encoding():
85112
""" tests whether http response headers are USASCII encoded """
86113

tests/test_uwsgi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class MockConfig:
5353
def __init__(self, is_ssl=False, uwsgi_allow_ips=None):
5454
self.is_ssl = is_ssl
5555
self.uwsgi_allow_ips = uwsgi_allow_ips or ['127.0.0.1', '::1']
56+
self.buf_read_size = 1024
5657

5758

5859
class TestUWSGIPacketConstruction:

0 commit comments

Comments
 (0)