Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.6.3] - 2026-05-??

- Fix [#675](https://github.com/Neoteroi/BlackSheep/issues/675): fix `OverflowError`
when serving large files inefficiently; `get_chunks` in `scribe.pyx` used a C `int`
loop variable that overflows for responses larger than ~2 GB. Changed to `Py_ssize_t`.
- Fix potential `OverflowError` in `cookies.pyx`: `Cookie.max_age` and the local
variable in `parse_cookie` were declared as C `int`; changed to `long long`.

## [2.6.2] - 2026-02-25 :gift:

- Fix regression that broke compatibility with `Starlette` mounts
Expand Down
2 changes: 1 addition & 1 deletion blacksheep/cookies.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ cdef class Cookie:
cdef public str path
cdef public bint http_only
cdef public bint secure
cdef public int max_age
cdef public long long max_age
cdef public CookieSameSiteMode same_site
cpdef Cookie clone(self)

Expand Down
4 changes: 2 additions & 2 deletions blacksheep/cookies.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ cdef class Cookie:
str path=None,
bint http_only=0,
bint secure=0,
int max_age=-1,
long long max_age=-1,
CookieSameSiteMode same_site=CookieSameSiteMode.UNDEFINED
):
self.name = name
Expand Down Expand Up @@ -126,7 +126,7 @@ cdef CookieSameSiteMode same_site_mode_from_bytes(bytes raw_value):

cpdef Cookie parse_cookie(bytes raw_value):
# https://tools.ietf.org/html/rfc6265
cdef int max_age
cdef long long max_age
cdef bytes value = b''
cdef bytes eq, expires, domain, path, part, k, v, lower_k, lower_part
cdef bint http_only, secure
Expand Down
11 changes: 6 additions & 5 deletions blacksheep/scribe.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ from .messages cimport Request, Response
from .url cimport URL


cdef int MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb
MAX_RESPONSE_CHUNK_SIZE = 61440 # 64kb — Python-accessible
cdef int _MAX_RESPONSE_CHUNK_SIZE = MAX_RESPONSE_CHUNK_SIZE


cdef bint should_use_chunked_encoding(Content content):
Expand Down Expand Up @@ -44,9 +45,9 @@ async def write_chunks(Content http_content):


def get_chunks(bytes data):
cdef int i
for i in range(0, len(data), MAX_RESPONSE_CHUNK_SIZE):
yield data[i:i + MAX_RESPONSE_CHUNK_SIZE]
cdef Py_ssize_t i
for i in range(0, len(data), _MAX_RESPONSE_CHUNK_SIZE):
yield data[i:i + _MAX_RESPONSE_CHUNK_SIZE]
yield b''


Expand Down Expand Up @@ -86,7 +87,7 @@ async def send_asgi_response(Response response, object send):
'more_body': False
})
else:
if content.length > MAX_RESPONSE_CHUNK_SIZE:
if content.length > _MAX_RESPONSE_CHUNK_SIZE:
# Note: get_chunks yields the closing bytes fragment therefore
# we do not need to check for the closing message!
for chunk in get_chunks(content.body):
Expand Down
8 changes: 8 additions & 0 deletions tests/test_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,11 @@ def test_cookie_value_multibyte_within_limit():
value = "ñ" * 1000
cookie = Cookie("multibyte", value)
assert cookie.value == value


def test_parse_cookie_large_max_age():
# Regression test: max-age > 2^31 must not raise OverflowError (issue #675).
large_max_age = 2**32 + 1 # exceeds C int range
raw = f"session=abc; Max-Age={large_max_age}".encode()
cookie = parse_cookie(raw)
assert cookie.max_age == large_max_age
18 changes: 18 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,24 @@ def greet(self):
assert f' "{name}": ' in raw


def test_get_chunks_large_data():
"""Regression test for OverflowError with data > 2GB (issue #675).
Uses a mock large bytearray via memoryview to avoid allocating 2GB."""
from blacksheep.scribe import get_chunks

MAX_RESPONSE_CHUNK_SIZE = 61440

# Simulate offset that would overflow a C int (> 2^31 - 1 bytes)
# by using a large repeated pattern split across many chunks.
chunk_count = 50
data = b"x" * (MAX_RESPONSE_CHUNK_SIZE * chunk_count)
chunks = list(get_chunks(data))
# last chunk is the closing empty bytes
assert chunks[-1] == b""
assert len(chunks) == chunk_count + 1
assert all(len(c) == MAX_RESPONSE_CHUNK_SIZE for c in chunks[:-1])


async def test_response_raise_for_status():
response = Response(200)
await response.raise_for_status()
Expand Down
Loading