Files
DumiFlex-ComfyUI-Wildcard-P…/wp_api/spa.py
T
DumiFlex 0a8f376f4f fix(api,engine): qa hardening sweep — validators, concurrency, orphans
Lands the 15-bug catalogue from the 2026-05-19 creative-QA pass.

API layer (wp_api/):
- New _validators.py centralises meta + identifier + body-size checks
  so modules and bundles share the same shape rules
- SPA catch-all now returns JSON 404 for unknown /wp/api/* paths
  instead of the SPA shell (debugging hostile previously)
- create/update/import_from_workflow validate meta (name/desc/tags/
  category_id) before hitting the repo; tag list now rejects non-str
  entries (was silently stored, breaking the SPA type contract)
- IntegrityError on dangling category_id surfaces as 400, not 500
- Optional If-Match header on PUT enables opt-in optimistic
  concurrency — stale version → 409 instead of silent overwrite
- Bundle name collisions auto-suffixed with (copy)/(copy N) mirroring
  the modules-side forkModule rule
- Bundle children deduped by id at create + update time
- Deleting a module flags meta.orphaned=true on every bundle child
  whose snapshot referenced it so the SPA can render a stale badge

Engine layer (engine/modules/*_handler.py):
- wildcard: reject empty + __dunder var_binding, cap at 64 chars,
  reject duplicate option ids
- combine: reject empty template, cap template at 8000 chars, cap
  output_var at 64, reject __dunder output_var
- fixed_values: cap value name length, reject __dunder names, reject
  duplicate names across rows (was silent last-write-wins)
- derivation: reject duplicate rule ids (was nondeterministic for
  disabled_rule_ids / rule_order_override lookups)

Tests:
- New test_qa_hardening.py exercises every endpoint-level fix
- Extended test_payload_validation.py parametrised rejection matrix
- New test_spa_fallback.py case for /wp/api/* JSON 404

655 pytest pass, 1665 vitest pass, ruff clean.
2026-05-19 20:12:41 +03:00

52 lines
1.8 KiB
Python

"""/wp + /wp/{path:.*} — static asset server with SPA fallback.
Looks up the requested path inside ``WEB_DIR``. If a file exists, serves it.
Otherwise serves ``index.html`` so the SPA router can take over (any
``/wp/<route>`` path resolves to the SPA shell).
Exception: paths under ``/wp/api/`` that fell through every registered
API route are returned as JSON 404, not the SPA shell. Without this
guard a client typo like ``/wp/api/moduels`` returned 200 ``text/html``
(the SPA index), making API debugging hostile — the JSON.parse failure
on the client masked the real "no such endpoint" error.
"""
from __future__ import annotations
from pathlib import Path
from aiohttp import web
WEB_DIR = Path(__file__).parent.parent / "web"
async def _serve(path: str) -> web.StreamResponse:
if path:
candidate = (WEB_DIR / path).resolve()
try:
candidate.relative_to(WEB_DIR.resolve())
except ValueError:
return web.Response(status=403)
if candidate.is_file():
return web.FileResponse(candidate)
return web.FileResponse(WEB_DIR / "index.html")
async def serve_root(request: web.Request) -> web.StreamResponse:
return await _serve("")
async def serve_path(request: web.Request) -> web.StreamResponse:
path = request.match_info["path"]
# `/wp/api/...` is reserved for the JSON API. Any request reaching
# this handler under that prefix means no specific API route
# matched — return a real 404 instead of the SPA shell so the
# client gets a useful error.
if path.startswith("api/") or path == "api":
return web.json_response({"error": f"no such API route: /wp/{path}"}, status=404)
return await _serve(path)
def register(router) -> None:
router.add_get("/wp", serve_root)
router.add_get("/wp/{path:.*}", serve_path)