mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-20 18:51:26 -03:00
models.dev is served by Cloudflare with brotli compression when the client advertises it, and brotli is a required dependency here, so aiohttp always negotiates br. A corrupted br stream can crash the native decoder with a Windows access violation (a Python-level exception handler cannot catch it), or produce garbage bytes. Send an explicit "Accept-Encoding: gzip, deflate" header on the model catalog and Ollama model-list requests so the server never returns brotli. zlib handles corrupt gzip data by raising ContentEncodingError (an aiohttp.ClientError subclass), which the existing handlers already catch and degrade to a warning with an empty-catalog fallback.
This commit is contained in:
@@ -34,6 +34,14 @@ _model_output_limits: Dict[str, Dict[str, int]] = {}
|
|||||||
|
|
||||||
_CATALOG_TIMEOUT = aiohttp.ClientTimeout(total=30)
|
_CATALOG_TIMEOUT = aiohttp.ClientTimeout(total=30)
|
||||||
|
|
||||||
|
# Cloudflare serves brotli when the client advertises it, and brotli is a
|
||||||
|
# required dependency here — a corrupted br stream can crash the native
|
||||||
|
# decoder with a Windows access violation (issue #1099). Request gzip
|
||||||
|
# instead; zlib decompression is not affected and corrupt gzip data only
|
||||||
|
# raises ContentEncodingError (an aiohttp.ClientError subclass), which the
|
||||||
|
# exception handlers below already catch.
|
||||||
|
_NO_BROTLI_HEADERS = {"Accept-Encoding": "gzip, deflate"}
|
||||||
|
|
||||||
|
|
||||||
async def _load_model_catalog() -> Dict[str, List[str]]:
|
async def _load_model_catalog() -> Dict[str, List[str]]:
|
||||||
"""Fetch and parse the model catalog.
|
"""Fetch and parse the model catalog.
|
||||||
@@ -53,7 +61,7 @@ async def _load_model_catalog() -> Dict[str, List[str]]:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=_CATALOG_TIMEOUT) as session:
|
async with aiohttp.ClientSession(timeout=_CATALOG_TIMEOUT) as session:
|
||||||
async with session.get(_MODEL_CATALOG_URL) as resp:
|
async with session.get(_MODEL_CATALOG_URL, headers=_NO_BROTLI_HEADERS) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
logger.warning("Model catalog returned HTTP %s", resp.status)
|
logger.warning("Model catalog returned HTTP %s", resp.status)
|
||||||
return _catalog_cache or {}
|
return _catalog_cache or {}
|
||||||
@@ -126,7 +134,7 @@ async def fetch_ollama_models(api_base: str) -> List[str]:
|
|||||||
url = f"{api_base.rstrip('/')}/models"
|
url = f"{api_base.rstrip('/')}/models"
|
||||||
try:
|
try:
|
||||||
async with aiohttp.ClientSession(timeout=_OLLAMA_API_TIMEOUT) as session:
|
async with aiohttp.ClientSession(timeout=_OLLAMA_API_TIMEOUT) as session:
|
||||||
async with session.get(url) as resp:
|
async with session.get(url, headers=_NO_BROTLI_HEADERS) as resp:
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
logger.debug("Ollama API returned HTTP %s from %s", resp.status, api_base)
|
logger.debug("Ollama API returned HTTP %s from %s", resp.status, api_base)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -322,8 +322,12 @@ class MockGetSession:
|
|||||||
|
|
||||||
def __init__(self, response):
|
def __init__(self, response):
|
||||||
self._response = response
|
self._response = response
|
||||||
|
self.last_url = None
|
||||||
|
self.last_headers = None
|
||||||
|
|
||||||
def get(self, url):
|
def get(self, url, headers=None):
|
||||||
|
self.last_url = url
|
||||||
|
self.last_headers = headers
|
||||||
return self._response
|
return self._response
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
@@ -373,3 +377,27 @@ class TestModelCatalog:
|
|||||||
models = await fetch_ollama_models("http://localhost:11434/v1")
|
models = await fetch_ollama_models("http://localhost:11434/v1")
|
||||||
|
|
||||||
assert models == []
|
assert models == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_catalog_request_disables_brotli_encoding(self):
|
||||||
|
"""The catalog request must not advertise br — a corrupt brotli stream
|
||||||
|
can crash the native decoder (Windows access violation, issue #1099)."""
|
||||||
|
response = MockResponse(200, json_data={})
|
||||||
|
session = MockGetSession(response)
|
||||||
|
|
||||||
|
with mock.patch("aiohttp.ClientSession", return_value=session):
|
||||||
|
await llm_module._load_model_catalog()
|
||||||
|
|
||||||
|
assert session.last_headers == {"Accept-Encoding": "gzip, deflate"}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ollama_request_disables_brotli_encoding(self):
|
||||||
|
"""The Ollama models request must not advertise br either."""
|
||||||
|
response = MockResponse(200, json_data={"data": [{"id": "llama3"}]})
|
||||||
|
session = MockGetSession(response)
|
||||||
|
|
||||||
|
with mock.patch("aiohttp.ClientSession", return_value=session):
|
||||||
|
models = await fetch_ollama_models("http://localhost:11434/v1")
|
||||||
|
|
||||||
|
assert models == ["llama3"]
|
||||||
|
assert session.last_headers == {"Accept-Encoding": "gzip, deflate"}
|
||||||
|
|||||||
Reference in New Issue
Block a user