From af32747ca40d47caad1998a37bae1266484e4082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Marques?= Date: Sat, 10 Oct 2026 14:54:47 -0300 Subject: [PATCH 1/3] fix(serve): give a request that fails pydantic validation an error reason Its 400 carried only detail, so a caller reading error got none. --- maestro_worker_python/serve.py | 5 ++++- tests/test_serve.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/maestro_worker_python/serve.py b/maestro_worker_python/serve.py index eb6f656..4a58958 100644 --- a/maestro_worker_python/serve.py +++ b/maestro_worker_python/serve.py @@ -146,9 +146,12 @@ async def validation_error_handler(request: Request, exc: ValidationError): @app.exception_handler(pydantic.ValidationError) async def pydantic_validation_exception_handler(request: Request, exc: pydantic.ValidationError): + # `error` carries the reason a caller reads, as it does for ValidationError. + errors = exc.errors() + reason = "; ".join(f"{'.'.join(map(str, error['loc'])) or 'request'}: {error['msg']}" for error in errors) return JSONResponse( status_code=400, - content=jsonable_encoder({"detail": exc.errors()}), + content=jsonable_encoder({"error": reason, "detail": errors}), ) diff --git a/tests/test_serve.py b/tests/test_serve.py index 13277aa..447c6ed 100644 --- a/tests/test_serve.py +++ b/tests/test_serve.py @@ -345,6 +345,20 @@ def test_a_recoverable_failure_keeps_the_process_serving(tmp_path, monkeypatch, assert terminations == [] +def test_an_invalid_request_is_refused_with_a_reason_naming_the_field(tmp_path, monkeypatch): + _, client, _ = _serve_failing_worker( + tmp_path, + monkeypatch, + "import pydantic; type('Request', (pydantic.BaseModel,), {'__annotations__': {'stems': int}})(stems='x')", + ) + + refused = client.post("/inference", json={}) + + assert refused.status_code == 400 + assert refused.json()["error"].startswith("stems: ") + assert refused.json()["detail"][0]["loc"] == ["stems"] + + def _start_with_json_logging(tmp_path, then: str = "") -> subprocess.CompletedProcess: """Import serve.py in a fresh interpreter: json_logging configures process-wide state once.""" worker_path = tmp_path / "worker.py" From 889c392339b4a72efbe4c6fbb1b388fe187fecf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Marques?= Date: Sat, 10 Oct 2026 15:03:19 -0300 Subject: [PATCH 2/3] chore: bump version to 5.2.2 in pyproject.toml and uv.lock --- pyproject.toml | 2 +- uv.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 136a0f2..5bfe7f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "maestro-worker-python" -version = "5.2.1" +version = "5.2.2" description = "Utility to run workers on Moises/Maestro" readme = "README.md" requires-python = ">=3.10,<3.14" diff --git a/uv.lock b/uv.lock index fa005b1..3611116 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10, <3.14" [[package]] @@ -220,7 +220,7 @@ wheels = [ [[package]] name = "maestro-worker-python" -version = "5.2.1" +version = "5.2.2" source = { editable = "." } dependencies = [ { name = "fastapi" }, From 6f44ac8e3ce1a8de17fd36cc744acc646c5e0499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Marques?= Date: Sat, 10 Oct 2026 15:10:38 -0300 Subject: [PATCH 3/3] refactor(serve): name the validation-error description --- maestro_worker_python/serve.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/maestro_worker_python/serve.py b/maestro_worker_python/serve.py index 4a58958..67744f1 100644 --- a/maestro_worker_python/serve.py +++ b/maestro_worker_python/serve.py @@ -144,14 +144,23 @@ async def validation_error_handler(request: Request, exc: ValidationError): return JSONResponse(status_code=400, content=jsonable_encoder({"error": exc.reason})) +def _describe_validation_errors(errors: list) -> str: + """One line a caller can read, e.g. ``stems: Input should be a valid integer; outputs.midi: Field required``.""" + described = [] + for error in errors: + # A model-level validator reports an empty location. + field = ".".join(str(part) for part in error["loc"]) or "request" + described.append(f"{field}: {error['msg']}") + return "; ".join(described) + + @app.exception_handler(pydantic.ValidationError) async def pydantic_validation_exception_handler(request: Request, exc: pydantic.ValidationError): - # `error` carries the reason a caller reads, as it does for ValidationError. errors = exc.errors() - reason = "; ".join(f"{'.'.join(map(str, error['loc'])) or 'request'}: {error['msg']}" for error in errors) + # `error` carries the reason a caller reads, as it does for ValidationError. return JSONResponse( status_code=400, - content=jsonable_encoder({"error": reason, "detail": errors}), + content=jsonable_encoder({"error": _describe_validation_errors(errors), "detail": errors}), )