diff --git a/py/api/images.py b/py/api/images.py index 5076540..ebdfd87 100644 --- a/py/api/images.py +++ b/py/api/images.py @@ -311,7 +311,9 @@ class ImageRoutesMixin: async def generate_thumbnails_route(request): return await self.generate_thumbnails(request) - @routes.get("/prompt_manager/images/generate-thumbnails/progress") + # POST only: the stream writes files, so it must not be reachable + # from a prefetch, link preview or . + @routes.post("/prompt_manager/images/generate-thumbnails/progress") async def generate_thumbnails_progress_route(request): return await self.generate_thumbnails_with_progress(request) @@ -772,10 +774,12 @@ class ImageRoutesMixin: return {**result, "action": "error", "error": _safe_error(e)} async def generate_thumbnails_with_progress(self, request): - """Generate thumbnails with Server-Sent Events progress updates. + """Generate thumbnails with Server-Sent Events progress updates (POST). - Every blocking step (scan, PIL/ffmpeg) runs in the executor; the - event loop only writes SSE frames between files. + ``quality`` is read from the query string so a POST-capable SSE + client can keep using the same URL shape. Every blocking step (scan, + PIL/ffmpeg) runs in the executor; the event loop only writes SSE + frames between files. """ thumbnail_size = self._thumbnail_size(request.query.get("quality", "medium")) response = web.StreamResponse( diff --git a/tests/test_api_images.py b/tests/test_api_images.py index a080ba6..48f0796 100644 --- a/tests/test_api_images.py +++ b/tests/test_api_images.py @@ -377,14 +377,24 @@ class ThumbnailTestCase(ImageAPITestCase): class TestThumbnailProgressStream(ThumbnailTestCase): - """GET /prompt_manager/images/generate-thumbnails/progress (SSE).""" + """POST /prompt_manager/images/generate-thumbnails/progress (SSE).""" + + async def test_get_is_method_not_allowed(self): + # The stream writes thumbnails, so a plain GET (prefetch, link + # preview, ) must not be able to trigger it. + make_png(self.output_dir / "a.png") + + resp = await self.client.request("GET", self.PROGRESS_URL) + + self.assertEqual(resp.status, 405) + self.assertFalse(self._thumb_path("a").exists()) async def test_three_pngs_yield_three_thumbnails_and_events(self): for name in ("a", "b", "c"): make_png(self.output_dir / f"{name}.png", size=(64, 64)) calls = self._spy_executor() - resp = await self.client.request("GET", f"{self.PROGRESS_URL}?quality=low") + resp = await self.client.request("POST", f"{self.PROGRESS_URL}?quality=low") self.assertEqual(resp.status, 200) self.assertEqual(resp.headers["Content-Type"], "text/event-stream") @@ -407,7 +417,7 @@ class TestThumbnailProgressStream(ThumbnailTestCase): (self.output_dir / "bad.png").write_bytes(b"\x89PNG definitely not a png") make_png(self.output_dir / "good2.png") - resp = await self.client.request("GET", self.PROGRESS_URL) + resp = await self.client.request("POST", self.PROGRESS_URL) events = parse_sse(await resp.text()) file_errors = [d for e, d in events if e == "file_error"] @@ -423,24 +433,24 @@ class TestThumbnailProgressStream(ThumbnailTestCase): async def test_response_has_no_wildcard_cors_header(self): make_png(self.output_dir / "one.png") - resp = await self.client.request("GET", self.PROGRESS_URL) + resp = await self.client.request("POST", self.PROGRESS_URL) await resp.text() self.assertNotIn("Access-Control-Allow-Origin", resp.headers) async def test_existing_newer_thumbnail_is_skipped(self): make_png(self.output_dir / "one.png") - first = await self.client.request("GET", self.PROGRESS_URL) + first = await self.client.request("POST", self.PROGRESS_URL) await first.text() - second = await self.client.request("GET", self.PROGRESS_URL) + second = await self.client.request("POST", self.PROGRESS_URL) complete = dict(parse_sse(await second.text()))["complete"] self.assertEqual(complete["skipped"], 1) self.assertEqual(complete["count"], 0) async def test_empty_output_dir_completes_with_zero(self): - resp = await self.client.request("GET", self.PROGRESS_URL) + resp = await self.client.request("POST", self.PROGRESS_URL) events = parse_sse(await resp.text()) self.assertEqual(events[-1][0], "complete") @@ -449,7 +459,7 @@ class TestThumbnailProgressStream(ThumbnailTestCase): async def test_missing_output_dir_sends_error_event(self): self.api._find_comfyui_output_dir = lambda: None - resp = await self.client.request("GET", self.PROGRESS_URL) + resp = await self.client.request("POST", self.PROGRESS_URL) events = parse_sse(await resp.text()) self.assertEqual(events[-1][0], "error") @@ -457,7 +467,7 @@ class TestThumbnailProgressStream(ThumbnailTestCase): async def test_unknown_quality_falls_back_to_medium(self): make_png(self.output_dir / "one.png", size=(400, 400)) - resp = await self.client.request("GET", f"{self.PROGRESS_URL}?quality=huge") + resp = await self.client.request("POST", f"{self.PROGRESS_URL}?quality=huge") await resp.text() with Image.open(self._thumb_path("one")) as thumb: @@ -1085,7 +1095,7 @@ class TestThumbnailUnits(ImageRouteCoverageCase): async def test_progress_stream_reports_internal_failure(self): self.api._thumbnail_targets = _raise resp = await self.client.request( - "GET", "/prompt_manager/images/generate-thumbnails/progress" + "POST", "/prompt_manager/images/generate-thumbnails/progress" ) events = parse_sse(await resp.text()) self.assertEqual(events[-1][0], "error")