fix(pipeline): contain unexpected adapter serialization and formatter faults

Closed
#542 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

A pull request for this has already been merged.

  • #556 by @yeongseon — merged

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
68/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
azure, python

Research direction

Start by reading _build_response() in src/azure_functions_validation/pipeline.py and format_error_response() in src/azure_functions_validation/errors.py, then review the adapter guarantees in docs/adapters.md. Run the existing test suite and add coverage for the sync and async serializer failures and the adapter formatter failure. Done means adapter exceptions are logged and produce the sanitized 500 behavior in the acceptance checklist, without changing cancellation behavior.

Written by the indexing model from the issue text.

Description

bug needs-triage priority:medium

Context

The public adapter contract says unexpected adapter exceptions are logged with traceback and become sanitized 500 responses. This is implemented for parsing and response validation, but is missing from direct response serialization and the default 4xx adapter formatter. An adapter defect escapes both sync and async decorated handlers, and serializer failures skip the configured handler-level error formatter. Priority is medium because the defect requires a custom adapter failure, but violates a documented public fault boundary and breaks applications' error-response contract.

Reviewed commit: b38d702adb1039ed1eda2ef3799d873af2506f41 (azure-functions-validation 0.16.0). Locally reproduced on Python 3.12.14, Pydantic 2.14.1, and azure-functions 1.25.0, with the reviewed checkout's src on PYTHONPATH.

Run this complete snippet with those dependencies and the reviewed package installed:

import asyncio
import azure.functions as func
from pydantic import BaseModel
from azure_functions_validation import PydanticAdapter, validate_http

calls = []

def formatter(exc, status):
    calls.append((type(exc).__name__, status))
    return {"error": str(exc)}

class BrokenSerializer(PydanticAdapter):
    def serialize(self, obj):
        raise RuntimeError("synthetic serializer failure")

@validate_http(adapter=BrokenSerializer(), error_formatter=formatter)
def sync_handler(req: func.HttpRequest):
    return {"ok": True}

@validate_http(adapter=BrokenSerializer(), error_formatter=formatter)
async def async_handler(req: func.HttpRequest):
    return {"ok": True}

class Body(BaseModel):
    value: int

class BrokenFormatter(PydanticAdapter):
    def format_error(self, exc):
        raise RuntimeError("synthetic adapter formatter failure")

@validate_http(body=Body, adapter=BrokenFormatter())
def invalid_body(req: func.HttpRequest, body: Body):
    return body.model_dump()

req = func.HttpRequest(
    method="POST", url="http://localhost/api/probe", headers={}, params={}, body=b"{}",
)
for name, call in [
    ("sync serializer", lambda: sync_handler(req)),
    ("async serializer", lambda: asyncio.run(async_handler(req))),
    ("adapter formatter", lambda: invalid_body(req)),
]:
    try:
        response = call()
        print(name, response.status_code, response.get_body().decode())
    except Exception as exc:
        print(name, "escaped", type(exc).__name__, str(exc))
print("handler formatter calls:", calls)

Observed: all three calls raise RuntimeError out of the decorator. handler formatter calls is []. Substituting ValueError in BrokenSerializer.serialize() also escapes.

Expected: serializer faults are logged and produce 500, with the handler formatter receiving InternalServerError unless internal-error exposure was explicitly enabled. An exception from adapter.format_error() should be logged and use the built-in sanitized 500 fallback without invoking the broken adapter again. This report does not claim that the Azure host exposes exception text to HTTP clients; the confirmed failure is escaping the library's documented response boundary.

Root cause / fix direction: _build_response() catches only SerializationError and TypeError around adapter.serialize(). format_error_response() calls adapter.format_error() outside an exception guard, although it guards the handler-level formatter. Contain ordinary Exception faults at both adapter boundaries, route serializer failures through the existing 5xx formatter logic, and use an independent sanitized fallback for adapter-formatter faults. Keep cancellation and other BaseException subclasses outside this catch.

Acceptance Checklist

  • RuntimeError and ValueError from direct serialization return sanitized 500 in sync and async pipelines.
  • A handler error formatter is invoked once with the correct status and sanitized exception; explicit internal-error exposure retains its existing semantics.
  • Adapter format_error() failures return the built-in sanitized 500 and cannot recurse into the failing adapter.
  • Each fault is logged with traceback and no adapter failure is accidentally classified as a client validation error.
  • Existing serialization, custom formatter, and cancellation behavior stays covered.

Out of scope

  • Changing the public adapter protocol or implementing another validation backend.
  • The separate incompatibility between custom response model backends and the cached Pydantic response serializer.
  • Catching arbitrary exceptions from application handler code.

References

Dominant language
Python
Stars
13
Forks
4
Avg merge
14h 46m
Merged PRs (30d)
89

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.