From 05d1effc233cb04db7115f94a72115d1f0b2bc1a Mon Sep 17 00:00:00 2001 From: Charisn Date: Sun, 15 Feb 2026 13:03:09 +0200 Subject: [PATCH 1/2] Fix UnboundLocalError in OpenAPI schema generation with custom response classes When a route uses a custom response class whose __init__ does not expose a status_code parameter with an int default, and the route decorator does not specify an explicit status_code, the status_code local variable in get_openapi_path() was never assigned. This caused an UnboundLocalError at the point where status_code is used to populate the operation responses dict. The bug does not surface with standard Starlette response classes (JSONResponse, HTMLResponse, PlainTextResponse, StreamingResponse, etc.) because they all declare status_code: int = 200 (or 307 for redirects) in their __init__ signatures. It triggers when users define custom response classes that wrap or omit the status_code parameter, e.g.: class CustomResponse(Response): def __init__(self, content, **kwargs): super().__init__(content=content, status_code=200, **kwargs) @app.get("/items", response_class=CustomResponse) async def get_items(): ... Accessing /openapi.json or /docs would crash with: UnboundLocalError: cannot access local variable 'status_code' Fix: initialize status_code = "200" before the conditional block so it always has a safe default. The existing branches that extract status_code from the route or from the response class signature still override this default when applicable. --- fastapi/openapi/utils.py | 1 + ...pi_custom_response_class_no_status_code.py | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/test_openapi_custom_response_class_no_status_code.py diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py index 812003aee..910eca72f 100644 --- a/fastapi/openapi/utils.py +++ b/fastapi/openapi/utils.py @@ -339,6 +339,7 @@ def get_openapi_path( ) callbacks[callback.name] = {callback.path: cb_path} operation["callbacks"] = callbacks + status_code = "200" if route.status_code is not None: status_code = str(route.status_code) else: diff --git a/tests/test_openapi_custom_response_class_no_status_code.py b/tests/test_openapi_custom_response_class_no_status_code.py new file mode 100644 index 000000000..18294f181 --- /dev/null +++ b/tests/test_openapi_custom_response_class_no_status_code.py @@ -0,0 +1,61 @@ +from starlette.responses import Response + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from inline_snapshot import snapshot + + +class CustomResponse(Response): + """A custom response class whose __init__ does not expose status_code + as a parameter with an int default. This previously caused an + UnboundLocalError during OpenAPI schema generation when the route + did not specify an explicit status_code.""" + + media_type = "text/plain" + + def __init__(self, content: str, **kwargs: object) -> None: + super().__init__(content=content, status_code=200, **kwargs) + + +app = FastAPI() + + +@app.get("/items", response_class=CustomResponse) +async def get_items(): + return "ok" + + +client = TestClient(app) + + +def test_get_response(): + response = client.get("/items") + assert response.status_code == 200 + assert response.text == "ok" + + +def test_openapi_schema(): + """OpenAPI generation must not crash when the response class's __init__ + lacks a status_code parameter with an int default.""" + response = client.get("/openapi.json") + assert response.status_code == 200, response.text + assert response.json() == snapshot( + { + "openapi": "3.1.0", + "info": {"title": "FastAPI", "version": "0.1.0"}, + "paths": { + "/items": { + "get": { + "summary": "Get Items", + "operationId": "get_items_items_get", + "responses": { + "200": { + "description": "Successful Response", + "content": {"text/plain": {"schema": {"type": "string"}}}, + } + }, + } + } + }, + } + ) From 68a08572c1f814d5dea07a677f519ae6cafed8b6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 11:12:42 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=8E=A8=20Auto=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_openapi_custom_response_class_no_status_code.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_openapi_custom_response_class_no_status_code.py b/tests/test_openapi_custom_response_class_no_status_code.py index 18294f181..7ec1d7115 100644 --- a/tests/test_openapi_custom_response_class_no_status_code.py +++ b/tests/test_openapi_custom_response_class_no_status_code.py @@ -1,8 +1,7 @@ -from starlette.responses import Response - from fastapi import FastAPI from fastapi.testclient import TestClient from inline_snapshot import snapshot +from starlette.responses import Response class CustomResponse(Response): @@ -51,7 +50,9 @@ def test_openapi_schema(): "responses": { "200": { "description": "Successful Response", - "content": {"text/plain": {"schema": {"type": "string"}}}, + "content": { + "text/plain": {"schema": {"type": "string"}} + }, } }, }