fix: allow long-running community image fetches

This commit is contained in:
ethanfel committed 2026-09-06 01:35:26 +02:00
1 parent 6778110c50
commit 89d8119e8f
3 files changed
+137 -1

No files matched your search

+1
View File
@@ -104,6 +104,7 @@ Selecting a LoRA loader opens the **LoRA Info** sidebar and follows the node's c
- If the selected LoRA is indexed by the remote LoRA Manager, the sidebar shows its image or video preview, file details, base model, trigger words, tags, usage tips, and direct model links.
- Cached community creations and Civitai examples appear with video controls, their shared prompt and negative prompt, generation settings, attribution, navigation, and one-click prompt copying.
- Community image fetches and per-model refreshes can run beyond the normal connection timeout; progress updates and cancellation remain available while they finish.
- If a node contains multiple active LoRAs, use the selector at the top of the sidebar to switch cards.
- LoRA Info closes automatically after the selected LoRA loader is cleared, but stays open while switching directly between loaders.
- If no Manager card exists, the sidebar offers name searches on LoRA Manager, Civitai, Civitai Red, and CivArchive.
+31 -1
View File
@@ -38,6 +38,12 @@ _MAX_CONFIG_BODY = 64 * 1024
_MAX_TEST_RESPONSE = 64 * 1024
_MEDIA_STREAM_CHUNK_SIZE = 64 * 1024
_MEDIA_IDLE_TIMEOUT = 300
_COMMUNITY_FETCH_ROUTES = frozenset(
{
"/api/lm/community-images/fetch",
"/api/lm/community-images/refresh-model",
}
)
# ---------------------------------------------------------------------------
# URL prefixes that should be forwarded to the remote LoRA Manager
@@ -772,6 +778,15 @@ async def _proxy_http(
"auto_decompress": False,
}
)
elif request.method == "POST" and request.path in _COMMUNITY_FETCH_ROUTES:
# Community jobs only send a response when finished. Progress
# and cancellation use separate WebSocket/HTTP connections.
request_options["timeout"] = aiohttp.ClientTimeout(
total=None,
connect=min(snapshot.timeout, 30),
sock_connect=min(snapshot.timeout, 30),
sock_read=None,
)
async with session.request(
**request_options,
) as resp:
@@ -822,7 +837,22 @@ async def _proxy_http(
)
return downstream
logger.error(
"[LM-Remote] Proxy error for %s %s: %s", request.method, request.path, exc
"[LM-Remote] Proxy error for %s %s (%s): %s",
request.method,
request.path,
type(exc).__name__,
exc,
)
if isinstance(exc, asyncio.TimeoutError):
return web.json_response(
{
"error": (
"Remote LoRA Manager request timed out. "
"The operation may still be running on the Manager; "
"check its progress before retrying."
)
},
status=504,
)
return web.json_response(
{"error": "Remote LoRA Manager unavailable."},
+105
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import asyncio
import importlib.util
import json
import sys
@@ -9,6 +10,7 @@ from pathlib import Path
import pytest
from aiohttp import web
from aiohttp.test_utils import TestServer
@pytest.fixture(scope="module")
@@ -626,6 +628,109 @@ async def test_proxy_session_does_not_retain_remote_cookies(
await proxy._close_all_proxy_sessions()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"job_path",
[
"/api/lm/community-images/fetch",
"/api/lm/community-images/refresh-model",
],
)
async def test_community_job_outlives_lookup_timeout_and_remains_cancellable(
isolated_proxy, modules, job_path
):
proxy, config = isolated_proxy
started = asyncio.Event()
finish = asyncio.Event()
async def job_handler(request):
assert await request.json() == {"hashes": ["example-hash"]}
started.set()
await finish.wait()
return web.json_response({"success": True, "stored": 2})
async def cancel_handler(request):
finish.set()
return web.json_response({"success": True, "cancelled": True})
upstream = web.Application()
upstream.router.add_post(job_path, job_handler)
upstream.router.add_post("/api/lm/community-images/cancel", cancel_handler)
async with TestServer(upstream) as server:
# A short normal timeout keeps the real HTTP regression test fast.
snapshot = modules.config.ConfigSnapshot(
config.generation, str(server.make_url("")).rstrip("/"), 0.05, ()
)
task = asyncio.create_task(
proxy._proxy_http(
DummyRequest(
method="POST",
path=job_path,
payload={"hashes": ["example-hash"]},
),
snapshot,
)
)
try:
await asyncio.wait_for(started.wait(), timeout=2)
await asyncio.sleep(0.15)
assert not task.done(), "community fetch hit the normal lookup timeout"
cancelled = await proxy._proxy_http(
DummyRequest(method="POST", path="/api/lm/community-images/cancel"),
snapshot,
)
assert cancelled.status == 200
assert response_json(cancelled)["cancelled"] is True
response = await asyncio.wait_for(task, timeout=2)
assert response.status == 200
assert response_json(response) == {"success": True, "stored": 2}
finally:
finish.set()
if not task.done():
task.cancel()
await asyncio.gather(task, return_exceptions=True)
await proxy._close_all_proxy_sessions()
@pytest.mark.asyncio
@pytest.mark.parametrize(
("method", "path"),
[
("GET", "/api/lm/community-images/status"),
("POST", "/api/lm/community-images/cancel"),
("POST", "/api/lm/community-images/by-hashes"),
("GET", "/api/lm/community-images/fetch"),
],
)
async def test_normal_community_requests_still_time_out_with_clear_error(
isolated_proxy, modules, method, path
):
proxy, config = isolated_proxy
finish = asyncio.Event()
async def slow_handler(request):
await finish.wait()
return web.json_response({"success": True})
upstream = web.Application()
upstream.router.add_route(method, path, slow_handler)
async with TestServer(upstream) as server:
snapshot = modules.config.ConfigSnapshot(
config.generation, str(server.make_url("")).rstrip("/"), 0.05, ()
)
try:
response = await asyncio.wait_for(
proxy._proxy_http(DummyRequest(method=method, path=path), snapshot),
timeout=2,
)
assert response.status == 504
assert "timed out" in response_json(response)["error"]
assert "may still be running" in response_json(response)["error"]
finally:
finish.set()
await proxy._close_all_proxy_sessions()
@pytest.mark.asyncio
async def test_proxy_session_rotation_waits_for_inflight_request(
isolated_proxy, monkeypatch