Compare commits
11 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| a8e39cbbf9 | |||
| 18f290f9d4 | |||
| bf66f14aa8 | |||
| 1607edbe99 | |||
| f387cce8db | |||
| 4db296e29d | |||
| b97d647d7d | |||
| b6c3537d32 | |||
| 67a23d4b52 | |||
| 4a028ea762 | |||
| e5a174588e |
@@ -1,219 +0,0 @@
|
|||||||
From 9f7ec2eb512fcc3fe90b43cb9dd9e1d08696bec1 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Michael Honaker <37811263+HonakerM@users.noreply.github.com>
|
|
||||||
Date: Mon, 21 Jul 2025 02:24:02 +0900
|
|
||||||
Subject: [PATCH] Make UploadFile check for future rollover (#2962)
|
|
||||||
|
|
||||||
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
|
|
||||||
---
|
|
||||||
starlette/datastructures.py | 22 ++++++++++---
|
|
||||||
tests/test_formparsers.py | 66 +++++++++++++++++++++++++++++++++++--
|
|
||||||
2 files changed, 82 insertions(+), 6 deletions(-)
|
|
||||||
|
|
||||||
Index: starlette-0.41.3/starlette/datastructures.py
|
|
||||||
===================================================================
|
|
||||||
--- starlette-0.41.3.orig/starlette/datastructures.py
|
|
||||||
+++ starlette-0.41.3/starlette/datastructures.py
|
|
||||||
@@ -424,6 +424,10 @@ class UploadFile:
|
|
||||||
self.size = size
|
|
||||||
self.headers = headers or Headers()
|
|
||||||
|
|
||||||
+ # Capture max size from SpooledTemporaryFile if one is provided. This slightly speeds up future checks.
|
|
||||||
+ # Note 0 means unlimited mirroring SpooledTemporaryFile's __init__
|
|
||||||
+ self._max_mem_size = getattr(self.file, "_max_size", 0)
|
|
||||||
+
|
|
||||||
@property
|
|
||||||
def content_type(self) -> str | None:
|
|
||||||
return self.headers.get("content-type", None)
|
|
||||||
@@ -434,14 +438,24 @@ class UploadFile:
|
|
||||||
rolled_to_disk = getattr(self.file, "_rolled", True)
|
|
||||||
return not rolled_to_disk
|
|
||||||
|
|
||||||
+ def _will_roll(self, size_to_add: int) -> bool:
|
|
||||||
+ # If we're not in_memory then we will always roll
|
|
||||||
+ if not self._in_memory:
|
|
||||||
+ return True
|
|
||||||
+
|
|
||||||
+ # Check for SpooledTemporaryFile._max_size
|
|
||||||
+ future_size = self.file.tell() + size_to_add
|
|
||||||
+ return bool(future_size > self._max_mem_size) if self._max_mem_size else False
|
|
||||||
+
|
|
||||||
async def write(self, data: bytes) -> None:
|
|
||||||
+ new_data_len = len(data)
|
|
||||||
if self.size is not None:
|
|
||||||
- self.size += len(data)
|
|
||||||
+ self.size += new_data_len
|
|
||||||
|
|
||||||
- if self._in_memory:
|
|
||||||
- self.file.write(data)
|
|
||||||
- else:
|
|
||||||
+ if self._will_roll(new_data_len):
|
|
||||||
await run_in_threadpool(self.file.write, data)
|
|
||||||
+ else:
|
|
||||||
+ self.file.write(data)
|
|
||||||
|
|
||||||
async def read(self, size: int = -1) -> bytes:
|
|
||||||
if self._in_memory:
|
|
||||||
Index: starlette-0.41.3/tests/test_formparsers.py
|
|
||||||
===================================================================
|
|
||||||
--- starlette-0.41.3.orig/tests/test_formparsers.py
|
|
||||||
+++ starlette-0.41.3/tests/test_formparsers.py
|
|
||||||
@@ -1,15 +1,21 @@
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import os
|
|
||||||
+import threading
|
|
||||||
import typing
|
|
||||||
+from collections.abc import Generator
|
|
||||||
from contextlib import nullcontext as does_not_raise
|
|
||||||
+from io import BytesIO
|
|
||||||
from pathlib import Path
|
|
||||||
+from tempfile import SpooledTemporaryFile
|
|
||||||
+from typing import Any, ClassVar
|
|
||||||
+from unittest import mock
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from starlette.applications import Starlette
|
|
||||||
from starlette.datastructures import UploadFile
|
|
||||||
-from starlette.formparsers import MultiPartException, _user_safe_decode
|
|
||||||
+from starlette.formparsers import MultiPartException, MultiPartParser, _user_safe_decode
|
|
||||||
from starlette.requests import Request
|
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
from starlette.routing import Mount
|
|
||||||
@@ -104,6 +110,22 @@ async def app_read_body(scope: Scope, re
|
|
||||||
await response(scope, receive, send)
|
|
||||||
|
|
||||||
|
|
||||||
+async def app_monitor_thread(scope: Scope, receive: Receive, send: Send) -> None:
|
|
||||||
+ """Helper app to monitor what thread the app was called on.
|
|
||||||
+
|
|
||||||
+ This can later be used to validate thread/event loop operations.
|
|
||||||
+ """
|
|
||||||
+ request = Request(scope, receive)
|
|
||||||
+
|
|
||||||
+ # Make sure we parse the form
|
|
||||||
+ await request.form()
|
|
||||||
+ await request.close()
|
|
||||||
+
|
|
||||||
+ # Send back the current thread id
|
|
||||||
+ response = JSONResponse({"thread_ident": threading.current_thread().ident})
|
|
||||||
+ await response(scope, receive, send)
|
|
||||||
+
|
|
||||||
+
|
|
||||||
def make_app_max_parts(max_files: int = 1000, max_fields: int = 1000) -> ASGIApp:
|
|
||||||
async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
||||||
request = Request(scope, receive)
|
|
||||||
@@ -303,6 +325,88 @@ def test_multipart_request_mixed_files_a
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
+class ThreadTrackingSpooledTemporaryFile(SpooledTemporaryFile[bytes]):
|
|
||||||
+ """Helper class to track which threads performed the rollover operation.
|
|
||||||
+
|
|
||||||
+ This is not threadsafe/multi-test safe.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ rollover_threads: ClassVar[set[int | None]] = set()
|
|
||||||
+
|
|
||||||
+ def rollover(self) -> None:
|
|
||||||
+ ThreadTrackingSpooledTemporaryFile.rollover_threads.add(threading.current_thread().ident)
|
|
||||||
+ super().rollover()
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+@pytest.fixture
|
|
||||||
+def mock_spooled_temporary_file() -> Generator[None]:
|
|
||||||
+ try:
|
|
||||||
+ with mock.patch("starlette.formparsers.SpooledTemporaryFile", ThreadTrackingSpooledTemporaryFile):
|
|
||||||
+ yield
|
|
||||||
+ finally:
|
|
||||||
+ ThreadTrackingSpooledTemporaryFile.rollover_threads.clear()
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def test_multipart_request_large_file_rollover_in_background_thread(
|
|
||||||
+ mock_spooled_temporary_file: None, test_client_factory: TestClientFactory
|
|
||||||
+) -> None:
|
|
||||||
+ """Test that Spooled file rollovers happen in background threads."""
|
|
||||||
+ data = BytesIO(b" " * (MultiPartParser.spool_max_size + 1))
|
|
||||||
+
|
|
||||||
+ client = test_client_factory(app_monitor_thread)
|
|
||||||
+ response = client.post("/", files=[("test_large", data)])
|
|
||||||
+ assert response.status_code == 200
|
|
||||||
+
|
|
||||||
+ # Parse the event thread id from the API response and ensure we have one
|
|
||||||
+ app_thread_ident = response.json().get("thread_ident")
|
|
||||||
+ assert app_thread_ident is not None
|
|
||||||
+
|
|
||||||
+ # Ensure the app thread was not the same as the rollover one and that a rollover thread exists
|
|
||||||
+ assert app_thread_ident not in ThreadTrackingSpooledTemporaryFile.rollover_threads
|
|
||||||
+ assert len(ThreadTrackingSpooledTemporaryFile.rollover_threads) == 1
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+class ThreadTrackingSpooledTemporaryFile(SpooledTemporaryFile[bytes]):
|
|
||||||
+ """Helper class to track which threads performed the rollover operation.
|
|
||||||
+
|
|
||||||
+ This is not threadsafe/multi-test safe.
|
|
||||||
+ """
|
|
||||||
+
|
|
||||||
+ rollover_threads: ClassVar[set[int | None]] = set()
|
|
||||||
+
|
|
||||||
+ def rollover(self) -> None:
|
|
||||||
+ ThreadTrackingSpooledTemporaryFile.rollover_threads.add(threading.current_thread().ident)
|
|
||||||
+ super().rollover()
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+@pytest.fixture
|
|
||||||
+def mock_spooled_temporary_file() -> Generator[None]:
|
|
||||||
+ try:
|
|
||||||
+ with mock.patch("starlette.formparsers.SpooledTemporaryFile", ThreadTrackingSpooledTemporaryFile):
|
|
||||||
+ yield
|
|
||||||
+ finally:
|
|
||||||
+ ThreadTrackingSpooledTemporaryFile.rollover_threads.clear()
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def test_multipart_request_large_file_rollover_in_background_thread(
|
|
||||||
+ mock_spooled_temporary_file: None, test_client_factory: TestClientFactory
|
|
||||||
+) -> None:
|
|
||||||
+ """Test that Spooled file rollovers happen in background threads."""
|
|
||||||
+ data = BytesIO(b" " * (MultiPartParser.spool_max_size + 1))
|
|
||||||
+
|
|
||||||
+ client = test_client_factory(app_monitor_thread)
|
|
||||||
+ response = client.post("/", files=[("test_large", data)])
|
|
||||||
+ assert response.status_code == 200
|
|
||||||
+
|
|
||||||
+ # Parse the event thread id from the API response and ensure we have one
|
|
||||||
+ app_thread_ident = response.json().get("thread_ident")
|
|
||||||
+ assert app_thread_ident is not None
|
|
||||||
+
|
|
||||||
+ # Ensure the app thread was not the same as the rollover one and that a rollover thread exists
|
|
||||||
+ assert app_thread_ident not in ThreadTrackingSpooledTemporaryFile.rollover_threads
|
|
||||||
+ assert len(ThreadTrackingSpooledTemporaryFile.rollover_threads) == 1
|
|
||||||
+
|
|
||||||
+
|
|
||||||
def test_multipart_request_with_charset_for_filename(tmpdir: Path, test_client_factory: TestClientFactory) -> None:
|
|
||||||
client = test_client_factory(app)
|
|
||||||
response = client.post(
|
|
||||||
Index: starlette-0.41.3/starlette/formparsers.py
|
|
||||||
===================================================================
|
|
||||||
--- starlette-0.41.3.orig/starlette/formparsers.py
|
|
||||||
+++ starlette-0.41.3/starlette/formparsers.py
|
|
||||||
@@ -122,7 +122,10 @@ class FormParser:
|
|
||||||
|
|
||||||
|
|
||||||
class MultiPartParser:
|
|
||||||
- max_file_size = 1024 * 1024 # 1MB
|
|
||||||
+ spool_max_size = 1024 * 1024 # 1MB
|
|
||||||
+ """The maximum size of the spooled temporary file used to store file data."""
|
|
||||||
+ max_part_size = 1024 * 1024 # 1MB
|
|
||||||
+ """The maximum size of a part in the multipart request."""
|
|
||||||
max_part_size = 1024 * 1024 # 1MB
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
@@ -201,7 +204,7 @@ class MultiPartParser:
|
|
||||||
if self._current_files > self.max_files:
|
|
||||||
raise MultiPartException(f"Too many files. Maximum number of files is {self.max_files}.")
|
|
||||||
filename = _user_safe_decode(options[b"filename"], self._charset)
|
|
||||||
- tempfile = SpooledTemporaryFile(max_size=self.max_file_size)
|
|
||||||
+ tempfile = SpooledTemporaryFile(max_size=self.spool_max_size)
|
|
||||||
self._files_to_close_on_error.append(tempfile)
|
|
||||||
self._current_part.file = UploadFile(
|
|
||||||
file=tempfile, # type: ignore[arg-type]
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
From 4ea6e22b489ec388d6004cfbca52dd5b147127c5 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Marcelo Trylesinski <marcelotryle@gmail.com>
|
|
||||||
Date: Tue, 28 Oct 2025 18:14:01 +0100
|
|
||||||
Subject: [PATCH] Merge commit from fork
|
|
||||||
|
|
||||||
---
|
|
||||||
starlette/responses.py | 46 ++++++++++++++++++++++++++++-------------
|
|
||||||
tests/test_responses.py | 28 +++++++++++++++++++++++++
|
|
||||||
2 files changed, 60 insertions(+), 14 deletions(-)
|
|
||||||
|
|
||||||
Index: starlette-0.41.3/starlette/responses.py
|
|
||||||
===================================================================
|
|
||||||
--- starlette-0.41.3.orig/starlette/responses.py
|
|
||||||
+++ starlette-0.41.3/starlette/responses.py
|
|
||||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
||||||
import http.cookies
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
-import re
|
|
||||||
import stat
|
|
||||||
import typing
|
|
||||||
import warnings
|
|
||||||
@@ -272,9 +271,6 @@ class RangeNotSatisfiable(Exception):
|
|
||||||
self.max_size = max_size
|
|
||||||
|
|
||||||
|
|
||||||
-_RANGE_PATTERN = re.compile(r"(\d*)-(\d*)")
|
|
||||||
-
|
|
||||||
-
|
|
||||||
class FileResponse(Response):
|
|
||||||
chunk_size = 64 * 1024
|
|
||||||
|
|
||||||
@@ -435,8 +431,8 @@ class FileResponse(Response):
|
|
||||||
etag = f'"{md5_hexdigest(etag_base.encode(), usedforsecurity=False)}"'
|
|
||||||
return http_if_range == formatdate(stat_result.st_mtime, usegmt=True) or http_if_range == etag
|
|
||||||
|
|
||||||
- @staticmethod
|
|
||||||
- def _parse_range_header(http_range: str, file_size: int) -> list[tuple[int, int]]:
|
|
||||||
+ @classmethod
|
|
||||||
+ def _parse_range_header(cls, http_range: str, file_size: int) -> list[tuple[int, int]]:
|
|
||||||
ranges: list[tuple[int, int]] = []
|
|
||||||
try:
|
|
||||||
units, range_ = http_range.split("=", 1)
|
|
||||||
@@ -448,14 +444,7 @@ class FileResponse(Response):
|
|
||||||
if units != "bytes":
|
|
||||||
raise MalformedRangeHeader("Only support bytes range")
|
|
||||||
|
|
||||||
- ranges = [
|
|
||||||
- (
|
|
||||||
- int(_[0]) if _[0] else file_size - int(_[1]),
|
|
||||||
- int(_[1]) + 1 if _[0] and _[1] and int(_[1]) < file_size else file_size,
|
|
||||||
- )
|
|
||||||
- for _ in _RANGE_PATTERN.findall(range_)
|
|
||||||
- if _ != ("", "")
|
|
||||||
- ]
|
|
||||||
+ ranges = cls._parse_ranges(range_, file_size)
|
|
||||||
|
|
||||||
if len(ranges) == 0:
|
|
||||||
raise MalformedRangeHeader("Range header: range must be requested")
|
|
||||||
@@ -487,6 +476,35 @@ class FileResponse(Response):
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
+ @classmethod
|
|
||||||
+ def _parse_ranges(cls, range_: str, file_size: int) -> list[tuple[int, int]]:
|
|
||||||
+ ranges: list[tuple[int, int]] = []
|
|
||||||
+
|
|
||||||
+ for part in range_.split(","):
|
|
||||||
+ part = part.strip()
|
|
||||||
+
|
|
||||||
+ # If the range is empty or a single dash, we ignore it.
|
|
||||||
+ if not part or part == "-":
|
|
||||||
+ continue
|
|
||||||
+
|
|
||||||
+ # If the range is not in the format "start-end", we ignore it.
|
|
||||||
+ if "-" not in part:
|
|
||||||
+ continue
|
|
||||||
+
|
|
||||||
+ start_str, end_str = part.split("-", 1)
|
|
||||||
+ start_str = start_str.strip()
|
|
||||||
+ end_str = end_str.strip()
|
|
||||||
+
|
|
||||||
+ try:
|
|
||||||
+ start = int(start_str) if start_str else file_size - int(end_str)
|
|
||||||
+ end = int(end_str) + 1 if start_str and end_str and int(end_str) < file_size else file_size
|
|
||||||
+ ranges.append((start, end))
|
|
||||||
+ except ValueError:
|
|
||||||
+ # If the range is not numeric, we ignore it.
|
|
||||||
+ continue
|
|
||||||
+
|
|
||||||
+ return ranges
|
|
||||||
+
|
|
||||||
def generate_multipart(
|
|
||||||
self,
|
|
||||||
ranges: typing.Sequence[tuple[int, int]],
|
|
||||||
Index: starlette-0.41.3/tests/test_responses.py
|
|
||||||
===================================================================
|
|
||||||
--- starlette-0.41.3.orig/tests/test_responses.py
|
|
||||||
+++ starlette-0.41.3/tests/test_responses.py
|
|
||||||
@@ -684,6 +684,34 @@ def test_file_response_insert_ranges(fil
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
+def test_file_response_range_without_dash(file_response_client: TestClient) -> None:
|
|
||||||
+ response = file_response_client.get("/", headers={"Range": "bytes=100, 0-50"})
|
|
||||||
+ assert response.status_code == 206
|
|
||||||
+ assert response.headers["content-range"] == f"bytes 0-50/{len(README.encode('utf8'))}"
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def test_file_response_range_empty_start_and_end(file_response_client: TestClient) -> None:
|
|
||||||
+ response = file_response_client.get("/", headers={"Range": "bytes= - , 0-50"})
|
|
||||||
+ assert response.status_code == 206
|
|
||||||
+ assert response.headers["content-range"] == f"bytes 0-50/{len(README.encode('utf8'))}"
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def test_file_response_range_ignore_non_numeric(file_response_client: TestClient) -> None:
|
|
||||||
+ response = file_response_client.get("/", headers={"Range": "bytes=abc-def, 0-50"})
|
|
||||||
+ assert response.status_code == 206
|
|
||||||
+ assert response.headers["content-range"] == f"bytes 0-50/{len(README.encode('utf8'))}"
|
|
||||||
+
|
|
||||||
+
|
|
||||||
+def test_file_response_suffix_range(file_response_client: TestClient) -> None:
|
|
||||||
+ # Test suffix range (last N bytes) - line 523 with empty start_str
|
|
||||||
+ response = file_response_client.get("/", headers={"Range": "bytes=-100"})
|
|
||||||
+ assert response.status_code == 206
|
|
||||||
+ file_size = len(README.encode("utf8"))
|
|
||||||
+ assert response.headers["content-range"] == f"bytes {file_size - 100}-{file_size - 1}/{file_size}"
|
|
||||||
+ assert response.headers["content-length"] == "100"
|
|
||||||
+ assert response.content == README.encode("utf8")[-100:]
|
|
||||||
+
|
|
||||||
+
|
|
||||||
@pytest.mark.anyio
|
|
||||||
async def test_file_response_multi_small_chunk_size(readme_file: Path) -> None:
|
|
||||||
class SmallChunkSizeFileResponse(FileResponse):
|
|
||||||
@@ -1,12 +1,86 @@
|
|||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Wed Oct 29 14:29:58 UTC 2025 - Nico Krapp <nico.krapp@suse.com>
|
Wed Oct 29 09:14:32 UTC 2025 - Nico Krapp <nico.krapp@suse.com>
|
||||||
|
|
||||||
- Add CVE-2025-62727.patch to fix CVE-2025-62727 (bsc#1252805)
|
- Update to 0.49.1 (fixes CVE-2025-62727, bsc#1252805)
|
||||||
|
* This release fixes a security vulnerability in the parsing logic of the
|
||||||
|
Range header in FileResponse. You can view the full security advisory:
|
||||||
|
GHSA-7f5h-v6xp-fcq8
|
||||||
|
* Optimize the HTTP ranges parsing logic
|
||||||
|
- Update to 0.49.0
|
||||||
|
* Add encoding parameter to Config class
|
||||||
|
* Support multiple cookie headers in Request.cookies
|
||||||
|
* Use Literal type for WebSocketEndpoint encoding values
|
||||||
|
* Do not pollute exception context in Middleware when using
|
||||||
|
BaseHTTPMiddleware
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Thu Jul 31 09:20:02 UTC 2025 - Nico Krapp <nico.krapp@suse.com>
|
Fri Sep 26 08:49:58 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaubitz@suse.com>
|
||||||
|
|
||||||
- Add CVE-2025-54121.patch to fix CVE-2025-54121 (bsc#1246855)
|
- Update to 0.48.0
|
||||||
|
* Add official Python 3.14 support #3013.
|
||||||
|
* Implement RFC9110 http status names #2939.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Tue Jul 29 07:20:41 UTC 2025 - Steve Kowalik <steven.kowalik@suse.com>
|
||||||
|
|
||||||
|
- Correct typing_extensions Requires.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Tue Jul 22 07:41:52 UTC 2025 - Nico Krapp <nico.krapp@suse.com>
|
||||||
|
|
||||||
|
- Update to 0.47.2 (fixes CVE-2025-54121, bsc#1246855)
|
||||||
|
* Make UploadFile check for future rollover #2962.
|
||||||
|
- Update to 0.47.1
|
||||||
|
* Use Self in TestClient.__enter__ #2951
|
||||||
|
* Allow async exception handlers to type-check #2949
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Wed Jun 4 12:21:42 UTC 2025 - John Paul Adrian Glaubitz <adrian.glaubitz@suse.com>
|
||||||
|
|
||||||
|
- Update to 0.47.0
|
||||||
|
* Added
|
||||||
|
+ Add support for ASGI pathsend extension #2671.
|
||||||
|
+ Add partitioned attribute to Response.set_cookie #2501.
|
||||||
|
* Changed
|
||||||
|
+ Change methods parameter type from list[str] to Collection[str] #2903.
|
||||||
|
+ Replace import typing by from typing import ... in the whole codebase #2867.
|
||||||
|
* Fixed
|
||||||
|
+ Mark ExceptionMiddleware.http_exception as async to prevent thread creation #2922.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
Thu May 1 06:01:43 UTC 2025 - Steve Kowalik <steven.kowalik@suse.com>
|
||||||
|
|
||||||
|
- Update to 0.46.2:
|
||||||
|
* Added
|
||||||
|
+ GZipMiddleware: Make sure Vary header is always added if a response can
|
||||||
|
be compressed
|
||||||
|
+ Add max_part_size parameter to Request.form()
|
||||||
|
+ Add client parameter to TestClient
|
||||||
|
+ Make UUID path parameter conversion more flexible
|
||||||
|
+ Raise ClientDisconnect on StreamingResponse
|
||||||
|
* Fixed
|
||||||
|
+ Use correct index on backwards compatible logic in TemplateResponse
|
||||||
|
+ Prevents reraising of exception from BaseHTTPMiddleware
|
||||||
|
+ w relative directory path when follow_symlinks=True
|
||||||
|
+ Raise exception from background task on BaseHTTPMiddleware
|
||||||
|
+ GZipMiddleware: Don't compress on server sent events
|
||||||
|
+ Turn directory into string on lookup_path on commonpath comparison
|
||||||
|
+ Make create_memory_object_stream compatible with old anyio versions
|
||||||
|
once again, and bump anyio minimum version to 3.6.2
|
||||||
|
+ Collect errors more reliably from WebSocket test client
|
||||||
|
+ Fix unclosed MemoryObjectReceiveStream upon exception in
|
||||||
|
BaseHTTPMiddleware children
|
||||||
|
+ Use ETag from headers when parsing If-Range in FileResponse
|
||||||
|
+ Follow directory symlinks in StaticFiles when follow_symlinks=True
|
||||||
|
+ Bump minimum python-multipart version to 0.0.18
|
||||||
|
+ Bump minimum httpx version to 0.27.0
|
||||||
|
* Removed
|
||||||
|
+ Drop Python 3.8
|
||||||
|
+ Remove ExceptionMiddleware import proxy from starlette.exceptions
|
||||||
|
module
|
||||||
|
+ Remove deprecated WS_1004_NO_STATUS_RCVD and WS_1005_ABNORMAL_CLOSURE
|
||||||
|
+ Remove deprecated allow_redirects argument from TestClient
|
||||||
|
- Drop patch starlette-pr2773-httpx0.28.patch, included upstream.
|
||||||
|
|
||||||
-------------------------------------------------------------------
|
-------------------------------------------------------------------
|
||||||
Thu Dec 12 16:09:18 UTC 2024 - Ben Greiner <code@bnavigator.de>
|
Thu Dec 12 16:09:18 UTC 2024 - Ben Greiner <code@bnavigator.de>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#
|
#
|
||||||
# spec file for package python-starlette
|
# spec file for package python-starlette
|
||||||
#
|
#
|
||||||
# Copyright (c) 2024 SUSE LLC
|
# Copyright (c) 2025 SUSE LLC and contributors
|
||||||
#
|
#
|
||||||
# All modifications and additions to the file contributed by third parties
|
# All modifications and additions to the file contributed by third parties
|
||||||
# remain the property of their copyright owners, unless otherwise agreed
|
# remain the property of their copyright owners, unless otherwise agreed
|
||||||
@@ -27,37 +27,29 @@
|
|||||||
|
|
||||||
%{?sle15_python_module_pythons}
|
%{?sle15_python_module_pythons}
|
||||||
Name: python-starlette%{psuffix}
|
Name: python-starlette%{psuffix}
|
||||||
Version: 0.41.3
|
Version: 0.49.1
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: Lightweight ASGI framework/toolkit
|
Summary: Lightweight ASGI framework/toolkit
|
||||||
License: BSD-3-Clause
|
License: BSD-3-Clause
|
||||||
URL: https://github.com/encode/starlette
|
URL: https://github.com/encode/starlette
|
||||||
Source: https://github.com/encode/starlette/archive/refs/tags/%{version}.tar.gz#/starlette-%{version}.tar.gz
|
Source: https://github.com/encode/starlette/archive/refs/tags/%{version}.tar.gz#/starlette-%{version}.tar.gz
|
||||||
# PATCH-FIX-UPSTREAM starlette-pr2773-httpx0.28.patch gh#encode/starlette#2773
|
BuildRequires: %{python_module base >= 3.9}
|
||||||
Patch0: https://github.com/encode/starlette/pull/2773.patch#/starlette-pr2773-httpx0.28.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2025-54121.patch bsc#1246855
|
|
||||||
# taken from https://github.com/encode/starlette/commit/9f7ec2eb512fcc3fe90b43cb9dd9e1d08696bec1
|
|
||||||
# and https://github.com/encode/starlette/commit/4ae3213ca557a25aff91f1d43d530ca3962c729d
|
|
||||||
Patch1: CVE-2025-54121.patch
|
|
||||||
# PATCH-FIX-UPSTREAM CVE-2025-62727.patch https://github.com/Kludex/starlette/commit/4ea6e22b489ec388d6004cfbca52dd5b147127c5
|
|
||||||
Patch2: CVE-2025-62727.patch
|
|
||||||
BuildRequires: %{python_module base >= 3.8}
|
|
||||||
BuildRequires: %{python_module hatchling}
|
BuildRequires: %{python_module hatchling}
|
||||||
BuildRequires: %{python_module pip}
|
BuildRequires: %{python_module pip}
|
||||||
BuildRequires: fdupes
|
BuildRequires: fdupes
|
||||||
BuildRequires: python-rpm-macros
|
BuildRequires: python-rpm-macros
|
||||||
Requires: python-anyio >= 3.4.0
|
Requires: python-anyio >= 3.6.2
|
||||||
Requires: (python-typing_extensions >= 3.10.0 if python-base < 3.10)
|
Requires: (python-typing_extensions >= 4.10.0 if python-base < 3.13)
|
||||||
BuildArch: noarch
|
BuildArch: noarch
|
||||||
%if %{with test}
|
%if %{with test}
|
||||||
BuildRequires: %{python_module anyio >= 3.4.0}
|
BuildRequires: %{python_module anyio >= 3.6.2}
|
||||||
# typing_extensions, see below
|
# typing_extensions, see below
|
||||||
# SECTION [full]
|
# SECTION [full]
|
||||||
BuildRequires: %{python_module PyYAML}
|
BuildRequires: %{python_module PyYAML}
|
||||||
BuildRequires: %{python_module Jinja2}
|
BuildRequires: %{python_module Jinja2}
|
||||||
BuildRequires: %{python_module httpx >= 0.28}
|
BuildRequires: %{python_module httpx >= 0.28}
|
||||||
BuildRequires: %{python_module itsdangerous}
|
BuildRequires: %{python_module itsdangerous}
|
||||||
BuildRequires: %{python_module python-multipart >= 0.0.7}
|
BuildRequires: %{python_module python-multipart >= 0.0.18}
|
||||||
# /SECTION
|
# /SECTION
|
||||||
# SECTION test
|
# SECTION test
|
||||||
BuildRequires: %{python_module exceptiongroup}
|
BuildRequires: %{python_module exceptiongroup}
|
||||||
@@ -99,7 +91,7 @@ building high performance asyncio services.
|
|||||||
ignored_tests="test_set_cookie"
|
ignored_tests="test_set_cookie"
|
||||||
ignored_tests="$ignored_tests or test_expires_on_set_cookie"
|
ignored_tests="$ignored_tests or test_expires_on_set_cookie"
|
||||||
# fails to raise a deprecation warning as of 2024/04/25
|
# fails to raise a deprecation warning as of 2024/04/25
|
||||||
ignored_tests="$ignored_tests or test_lifespan_with_on_events"
|
##ignored_tests="$ignored_tests or test_lifespan_with_on_events"
|
||||||
%pytest -W ignore::PendingDeprecationWarning --asyncio-mode=strict -k "not ($ignored_tests)"
|
%pytest -W ignore::PendingDeprecationWarning --asyncio-mode=strict -k "not ($ignored_tests)"
|
||||||
|
|
||||||
%endif
|
%endif
|
||||||
|
|||||||
BIN
starlette-0.41.3.tar.gz
LFS
BIN
starlette-0.41.3.tar.gz
LFS
Binary file not shown.
3
starlette-0.49.1.tar.gz
Normal file
3
starlette-0.49.1.tar.gz
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:56acc444bd0380a178ceda7fdbba018f7c84c5751460afcd0eeb2d961d91cf33
|
||||||
|
size 2656482
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
From b781c571068f4afc0417c7dfb8df2eda0547af55 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Marcelo Trylesinski <marcelotryle@gmail.com>
|
|
||||||
Date: Sat, 30 Nov 2024 10:32:50 +0100
|
|
||||||
Subject: [PATCH 1/2] Pin httpx in `full` extra
|
|
||||||
|
|
||||||
---
|
|
||||||
pyproject.toml | 2 +-
|
|
||||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
||||||
|
|
||||||
diff --git a/pyproject.toml b/pyproject.toml
|
|
||||||
index a532e4628..598d4333b 100644
|
|
||||||
--- a/pyproject.toml
|
|
||||||
+++ b/pyproject.toml
|
|
||||||
@@ -37,7 +37,7 @@ full = [
|
|
||||||
"jinja2",
|
|
||||||
"python-multipart>=0.0.7",
|
|
||||||
"pyyaml",
|
|
||||||
- "httpx>=0.22.0",
|
|
||||||
+ "httpx>=0.27.0,<0.29.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.urls]
|
|
||||||
|
|
||||||
From 24de2bfc8aa99a084a9b4fcfab1e52d7a6747cd9 Mon Sep 17 00:00:00 2001
|
|
||||||
From: Marcelo Trylesinski <marcelotryle@gmail.com>
|
|
||||||
Date: Sat, 30 Nov 2024 10:51:11 +0100
|
|
||||||
Subject: [PATCH 2/2] fix test
|
|
||||||
|
|
||||||
---
|
|
||||||
tests/middleware/test_wsgi.py | 2 +-
|
|
||||||
tests/test_requests.py | 4 ++--
|
|
||||||
2 files changed, 3 insertions(+), 3 deletions(-)
|
|
||||||
|
|
||||||
diff --git a/tests/middleware/test_wsgi.py b/tests/middleware/test_wsgi.py
|
|
||||||
index 58696bb65..e4ac66ab4 100644
|
|
||||||
--- a/tests/middleware/test_wsgi.py
|
|
||||||
+++ b/tests/middleware/test_wsgi.py
|
|
||||||
@@ -77,7 +77,7 @@ def test_wsgi_post(test_client_factory: TestClientFactory) -> None:
|
|
||||||
client = test_client_factory(app)
|
|
||||||
response = client.post("/", json={"example": 123})
|
|
||||||
assert response.status_code == 200
|
|
||||||
- assert response.text == '{"example": 123}'
|
|
||||||
+ assert response.text == '{"example":123}'
|
|
||||||
|
|
||||||
|
|
||||||
def test_wsgi_exception(test_client_factory: TestClientFactory) -> None:
|
|
||||||
diff --git a/tests/test_requests.py b/tests/test_requests.py
|
|
||||||
index f0494e751..665dceb87 100644
|
|
||||||
--- a/tests/test_requests.py
|
|
||||||
+++ b/tests/test_requests.py
|
|
||||||
@@ -91,7 +91,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
||||||
assert response.json() == {"body": ""}
|
|
||||||
|
|
||||||
response = client.post("/", json={"a": "123"})
|
|
||||||
- assert response.json() == {"body": '{"a": "123"}'}
|
|
||||||
+ assert response.json() == {"body": '{"a":"123"}'}
|
|
||||||
|
|
||||||
response = client.post("/", data="abc") # type: ignore
|
|
||||||
assert response.json() == {"body": "abc"}
|
|
||||||
@@ -112,7 +112,7 @@ async def app(scope: Scope, receive: Receive, send: Send) -> None:
|
|
||||||
assert response.json() == {"body": ""}
|
|
||||||
|
|
||||||
response = client.post("/", json={"a": "123"})
|
|
||||||
- assert response.json() == {"body": '{"a": "123"}'}
|
|
||||||
+ assert response.json() == {"body": '{"a":"123"}'}
|
|
||||||
|
|
||||||
response = client.post("/", data="abc") # type: ignore
|
|
||||||
assert response.json() == {"body": "abc"}
|
|
||||||
Reference in New Issue
Block a user