fix(civitai): surface missing-model diagnostics and anchor image-page URL parsing

- Return the ResourceNotFoundError detail from get_civitai_versions and
  get_model_versions_status instead of a bare "Model not found", and let
  the download modal display the server-provided message
- Anchor extract_civitai_image_id / extractCivitaiImageId to
  ^/images/<id> so API paths and partial numeric IDs are rejected, same
  as the model-page parsing fix in #1140
This commit is contained in:
Will Miao
2026-10-09 08:12:56 +08:00
parent 01ffda7303
commit 18f3208468
9 changed files with 99 additions and 8 deletions
@@ -379,6 +379,12 @@ describe('civitaiUtils', () => {
it('rejects image-like URLs from unsupported hosts', () => {
expect(extractCivitaiImageId('https://example.com/images/126920345')).toBe(null);
});
it.each(['civitai.com', 'civitai.red'])('rejects non-image pages on %s', (host) => {
for (const path of ['/api/v1/images/126920345', '/images/126920345invalid']) {
expect(extractCivitaiImageId(`https://${host}${path}`)).toBe(null);
}
});
});
describe('classifyModelRelinkUrl', () => {
+33
View File
@@ -37,6 +37,7 @@ from py.utils.session_logging import (
)
from py.routes.misc_route_registrar import MISC_ROUTE_DEFINITIONS, MiscRouteRegistrar
from py.routes.misc_routes import MiscRoutes
from py.services.errors import ResourceNotFoundError
def _json_payload(response) -> dict[str, Any]:
@@ -2748,6 +2749,38 @@ async def test_get_model_versions_status_supported_type_stays_interactive():
]
@pytest.mark.asyncio
async def test_get_model_versions_status_surfaces_not_found_details():
"""A missing CivitAI model keeps the provider's diagnostic message."""
class NotFoundProvider:
async def get_model_versions(self, _model_id):
raise ResourceNotFoundError("Civitai model 3390681 was not found")
async def metadata_factory():
return NotFoundProvider()
handler = ModelLibraryHandler(
ServiceRegistryAdapter(
get_lora_scanner=fake_scanner_factory,
get_checkpoint_scanner=fake_scanner_factory,
get_embedding_scanner=fake_scanner_factory,
get_other_scanner=fake_scanner_factory,
get_downloaded_version_history_service=fake_download_history_service_factory,
),
metadata_provider_factory=metadata_factory,
)
response = await handler.get_model_versions_status(
FakeRequest(query={"modelId": "3390681"}) # pyright: ignore[reportArgumentType]
)
payload = _json_payload(response)
assert response.status == 404
assert payload["success"] is False
assert "3390681" in payload["error"]
class DummySidecarMigrationUseCase:
def __init__(self, result):
self.result = result
+38
View File
@@ -15,6 +15,7 @@ from py.routes.handlers.model_handlers import (
ModelUpdateHandler,
)
from py.routes.model_route_registrar import COMMON_ROUTE_DEFINITIONS
from py.services.errors import ResourceNotFoundError
from py.services.service_registry import ServiceRegistry
from py.utils.metadata_manager import MetadataManager
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
@@ -231,6 +232,43 @@ async def test_get_civitai_versions_degrades_when_download_history_unavailable(m
assert payload[0]["hasBeenDownloaded"] is False
@pytest.mark.asyncio
async def test_get_civitai_versions_surfaces_not_found_details():
cache = SimpleNamespace(version_index={})
service = DummyService(cache)
class NotFoundProvider:
async def get_model_versions(self, model_id):
raise ResourceNotFoundError(f"Civitai model {model_id} was not found")
async def metadata_provider_factory():
return NotFoundProvider()
handler = ModelCivitaiHandler(
service=service,
settings_service=SimpleNamespace(get=lambda *_: False), # pyright: ignore[reportArgumentType]
ws_manager=SimpleNamespace(), # pyright: ignore[reportArgumentType]
logger=logging.getLogger(__name__),
metadata_provider_factory=metadata_provider_factory,
validate_model_type=lambda *_: True,
expected_model_types=lambda: "LoRA",
find_model_file=lambda *_: None,
metadata_sync=SimpleNamespace(), # pyright: ignore[reportArgumentType]
metadata_refresh_use_case=SimpleNamespace(), # pyright: ignore[reportArgumentType]
metadata_progress_callback=lambda *_args, **_kwargs: None, # pyright: ignore[reportArgumentType]
)
response = await handler.get_civitai_versions(
SimpleNamespace(match_info={"model_id": "42"}) # pyright: ignore[reportArgumentType]
)
text = response.text
assert text is not None
payload = json.loads(text)
assert response.status == 404
assert "42" in payload["error"]
@pytest.mark.asyncio
async def test_refresh_model_updates_filters_records_without_updates():
cache = SimpleNamespace(version_index={})
+14 -1
View File
@@ -10,14 +10,27 @@ from py.utils.civitai_utils import (
resolve_license_payload,
)
@pytest.mark.parametrize("host", ["civitai.com", "civitai.red"])
@pytest.mark.parametrize("path", ["/api/download/models/3390681", "/api/v1/models/3390681", "/models/3390681invalid"])
@pytest.mark.parametrize(
"path",
["/api/download/models/3390681", "/api/v1/models/3390681", "/models/3390681invalid"],
)
def test_extract_civitai_model_url_parts_rejects_non_model_pages(host, path):
assert extract_civitai_model_url_parts(
f"https://{host}{path}?modelVersionId=3390681"
) == (None, None)
@pytest.mark.parametrize("host", ["civitai.com", "civitai.red"])
@pytest.mark.parametrize(
"path",
["/api/v1/images/126920345", "/images/126920345invalid"],
)
def test_extract_civitai_image_id_rejects_non_image_pages(host, path):
assert extract_civitai_image_id(f"https://{host}{path}") is None
def test_resolve_license_payload_defaults():
payload, flags = resolve_license_info({})