1import contextlib2import copy3import email.message4import errno5import functools6import inspect7import json8import math9import os10import stat11import threading12import types13import warnings14from collections.abc import (15 AsyncIterator,16 Awaitable,17 Callable,18 Collection,19 Coroutine,20 Generator,21 Iterator,22 Mapping,23 Sequence,24)25from contextlib import (26 AbstractAsyncContextManager,27 AbstractContextManager,28 AsyncExitStack,29 asynccontextmanager,30)31from contextvars import ContextVar32from dataclasses import dataclass, field33from enum import Enum, IntEnum34from typing import (35 Annotated,36 Any,37 Literal,38 Protocol,39 TypeVar,40 cast,41)4243import anyio44from annotated_doc import Doc45from anyio.abc import ObjectReceiveStream46from fastapi import params47from fastapi._compat import (48 ModelField,49 Undefined,50 lenient_issubclass,51)52from fastapi.datastructures import Default, DefaultPlaceholder53from fastapi.dependencies.models import (54 Dependant,55 _is_async_gen_callable,56 _is_coroutine_callable,57 _is_gen_callable,58)59from fastapi.dependencies.utils import (60 SolvedDependency,61 _get_body_field,62 _get_flat_body_params,63 _should_embed_body_fields,64 get_dependant,65 get_parameterless_sub_dependant,66 get_stream_item_type,67 get_typed_return_annotation,68 solve_dependencies,69)70from fastapi.encoders import jsonable_encoder71from fastapi.exceptions import (72 EndpointContext,73 FastAPIError,74 RequestValidationError,75 ResponseValidationError,76 WebSocketRequestValidationError,77)78from fastapi.sse import (79 _PING_INTERVAL,80 KEEPALIVE_COMMENT,81 EventSourceResponse,82 ServerSentEvent,83 format_sse_event,84)85from fastapi.telemetry._api import (86 _operation,87 _route_selected,88 _run_sync_endpoint,89 _validation_failed,90 get_telemetry_data,91)92from fastapi.types import DecoratedCallable, IncEx93from fastapi.utils import (94 create_model_field,95 generate_unique_id,96 get_value_or_default,97 is_body_allowed_for_status_code,98)99from starlette import routing100from starlette._exception_handler import wrap_app_handling_exceptions101from starlette._utils import get_route_path, is_async_callable102from starlette.concurrency import iterate_in_threadpool, run_in_threadpool103from starlette.datastructures import URL, FormData, URLPath104from starlette.exceptions import HTTPException105from starlette.requests import Request106from starlette.responses import (107 JSONResponse,108 PlainTextResponse,109 RedirectResponse,110 Response,111 StreamingResponse,112)113from starlette.routing import (114 BaseRoute,115 Match,116 NoMatchFound,117 compile_path,118 get_name,119)120from starlette.routing import Mount as Mount # noqa121from starlette.staticfiles import StaticFiles122from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send123from starlette.websockets import WebSocket124from typing_extensions import deprecated125126127# Copy of starlette.routing.request_response modified to include the128# dependencies' AsyncExitStack129def request_response(130 func: Callable[[Request], Awaitable[Response] | Response],131) -> ASGIApp:132 """133 Takes a function or coroutine `func(request) -> response`,134 and returns an ASGI application.135 """136 f: Callable[[Request], Awaitable[Response]] = (137 func # type: ignore[assignment]138 if is_async_callable(func)139 else functools.partial(run_in_threadpool, func) # type: ignore[call-arg]140 ) # ty: ignore[invalid-assignment]141142 async def app(scope: Scope, receive: Receive, send: Send) -> None:143 request = Request(scope, receive, send)144145 async def app(scope: Scope, receive: Receive, send: Send) -> None:146 # Starts customization147 response_awaited = False148 async with AsyncExitStack() as request_stack:149 scope["fastapi_inner_astack"] = request_stack150 async with AsyncExitStack() as function_stack:151 scope["fastapi_function_astack"] = function_stack152 response = await f(request)153 await response(scope, receive, send)154 # Continues customization155 response_awaited = True156 if not response_awaited:157 raise FastAPIError(158 "Response not awaited. There's a high chance that the "159 "application code is raising an exception and a dependency with yield "160 "has a block with a bare except, or a block with except Exception, "161 "and is not raising the exception again. Read more about it in the "162 "docs: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/#dependencies-with-yield-and-except"163 )164165 # Same as in Starlette166 await wrap_app_handling_exceptions(app, request)(scope, receive, send)167168 return app169170171# Copy of starlette.routing.websocket_session modified to include the172# dependencies' AsyncExitStack173def websocket_session(174 func: Callable[[WebSocket], Awaitable[None]],175) -> ASGIApp:176 """177 Takes a coroutine `func(session)`, and returns an ASGI application.178 """179 # assert asyncio.iscoroutinefunction(func), "WebSocket endpoints must be async"180181 async def app(scope: Scope, receive: Receive, send: Send) -> None:182 session = WebSocket(scope, receive=receive, send=send)183184 async def app(scope: Scope, receive: Receive, send: Send) -> None:185 async with AsyncExitStack() as request_stack:186 scope["fastapi_inner_astack"] = request_stack187 async with AsyncExitStack() as function_stack:188 scope["fastapi_function_astack"] = function_stack189 await func(session)190191 # Same as in Starlette192 await wrap_app_handling_exceptions(app, session)(scope, receive, send)193194 return app195196197_T = TypeVar("_T")198199200# Vendored from starlette.routing to avoid importing private symbols201class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]):202 """203 Wraps a synchronous context manager to make it async.204205 This is vendored from Starlette to avoid importing private symbols.206 """207208 def __init__(self, cm: AbstractContextManager[_T]) -> None:209 self._cm = cm210211 async def __aenter__(self) -> _T:212 return self._cm.__enter__()213214 async def __aexit__(215 self,216 exc_type: type[BaseException] | None,217 exc_value: BaseException | None,218 traceback: types.TracebackType | None,219 ) -> bool | None:220 return self._cm.__exit__(exc_type, exc_value, traceback)221222223# Vendored from starlette.routing to avoid importing private symbols224def _wrap_gen_lifespan_context(225 lifespan_context: Callable[[Any], Generator[Any, Any, Any]],226) -> Callable[[Any], AbstractAsyncContextManager[Any]]:227 """228 Wrap a generator-based lifespan context into an async context manager.229230 This is vendored from Starlette to avoid importing private symbols.231 """232 cmgr = contextlib.contextmanager(lifespan_context)233234 @functools.wraps(cmgr)235 def wrapper(app: Any) -> _AsyncLiftContextManager[Any]:236 return _AsyncLiftContextManager(cmgr(app))237238 return wrapper239240241def _merge_lifespan_context(242 original_context: Lifespan[Any], nested_context: Lifespan[Any]243) -> Lifespan[Any]:244 @asynccontextmanager245 async def merged_lifespan(246 app: AppType,247 ) -> AsyncIterator[Mapping[str, Any] | None]:248 async with original_context(app) as maybe_original_state:249 async with nested_context(app) as maybe_nested_state:250 if maybe_nested_state is None and maybe_original_state is None:251 yield None # old ASGI compatibility252 else:253 yield {**(maybe_nested_state or {}), **(maybe_original_state or {})}254255 return merged_lifespan # type: ignore[return-value] # ty: ignore[invalid-return-type]256257258class _DefaultLifespan:259 """260 Default lifespan context manager that runs on_startup and on_shutdown handlers.261262 This is a copy of the Starlette _DefaultLifespan class that was removed263 in Starlette. FastAPI keeps it to maintain backward compatibility with264 on_startup and on_shutdown event handlers.265266 Ref: https://github.com/Kludex/starlette/pull/3117267 """268269 def __init__(self, router: "APIRouter") -> None:270 self._router = router271272 async def __aenter__(self) -> None:273 await self._router._startup()274275 async def __aexit__(self, *exc_info: object) -> None:276 await self._router._shutdown()277278 def __call__(self: _T, app: object) -> _T:279 return self280281282# Cache for endpoint context to avoid re-extracting on every request283_endpoint_context_cache: dict[int, EndpointContext] = {}284285286def _extract_endpoint_context(func: Any) -> EndpointContext:287 """Extract endpoint context with caching to avoid repeated file I/O."""288 func_id = id(func)289290 if func_id in _endpoint_context_cache:291 return _endpoint_context_cache[func_id]292293 try:294 ctx: EndpointContext = {}295296 if (source_file := inspect.getsourcefile(func)) is not None:297 ctx["file"] = source_file298 if (line_number := inspect.getsourcelines(func)[1]) is not None:299 ctx["line"] = line_number300 if (func_name := getattr(func, "__name__", None)) is not None:301 ctx["function"] = func_name302 except Exception:303 ctx = EndpointContext()304305 _endpoint_context_cache[func_id] = ctx306 return ctx307308309def _normalize_non_finite_floats(data: Any) -> Any:310 """Replace non-finite values in JSON-compatible data, preserving dict keys."""311 if isinstance(data, float):312 return data if math.isfinite(data) else None313 if isinstance(data, dict):314 return {key: _normalize_non_finite_floats(value) for key, value in data.items()}315 if isinstance(data, list):316 return [_normalize_non_finite_floats(value) for value in data]317 return data318319320async def serialize_response(321 *,322 field: ModelField | None = None,323 response_content: Any,324 include: IncEx | None = None,325 exclude: IncEx | None = None,326 by_alias: bool = True,327 exclude_unset: bool = False,328 exclude_defaults: bool = False,329 exclude_none: bool = False,330 is_coroutine: bool = True,331 endpoint_ctx: EndpointContext | None = None,332 dump_json: bool = False,333) -> Any:334 if field:335 if is_coroutine:336 value, errors = field.validate(response_content, {}, loc=("response",))337 else:338 value, errors = await run_in_threadpool(339 field.validate, response_content, {}, loc=("response",)340 )341 if errors:342 ctx = endpoint_ctx or EndpointContext()343 raise ResponseValidationError(344 errors=errors,345 body=response_content,346 endpoint_ctx=ctx,347 )348 serializer = field.serialize_json if dump_json else field.serialize349 return serializer(350 value,351 include=include,352 exclude=exclude,353 by_alias=by_alias,354 exclude_unset=exclude_unset,355 exclude_defaults=exclude_defaults,356 exclude_none=exclude_none,357 )358359 else:360 return jsonable_encoder(response_content)361362363async def run_endpoint_function(364 *, dependant: Dependant, values: dict[str, Any], is_coroutine: bool365) -> Any:366 # Only called by get_request_handler. Has been split into its own function to367 # facilitate profiling endpoints, since inner functions are harder to profile.368 assert dependant.call is not None, "dependant.call must be a function"369370 if is_coroutine:371 with _operation(name="endpoint", function=dependant.call):372 return await dependant.call(**values)373 else:374 return await run_in_threadpool(375 _run_sync_endpoint, function=dependant.call, arguments=values376 )377378379def _build_response_args(380 *, status_code: int | None, solved_result: Any381) -> dict[str, Any]:382 response_args: dict[str, Any] = {383 "background": solved_result.background_tasks,384 }385 # If status_code was set, use it, otherwise use the default from the386 # response class, in the case of redirect it's 307387 current_status_code = (388 status_code if status_code else solved_result.response.status_code389 )390 if current_status_code is not None:391 response_args["status_code"] = current_status_code392 if solved_result.response.status_code:393 response_args["status_code"] = solved_result.response.status_code394 return response_args395396397def get_request_handler(398 dependant: Dependant,399 body_field: ModelField | None = None,400 status_code: int | None = None,401 response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),402 response_field: ModelField | None = None,403 response_model_include: IncEx | None = None,404 response_model_exclude: IncEx | None = None,405 response_model_by_alias: bool = True,406 response_model_exclude_unset: bool = False,407 response_model_exclude_defaults: bool = False,408 response_model_exclude_none: bool = False,409 dependency_overrides_provider: Any | None = None,410 embed_body_fields: bool = False,411 strict_content_type: bool | DefaultPlaceholder = Default(True),412 stream_item_field: ModelField | None = None,413 is_json_stream: bool = False,414) -> Callable[[Request], Coroutine[Any, Any, Response]]:415 assert dependant.call is not None, "dependant.call must be a function"416 is_coroutine = _is_coroutine_callable(dependant.call)417 is_body_form = body_field and isinstance(body_field.field_info, params.Form)418 if isinstance(response_class, DefaultPlaceholder):419 actual_response_class: type[Response] = response_class.value420 else:421 actual_response_class = response_class422 is_sse_stream = lenient_issubclass(actual_response_class, EventSourceResponse)423 if isinstance(strict_content_type, DefaultPlaceholder):424 actual_strict_content_type: bool = strict_content_type.value425 else:426 actual_strict_content_type = strict_content_type427428 async def app(request: Request) -> Response:429 telemetry_data = get_telemetry_data()430 if telemetry_data is not None:431 telemetry_data.request = request432 response: Response | None = None433 file_stack = request.scope.get("fastapi_middleware_astack")434 assert isinstance(file_stack, AsyncExitStack), (435 "fastapi_middleware_astack not found in request scope"436 )437438 # Extract endpoint context for error messages439 endpoint_ctx = (440 _extract_endpoint_context(dependant.call)441 if dependant.call442 else EndpointContext()443 )444445 if dependant.path:446 # For mounted sub-apps, include the mount path prefix447 mount_path = request.scope.get("root_path", "").rstrip("/")448 endpoint_ctx["path"] = f"{request.method} {mount_path}{dependant.path}"449450 # Read body and auto-close files451 try:452 body: Any = None453 if body_field:454 if is_body_form:455 body = await request.form()456 file_stack.push_async_callback(body.close)457 else:458 body_bytes = await request.body()459 if body_bytes:460 json_body: Any = Undefined461 content_type_value = request.headers.get("content-type")462 if not content_type_value:463 if not actual_strict_content_type:464 json_body = await request.json()465 else:466 message = email.message.Message()467 message["content-type"] = content_type_value468 if message.get_content_maintype() == "application":469 subtype = message.get_content_subtype()470 if subtype == "json" or subtype.endswith("+json"):471 json_body = await request.json()472 if json_body != Undefined:473 body = json_body474 else:475 body = body_bytes476 except json.JSONDecodeError as e:477 validation_error = RequestValidationError(478 [479 {480 "type": "json_invalid",481 "loc": ("body", e.pos),482 "msg": "JSON decode error",483 "input": {},484 "ctx": {"error": e.msg},485 }486 ],487 body=e.doc,488 endpoint_ctx=endpoint_ctx,489 )490 _validation_failed(validation_error)491 raise validation_error from e492 except HTTPException:493 # If a middleware raises an HTTPException, it should be raised again494 raise495 except Exception as e:496 http_error = HTTPException(497 status_code=400, detail="There was an error parsing the body"498 )499 raise http_error from e500501 if telemetry_data is not None:502 telemetry_data.body = body503504 # Solve dependencies and run path operation function, auto-closing dependencies505 errors: list[Any] = []506 async_exit_stack = request.scope.get("fastapi_inner_astack")507 assert isinstance(async_exit_stack, AsyncExitStack), (508 "fastapi_inner_astack not found in request scope"509 )510 with _operation(name="dependencies", function=dependant.call):511 solved_result = await solve_dependencies(512 request=request,513 dependant=dependant,514 body=cast(dict[str, Any] | FormData | bytes | None, body),515 dependency_overrides_provider=dependency_overrides_provider,516 async_exit_stack=async_exit_stack,517 embed_body_fields=embed_body_fields,518 )519 if telemetry_data is not None:520 telemetry_data.values = solved_result.values521 telemetry_data.errors = solved_result.errors522 errors = solved_result.errors523 assert dependant.call # For types524 if not errors:525 # Shared serializer for stream items (JSONL and SSE).526 # Validates against stream_item_field when set, then527 # serializes to JSON bytes.528 def _serialize_data(data: Any) -> bytes:529 if stream_item_field:530 value, errors_ = stream_item_field.validate(531 data, {}, loc=("response",)532 )533 if errors_:534 ctx = endpoint_ctx or EndpointContext()535 raise ResponseValidationError(536 errors=errors_,537 body=data,538 endpoint_ctx=ctx,539 )540 return stream_item_field.serialize_json(541 value,542 include=response_model_include,543 exclude=response_model_exclude,544 by_alias=response_model_by_alias,545 exclude_unset=response_model_exclude_unset,546 exclude_defaults=response_model_exclude_defaults,547 exclude_none=response_model_exclude_none,548 )549 else:550 data = jsonable_encoder(data)551 return json.dumps(_normalize_non_finite_floats(data)).encode(552 "utf-8"553 )554555 if is_sse_stream:556 # Generator endpoint: stream as Server-Sent Events557 gen = dependant.call(**solved_result.values)558559 def _serialize_sse_item(item: Any) -> bytes:560 if isinstance(item, ServerSentEvent):561 # User controls the event structure.562 # Serialize the data payload if present.563 # For ServerSentEvent items we skip stream_item_field564 # validation (the user may mix types intentionally).565 if item.raw_data is not None:566 data_str: str | None = item.raw_data567 elif item.data is not None:568 if hasattr(item.data, "model_dump_json"):569 data_str = item.data.model_dump_json()570 else:571 data_str = json.dumps(572 _normalize_non_finite_floats(573 jsonable_encoder(item.data)574 )575 )576 else:577 data_str = None578 return format_sse_event(579 data_str=data_str,580 event=item.event,581 id=item.id,582 retry=item.retry,583 comment=item.comment,584 )585 else:586 # Plain object: validate + serialize via587 # stream_item_field (if set) and wrap in data field588 return format_sse_event(589 data_str=_serialize_data(item).decode("utf-8")590 )591592 if _is_async_gen_callable(dependant.call):593 sse_aiter: AsyncIterator[Any] = gen.__aiter__()594 else:595 sse_aiter = iterate_in_threadpool(gen)596597 @asynccontextmanager598 async def _sse_producer_cm() -> AsyncIterator[599 ObjectReceiveStream[bytes]600 ]:601 # Use a memory stream to decouple generator iteration602 # from the keepalive timer. A producer task pulls items603 # from the generator independently, so604 # `anyio.fail_after` never wraps the generator's605 # `__anext__` directly - avoiding CancelledError that606 # would finalize the generator and also working for sync607 # generators running in a thread pool.608 #609 # This context manager is entered on the request-scoped610 # AsyncExitStack so its __aexit__ (which cancels the611 # task group) is called by the exit stack after the612 # streaming response completes — not by async generator613 # finalization via GeneratorExit.614 # Ref: https://peps.python.org/pep-0789/615 send_stream, receive_stream = anyio.create_memory_object_stream[616 bytes617 ](max_buffer_size=1)618619 async def _producer() -> None:620 async with send_stream:621 async for raw_item in sse_aiter:622 await send_stream.send(_serialize_sse_item(raw_item))623624 send_keepalive, receive_keepalive = (625 anyio.create_memory_object_stream[bytes](max_buffer_size=1)626 )627628 async def _keepalive_inserter() -> None:629 """Read from the producer and forward to the output,630 inserting keepalive comments on timeout."""631 async with send_keepalive, receive_stream:632 try:633 while True:634 try:635 with anyio.fail_after(_PING_INTERVAL):636 data = await receive_stream.receive()637 await send_keepalive.send(data)638 except TimeoutError:639 await send_keepalive.send(KEEPALIVE_COMMENT)640 except anyio.EndOfStream:641 pass642643 async with anyio.create_task_group() as tg:644 tg.start_soon(_producer)645 tg.start_soon(_keepalive_inserter)646 yield receive_keepalive647 tg.cancel_scope.cancel()648649 # Enter the SSE context manager on the request-scoped650 # exit stack. The stack outlives the streaming response,651 # so __aexit__ runs via proper structured teardown, not652 # via GeneratorExit thrown into an async generator.653 sse_receive_stream = await async_exit_stack.enter_async_context(654 _sse_producer_cm()655 )656 # Ensure the receive stream is closed when the exit stack657 # unwinds, preventing ResourceWarning from __del__.658 async_exit_stack.push_async_callback(sse_receive_stream.aclose)659660 async def _sse_with_checkpoints(661 stream: ObjectReceiveStream[bytes],662 ) -> AsyncIterator[bytes]:663 async for data in stream:664 yield data665 # Guarantee a checkpoint so cancellation can be666 # delivered even when the producer is faster than667 # the consumer and receive() never suspends.668 await anyio.sleep(0)669670 sse_stream_content: AsyncIterator[bytes] | Iterator[bytes] = (671 _sse_with_checkpoints(sse_receive_stream)672 )673674 response_args = _build_response_args(675 status_code=status_code, solved_result=solved_result676 )677 response = StreamingResponse(678 sse_stream_content,679 media_type="text/event-stream",680 **response_args,681 )682 response.headers["Cache-Control"] = "no-cache"683 # For Nginx proxies to not buffer server sent events684 response.headers["X-Accel-Buffering"] = "no"685 response.headers.raw.extend(solved_result.response.headers.raw)686 elif is_json_stream:687 # Generator endpoint: stream as JSONL688 gen = dependant.call(**solved_result.values)689690 def _serialize_item(item: Any) -> bytes:691 return _serialize_data(item) + b"\n"692693 if _is_async_gen_callable(dependant.call):694695 async def _async_stream_jsonl() -> AsyncIterator[bytes]:696 async for item in gen:697 yield _serialize_item(item)698 # To allow for cancellation to trigger699 # Ref: https://github.com/fastapi/fastapi/issues/14680700 await anyio.sleep(0)701702 jsonl_stream_content: AsyncIterator[bytes] | Iterator[bytes] = (703 _async_stream_jsonl()704 )705 else:706707 def _sync_stream_jsonl() -> Iterator[bytes]:708 for item in gen: # ty: ignore[not-iterable]709 yield _serialize_item(item)710711 jsonl_stream_content = _sync_stream_jsonl()712713 response_args = _build_response_args(714 status_code=status_code, solved_result=solved_result715 )716 response = StreamingResponse(717 jsonl_stream_content,718 media_type="application/jsonl",719 **response_args,720 )721 response.headers.raw.extend(solved_result.response.headers.raw)722 elif _is_async_gen_callable(dependant.call) or _is_gen_callable(723 dependant.call724 ):725 # Raw streaming with explicit response_class (e.g. StreamingResponse)726 gen = dependant.call(**solved_result.values)727 if _is_async_gen_callable(dependant.call):728729 async def _async_stream_raw(730 async_gen: AsyncIterator[Any],731 ) -> AsyncIterator[Any]:732 async for chunk in async_gen:733 yield chunk734 # To allow for cancellation to trigger735 # Ref: https://github.com/fastapi/fastapi/issues/14680736 await anyio.sleep(0)737738 gen = _async_stream_raw(gen)739 response_args = _build_response_args(740 status_code=status_code, solved_result=solved_result741 )742 response = actual_response_class(content=gen, **response_args)743 response.headers.raw.extend(solved_result.response.headers.raw)744 else:745 raw_response = await run_endpoint_function(746 dependant=dependant,747 values=solved_result.values,748 is_coroutine=is_coroutine,749 )750 if isinstance(raw_response, Response):751 if raw_response.background is None:752 raw_response.background = solved_result.background_tasks753 response = raw_response754 else:755 response_args = _build_response_args(756 status_code=status_code, solved_result=solved_result757 )758 # Use the fast path (dump_json) when no custom response759 # class was set and a response field with a TypeAdapter760 # exists. Serializes directly to JSON bytes via Pydantic's761 # Rust core, skipping the intermediate Python dict +762 # json.dumps() step.763 use_dump_json = response_field is not None and isinstance(764 response_class, DefaultPlaceholder765 )766 with _operation(name="serialization", function=dependant.call):767 content = await serialize_response(768 field=response_field,769 response_content=raw_response,770 include=response_model_include,771 exclude=response_model_exclude,772 by_alias=response_model_by_alias,773 exclude_unset=response_model_exclude_unset,774 exclude_defaults=response_model_exclude_defaults,775 exclude_none=response_model_exclude_none,776 is_coroutine=is_coroutine,777 endpoint_ctx=endpoint_ctx,778 dump_json=use_dump_json,779 )780 if use_dump_json:781 response = Response(782 content=content,783 media_type="application/json",784 **response_args,785 )786 else:787 response = actual_response_class(content, **response_args)788 if not is_body_allowed_for_status_code(response.status_code):789 response.body = b""790 response.headers.raw.extend(solved_result.response.headers.raw)791 if errors:792 validation_error = RequestValidationError(793 errors, body=body, endpoint_ctx=endpoint_ctx794 )795 _validation_failed(validation_error)796 raise validation_error797798 # Return response799 assert response800 return response801802 return app803804805def get_websocket_app(806 dependant: Dependant,807 dependency_overrides_provider: Any | None = None,808 embed_body_fields: bool = False,809) -> Callable[[WebSocket], Coroutine[Any, Any, Any]]:810 async def app(websocket: WebSocket) -> None:811 telemetry_data = get_telemetry_data()812 if telemetry_data is not None:813 telemetry_data.websocket = websocket814 endpoint_ctx = (815 _extract_endpoint_context(dependant.call)816 if dependant.call817 else EndpointContext()818 )819 if dependant.path:820 # For mounted sub-apps, include the mount path prefix821 mount_path = websocket.scope.get("root_path", "").rstrip("/")822 endpoint_ctx["path"] = f"WS {mount_path}{dependant.path}"823 async_exit_stack = websocket.scope.get("fastapi_inner_astack")824 assert isinstance(async_exit_stack, AsyncExitStack), (825 "fastapi_inner_astack not found in request scope"826 )827 with _operation(name="dependencies", function=dependant.call):828 solved_result = await solve_dependencies(829 request=websocket,830 dependant=dependant,831 dependency_overrides_provider=dependency_overrides_provider,832 async_exit_stack=async_exit_stack,833 embed_body_fields=embed_body_fields,834 )835 if telemetry_data is not None:836 telemetry_data.values = solved_result.values837 telemetry_data.errors = solved_result.errors838 if solved_result.errors:839 validation_error = WebSocketRequestValidationError(840 solved_result.errors,841 endpoint_ctx=endpoint_ctx,842 )843 _validation_failed(validation_error)844 raise validation_error845 assert dependant.call is not None, "dependant.call must be a function"846 with _operation(name="endpoint", function=dependant.call):847 await dependant.call(**solved_result.values)848849 return app850851852class APIWebSocketRoute(routing.WebSocketRoute):853 def __init__(854 self,855 path: str,856 endpoint: Callable[..., Any],857 *,858 name: str | None = None,859 dependencies: Sequence[params.Depends] | None = None,860 dependency_overrides_provider: Any | None = None,861 ) -> None:862 self.path = path863 self.endpoint = endpoint864 self.name = get_name(endpoint) if name is None else name865 self.dependencies = list(dependencies or [])866 self.path_regex, self.path_format, self.param_convertors = compile_path(path)867 (868 self.dependant,869 _,870 self._embed_body_fields,871 ) = _build_dependant_with_parameterless_dependencies(872 path=self.path_format,873 call=self.endpoint,874 dependencies=self.dependencies,875 )876 self.app = websocket_session(877 get_websocket_app(878 dependant=self.dependant,879 dependency_overrides_provider=dependency_overrides_provider,880 embed_body_fields=self._embed_body_fields,881 )882 )883884 def matches(self, scope: Scope) -> tuple[Match, Scope]:885 match, child_scope = super().matches(scope)886 if match != Match.NONE:887 child_scope["route"] = self888 return match, child_scope889890891_FASTAPI_SCOPE_KEY = "fastapi"892_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY = "effective_route_context"893_FASTAPI_FRONTEND_PATH_KEY = "frontend_path"894_FASTAPI_FRONTEND_SPECIFICITY_KEY = "frontend_specificity"895_FASTAPI_INCLUDED_ROUTER_KEY = "included_router"896_effective_route_context_var: ContextVar[Any | None] = ContextVar(897 "fastapi_effective_route_context", default=None898)899_SCOPE_MISSING = object()900901902def _frontend_dependency_endpoint() -> None:903 pass # pragma: no cover904905906def _build_dependant_with_parameterless_dependencies(907 *,908 path: str,909 call: Callable[..., Any],910 dependencies: Sequence[params.Depends],911) -> tuple[Dependant, list[ModelField], bool]:912 dependant = get_dependant(path=path, call=call, scope="function")913 for depends in dependencies[::-1]:914 dependant.dependencies.insert(915 0,916 get_parameterless_sub_dependant(depends=depends, path=path),917 )918 body_params = _get_flat_body_params(dependant)919 embed_body_fields = _should_embed_body_fields(body_params)920 return dependant, body_params, embed_body_fields921922923class _RouteWithPath(Protocol):924 path: str925926927def _get_fastapi_scope(scope: Scope) -> dict[str, Any]:928 fastapi_scope = scope.setdefault(_FASTAPI_SCOPE_KEY, {})929 assert isinstance(fastapi_scope, dict)930 return fastapi_scope931932933def _update_scope(scope: Scope, child_scope: Scope) -> None:934 fastapi_child_scope = child_scope.get(_FASTAPI_SCOPE_KEY)935 for key, value in child_scope.items():936 if key != _FASTAPI_SCOPE_KEY:937 scope[key] = value938 if isinstance(fastapi_child_scope, dict):939 _get_fastapi_scope(scope).update(fastapi_child_scope)940941942def _get_scope_effective_route_context(scope: Scope) -> Any | None:943 return scope.get(_FASTAPI_SCOPE_KEY, {}).get(_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY)944945946def _get_scope_included_router(scope: Scope) -> Any | None:947 return scope.get(_FASTAPI_SCOPE_KEY, {}).get(_FASTAPI_INCLUDED_ROUTER_KEY)948949950def _frontend_scope_specificity(scope: Scope) -> int | None:951 specificity = scope.get(_FASTAPI_SCOPE_KEY, {}).get(952 _FASTAPI_FRONTEND_SPECIFICITY_KEY953 )954 if isinstance(specificity, int):955 return specificity956 return None957958959def _restore_fastapi_scope_key(scope: Scope, key: str, previous: Any) -> None:960 fastapi_scope = scope.get(_FASTAPI_SCOPE_KEY)961 if not isinstance(fastapi_scope, dict):962 return963 if previous is _SCOPE_MISSING:964 fastapi_scope.pop(key, None)965 else:966 fastapi_scope[key] = previous967968969class _APIRouteLike(Protocol):970 path: str971 endpoint: Callable[..., Any]972 stream_item_type: Any | None973 response_model: Any974 summary: str | None975 response_description: str976 deprecated: bool | None977 operation_id: str | None978 response_model_include: IncEx | None979 response_model_exclude: IncEx | None980 response_model_by_alias: bool981 response_model_exclude_unset: bool982 response_model_exclude_defaults: bool983 response_model_exclude_none: bool984 include_in_schema: bool985 response_class: type[Response] | DefaultPlaceholder986 dependency_overrides_provider: Any | None987 callbacks: list[BaseRoute] | None988 openapi_extra: dict[str, Any] | None989 generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder990 strict_content_type: bool | DefaultPlaceholder991 tags: list[str | Enum]992 responses: dict[int | str, dict[str, Any]]993 name: str994 path_regex: Any995 path_format: str996 param_convertors: dict[str, Any]997 methods: set[str]998 unique_id: str999 status_code: int | None1000 response_field: ModelField | None1001 stream_item_field: ModelField | None1002 dependencies: list[params.Depends]1003 description: str1004 response_fields: dict[int | str, ModelField]1005 dependant: Dependant1006 _embed_body_fields: bool1007 body_field: ModelField | None1008 is_sse_stream: bool1009 is_json_stream: bool101010111012def _populate_api_route_state(1013 route: _APIRouteLike,1014 path: str,1015 endpoint: Callable[..., Any],1016 *,1017 response_model: Any = Default(None),1018 status_code: int | None = None,1019 tags: list[str | Enum] | None = None,1020 dependencies: Sequence[params.Depends] | None = None,1021 summary: str | None = None,1022 description: str | None = None,1023 response_description: str = "Successful Response",1024 responses: dict[int | str, dict[str, Any]] | None = None,1025 deprecated: bool | None = None,1026 name: str | None = None,1027 methods: set[str] | list[str] | None = None,1028 operation_id: str | None = None,1029 response_model_include: IncEx | None = None,1030 response_model_exclude: IncEx | None = None,1031 response_model_by_alias: bool = True,1032 response_model_exclude_unset: bool = False,1033 response_model_exclude_defaults: bool = False,1034 response_model_exclude_none: bool = False,1035 include_in_schema: bool = True,1036 response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),1037 dependency_overrides_provider: Any | None = None,1038 callbacks: list[BaseRoute] | None = None,1039 openapi_extra: dict[str, Any] | None = None,1040 generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder = Default(1041 generate_unique_id1042 ),1043 strict_content_type: bool | DefaultPlaceholder = Default(True),1044 stream_item_type: Any | None = None,1045) -> None:1046 route.path = path1047 route.endpoint = endpoint1048 route.stream_item_type = stream_item_type1049 route.summary = summary1050 route.response_description = response_description1051 route.deprecated = deprecated1052 route.operation_id = operation_id1053 route.response_model_include = response_model_include1054 route.response_model_exclude = response_model_exclude1055 route.response_model_by_alias = response_model_by_alias1056 route.response_model_exclude_unset = response_model_exclude_unset1057 route.response_model_exclude_defaults = response_model_exclude_defaults1058 route.response_model_exclude_none = response_model_exclude_none1059 route.include_in_schema = include_in_schema1060 route.response_class = response_class1061 route.dependency_overrides_provider = dependency_overrides_provider1062 route.callbacks = callbacks1063 route.openapi_extra = openapi_extra1064 route.generate_unique_id_function = generate_unique_id_function1065 route.strict_content_type = strict_content_type1066 route.tags = tags or []1067 route.responses = responses or {}1068 route.name = get_name(endpoint) if name is None else name1069 route.path_regex, route.path_format, route.param_convertors = compile_path(path)1070 if methods is None:1071 methods = ["GET"]1072 route.methods = {method.upper() for method in methods}1073 if isinstance(generate_unique_id_function, DefaultPlaceholder):1074 current_generate_unique_id: Callable[[Any], str] = (1075 generate_unique_id_function.value1076 )1077 else:1078 current_generate_unique_id = generate_unique_id_function1079 route.unique_id = route.operation_id or current_generate_unique_id(route)1080 # normalize enums e.g. http.HTTPStatus1081 if isinstance(status_code, IntEnum):1082 status_code = int(status_code)1083 route.status_code = status_code1084 route.dependencies = list(dependencies or [])1085 route.description = description or inspect.cleandoc(route.endpoint.__doc__ or "")1086 # if a "form feed" character (page break) is found in the description text,1087 # truncate description text to the content preceding the first "form feed"1088 route.description = route.description.split("\f")[0].strip()1089 response_fields = {}1090 for additional_status_code, response in route.responses.items():1091 assert isinstance(response, dict), "An additional response must be a dict"1092 model = response.get("model")1093 if model:1094 assert is_body_allowed_for_status_code(additional_status_code), (1095 f"Status code {additional_status_code} must not have a response body"1096 )1097 response_name = f"Response_{additional_status_code}_{route.unique_id}"1098 response_field = create_model_field(1099 name=response_name, type_=model, mode="serialization"1100 )1101 response_fields[additional_status_code] = response_field1102 if response_fields:1103 route.response_fields = response_fields1104 else:1105 route.response_fields = {}11061107 assert callable(endpoint), "An endpoint must be a callable"1108 (1109 route.dependant,1110 body_params,1111 route._embed_body_fields,1112 ) = _build_dependant_with_parameterless_dependencies(1113 path=route.path_format,1114 call=route.endpoint,1115 dependencies=route.dependencies,1116 )1117 route.body_field = _get_body_field(1118 body_params=body_params,1119 name=route.unique_id,1120 embed_body_fields=route._embed_body_fields,1121 )1122 # Detect generator endpoints that should stream as JSONL or SSE1123 is_generator = _is_async_gen_callable(route.dependant.call) or _is_gen_callable(1124 route.dependant.call1125 )1126 route.is_sse_stream = is_generator and lenient_issubclass(1127 response_class, EventSourceResponse1128 )1129 route.is_json_stream = is_generator and isinstance(1130 response_class, DefaultPlaceholder1131 )1132 if isinstance(response_model, DefaultPlaceholder):1133 return_annotation = get_typed_return_annotation(endpoint)1134 if lenient_issubclass(return_annotation, Response):1135 response_model = None1136 else:1137 stream_item = get_stream_item_type(return_annotation)1138 if stream_item is not None and is_generator:1139 # Extract item type for JSONL or SSE streaming for1140 # generator endpoints when response_class is1141 # DefaultPlaceholder (JSONL) or EventSourceResponse (SSE).1142 # ServerSentEvent is excluded: it's a transport1143 # wrapper, not a data model, so it shouldn't feed1144 # into validation or OpenAPI schema generation.1145 if (1146 isinstance(response_class, DefaultPlaceholder)1147 or lenient_issubclass(response_class, EventSourceResponse)1148 ) and not lenient_issubclass(stream_item, ServerSentEvent):1149 route.stream_item_type = stream_item1150 response_model = None1151 else:1152 response_model = return_annotation1153 route.response_model = response_model1154 if route.response_model:1155 assert is_body_allowed_for_status_code(status_code), (1156 f"Status code {status_code} must not have a response body"1157 )1158 response_name = "Response_" + route.unique_id1159 route.response_field = create_model_field(1160 name=response_name,1161 type_=route.response_model,1162 mode="serialization",1163 )1164 else:1165 route.response_field = None1166 if route.stream_item_type:1167 stream_item_name = "StreamItem_" + route.unique_id1168 route.stream_item_field = create_model_field(1169 name=stream_item_name,1170 type_=route.stream_item_type,1171 mode="serialization",1172 )1173 else:1174 route.stream_item_field = None117511761177class APIRoute(routing.Route):1178 stream_item_type: Any | None1179 response_model: Any1180 summary: str | None1181 response_description: str1182 deprecated: bool | None1183 operation_id: str | None1184 response_model_include: IncEx | None1185 response_model_exclude: IncEx | None1186 response_model_by_alias: bool1187 response_model_exclude_unset: bool1188 response_model_exclude_defaults: bool1189 response_model_exclude_none: bool1190 include_in_schema: bool1191 response_class: type[Response] | DefaultPlaceholder1192 dependency_overrides_provider: Any | None1193 callbacks: list[BaseRoute] | None1194 openapi_extra: dict[str, Any] | None1195 generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder1196 strict_content_type: bool | DefaultPlaceholder1197 tags: list[str | Enum]1198 responses: dict[int | str, dict[str, Any]]1199 unique_id: str1200 status_code: int | None1201 response_field: ModelField | None1202 stream_item_field: ModelField | None1203 dependencies: list[params.Depends]1204 description: str1205 response_fields: dict[int | str, ModelField]1206 dependant: Dependant1207 _embed_body_fields: bool1208 body_field: ModelField | None1209 is_sse_stream: bool1210 is_json_stream: bool12111212 def __init__(1213 self,1214 path: str,1215 endpoint: Callable[..., Any],1216 *,1217 response_model: Any = Default(None),1218 status_code: int | None = None,1219 tags: list[str | Enum] | None = None,1220 dependencies: Sequence[params.Depends] | None = None,1221 summary: str | None = None,1222 description: str | None = None,1223 response_description: str = "Successful Response",1224 responses: dict[int | str, dict[str, Any]] | None = None,1225 deprecated: bool | None = None,1226 name: str | None = None,1227 methods: set[str] | list[str] | None = None,1228 operation_id: str | None = None,1229 response_model_include: IncEx | None = None,1230 response_model_exclude: IncEx | None = None,1231 response_model_by_alias: bool = True,1232 response_model_exclude_unset: bool = False,1233 response_model_exclude_defaults: bool = False,1234 response_model_exclude_none: bool = False,1235 include_in_schema: bool = True,1236 response_class: type[Response] | DefaultPlaceholder = Default(JSONResponse),1237 dependency_overrides_provider: Any | None = None,1238 callbacks: list[BaseRoute] | None = None,1239 openapi_extra: dict[str, Any] | None = None,1240 generate_unique_id_function: Callable[["APIRoute"], str]1241 | DefaultPlaceholder = Default(generate_unique_id),1242 strict_content_type: bool | DefaultPlaceholder = Default(True),1243 ) -> None:1244 _populate_api_route_state(1245 cast(_APIRouteLike, self),1246 path,1247 endpoint,1248 response_model=response_model,1249 status_code=status_code,1250 tags=tags,1251 dependencies=dependencies,1252 summary=summary,1253 description=description,1254 response_description=response_description,1255 responses=responses,1256 deprecated=deprecated,1257 name=name,1258 methods=methods,1259 operation_id=operation_id,1260 response_model_include=response_model_include,1261 response_model_exclude=response_model_exclude,1262 response_model_by_alias=response_model_by_alias,1263 response_model_exclude_unset=response_model_exclude_unset,1264 response_model_exclude_defaults=response_model_exclude_defaults,1265 response_model_exclude_none=response_model_exclude_none,1266 include_in_schema=include_in_schema,1267 response_class=response_class,1268 dependency_overrides_provider=dependency_overrides_provider,1269 callbacks=callbacks,1270 openapi_extra=openapi_extra,1271 generate_unique_id_function=generate_unique_id_function,1272 strict_content_type=strict_content_type,1273 )1274 self.app = request_response(self.get_route_handler())12751276 def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:1277 route = cast(_APIRouteLike, self)1278 # TODO: Replace or deprecate this no-scope hook so included-route1279 # effective context can be passed explicitly instead of via ContextVar.1280 effective_context = _effective_route_context_var.get()1281 if effective_context is not None and effective_context.original_route is self:1282 route = cast(_APIRouteLike, effective_context)1283 return get_request_handler(1284 dependant=route.dependant,1285 body_field=route.body_field,1286 status_code=route.status_code,1287 response_class=route.response_class,1288 response_field=route.response_field,1289 response_model_include=route.response_model_include,1290 response_model_exclude=route.response_model_exclude,1291 response_model_by_alias=route.response_model_by_alias,1292 response_model_exclude_unset=route.response_model_exclude_unset,1293 response_model_exclude_defaults=route.response_model_exclude_defaults,1294 response_model_exclude_none=route.response_model_exclude_none,1295 dependency_overrides_provider=route.dependency_overrides_provider,1296 embed_body_fields=route._embed_body_fields,1297 strict_content_type=route.strict_content_type,1298 stream_item_field=route.stream_item_field,1299 is_json_stream=route.is_json_stream,1300 )13011302 def matches(self, scope: Scope) -> tuple[Match, Scope]:1303 effective_context = _get_scope_effective_route_context(scope)1304 if effective_context is not None and effective_context.original_route is self:1305 match, child_scope = effective_context.matches(scope)1306 else:1307 match, child_scope = super().matches(scope)1308 if match != Match.NONE:1309 child_scope["route"] = self1310 return match, child_scope13111312 async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:1313 effective_context = _get_scope_effective_route_context(scope)1314 if effective_context is not None and effective_context.original_route is self:1315 methods = effective_context.methods1316 if methods and scope["method"] not in methods:1317 headers = {"Allow": ", ".join(methods)}1318 if "app" in scope:1319 raise HTTPException(status_code=405, headers=headers)1320 response = PlainTextResponse(1321 "Method Not Allowed", status_code=405, headers=headers1322 )1323 await response(scope, receive, send)1324 return1325 assert effective_context.app is not None1326 await effective_context.app(scope, receive, send)1327 return1328 await super().handle(scope, receive, send)132913301331@dataclass1332class _RouterIncludeContext:1333 included_router: "APIRouter"1334 prefix: str = ""1335 tags: list[str | Enum] = field(default_factory=list)1336 dependencies: list[params.Depends] = field(default_factory=list)1337 default_response_class: type[Response] | DefaultPlaceholder = field(1338 default_factory=lambda: Default(JSONResponse)1339 )1340 responses: dict[int | str, dict[str, Any]] = field(default_factory=dict)1341 callbacks: list[BaseRoute] = field(default_factory=list)1342 deprecated: bool | None = None1343 include_in_schema: bool = True1344 generate_unique_id_function: Callable[[APIRoute], str] | DefaultPlaceholder = field(1345 default_factory=lambda: Default(generate_unique_id)1346 )1347 strict_content_type: bool | DefaultPlaceholder = field(1348 default_factory=lambda: Default(True)1349 )1350 dependency_overrides_provider: Any | None = None13511352 @classmethod1353 def for_include(1354 cls,1355 *,1356 parent_router: "APIRouter",1357 included_router: "APIRouter",1358 prefix: str = "",1359 tags: list[str | Enum] | None = None,1360 dependencies: Sequence[params.Depends] | None = None,1361 default_response_class: type[Response] | DefaultPlaceholder = Default(1362 JSONResponse1363 ),1364 responses: dict[int | str, dict[str, Any]] | None = None,1365 callbacks: list[BaseRoute] | None = None,1366 deprecated: bool | None = None,1367 include_in_schema: bool = True,1368 generate_unique_id_function: Callable[[APIRoute], str]1369 | DefaultPlaceholder = Default(generate_unique_id),1370 ) -> "_RouterIncludeContext":1371 return cls(1372 included_router=included_router,1373 prefix=parent_router.prefix + prefix,1374 tags=[*parent_router.tags, *(tags or [])],1375 dependencies=[*parent_router.dependencies, *(dependencies or [])],1376 default_response_class=get_value_or_default(1377 default_response_class, parent_router.default_response_class1378 ),1379 responses={**parent_router.responses, **(responses or {})},1380 callbacks=[*parent_router.callbacks, *(callbacks or [])],1381 deprecated=deprecated or parent_router.deprecated,1382 include_in_schema=parent_router.include_in_schema and include_in_schema,1383 generate_unique_id_function=get_value_or_default(1384 generate_unique_id_function, parent_router.generate_unique_id_function1385 ),1386 strict_content_type=parent_router.strict_content_type,1387 dependency_overrides_provider=parent_router.dependency_overrides_provider,1388 )13891390 def combine(1391 self, child_context: "_RouterIncludeContext"1392 ) -> "_RouterIncludeContext":1393 return _RouterIncludeContext(1394 included_router=child_context.included_router,1395 prefix=self.prefix + child_context.prefix,1396 tags=[*self.tags, *child_context.tags],1397 dependencies=[*self.dependencies, *child_context.dependencies],1398 default_response_class=get_value_or_default(1399 child_context.default_response_class, self.default_response_class1400 ),1401 responses={**self.responses, **child_context.responses},1402 callbacks=[*self.callbacks, *child_context.callbacks],1403 deprecated=self.deprecated or child_context.deprecated,1404 include_in_schema=self.include_in_schema1405 and child_context.include_in_schema,1406 generate_unique_id_function=get_value_or_default(1407 child_context.generate_unique_id_function,1408 self.generate_unique_id_function,1409 ),1410 strict_content_type=get_value_or_default(1411 child_context.strict_content_type, self.strict_content_type1412 ),1413 dependency_overrides_provider=self.dependency_overrides_provider,1414 )14151416 def path_for(self, route: _RouteWithPath) -> str:1417 return self.prefix + route.path141814191420@dataclass1421class _EffectiveRouteContext:1422 original_route: BaseRoute1423 starlette_route: BaseRoute | None = None1424 app: ASGIApp | None = field(default=None, repr=False, compare=False)1425 frontend_prefix: str = ""1426 path: str = ""1427 endpoint: Callable[..., Any] | None = None1428 stream_item_type: Any | None = None1429 response_model: Any = None1430 summary: str | None = None1431 response_description: str = "Successful Response"1432 deprecated: bool | None = None1433 operation_id: str | None = None1434 response_model_include: IncEx | None = None1435 response_model_exclude: IncEx | None = None1436 response_model_by_alias: bool = True1437 response_model_exclude_unset: bool = False1438 response_model_exclude_defaults: bool = False1439 response_model_exclude_none: bool = False1440 include_in_schema: bool = True1441 response_class: type[Response] | DefaultPlaceholder = field(1442 default_factory=lambda: Default(JSONResponse)1443 )1444 dependency_overrides_provider: Any | None = None1445 callbacks: list[BaseRoute] | None = None1446 openapi_extra: dict[str, Any] | None = None1447 generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder = field(1448 default_factory=lambda: Default(generate_unique_id)1449 )1450 strict_content_type: bool | DefaultPlaceholder = field(1451 default_factory=lambda: Default(True)1452 )1453 tags: list[str | Enum] = field(default_factory=list)1454 responses: dict[int | str, dict[str, Any]] = field(default_factory=dict)1455 name: str = ""1456 path_regex: Any = None1457 path_format: str = ""1458 param_convertors: dict[str, Any] = field(default_factory=dict)1459 methods: set[str] = field(default_factory=set)1460 unique_id: str = ""1461 status_code: int | None = None1462 response_field: ModelField | None = None1463 stream_item_field: ModelField | None = None1464 dependencies: list[params.Depends] = field(default_factory=list)1465 description: str = ""1466 response_fields: dict[int | str, ModelField] = field(default_factory=dict)1467 dependant: Dependant | None = None1468 _embed_body_fields: bool = False1469 body_field: ModelField | None = None1470 is_sse_stream: bool = False1471 is_json_stream: bool = False14721473 @classmethod1474 def from_api_route(1475 cls,1476 *,1477 original_route: APIRoute,1478 include_context: _RouterIncludeContext,1479 ) -> "_EffectiveRouteContext":1480 route = cast(_APIRouteLike, original_route)1481 context = cls(original_route=original_route)1482 _populate_api_route_state(1483 cast(_APIRouteLike, context),1484 include_context.path_for(original_route),1485 route.endpoint,1486 response_model=route.response_model,1487 status_code=route.status_code,1488 tags=[*include_context.tags, *route.tags],1489 dependencies=[*include_context.dependencies, *route.dependencies],1490 summary=route.summary,1491 description=route.description,1492 response_description=route.response_description,1493 responses={**include_context.responses, **route.responses},1494 deprecated=route.deprecated or include_context.deprecated,1495 methods=route.methods,1496 operation_id=route.operation_id,1497 response_model_include=route.response_model_include,1498 response_model_exclude=route.response_model_exclude,1499 response_model_by_alias=route.response_model_by_alias,1500 response_model_exclude_unset=route.response_model_exclude_unset,1501 response_model_exclude_defaults=route.response_model_exclude_defaults,1502 response_model_exclude_none=route.response_model_exclude_none,1503 include_in_schema=route.include_in_schema1504 and include_context.include_in_schema,1505 response_class=get_value_or_default(1506 route.response_class,1507 include_context.included_router.default_response_class,1508 include_context.default_response_class,1509 ),1510 name=route.name,1511 dependency_overrides_provider=include_context.dependency_overrides_provider,1512 callbacks=[*include_context.callbacks, *(route.callbacks or [])],1513 openapi_extra=route.openapi_extra,1514 generate_unique_id_function=get_value_or_default(1515 route.generate_unique_id_function,1516 include_context.included_router.generate_unique_id_function,1517 include_context.generate_unique_id_function,1518 ),1519 strict_content_type=get_value_or_default(1520 route.strict_content_type,1521 include_context.included_router.strict_content_type,1522 include_context.strict_content_type,1523 ),1524 stream_item_type=route.stream_item_type,1525 )1526 # Build once per inclusion context, just as APIRoute does for direct routes.1527 # Integrations may wrap dependant.call while constructing the handler.1528 token = _effective_route_context_var.set(context)1529 try:1530 context.app = request_response(original_route.get_route_handler())1531 finally:1532 _effective_route_context_var.reset(token)1533 return context15341535 @classmethod1536 def from_frontend_route_group(1537 cls,1538 *,1539 original_route: "_FrontendRouteGroup",1540 include_context: _RouterIncludeContext,1541 ) -> "_EffectiveRouteContext":1542 dependencies = [*include_context.dependencies, *original_route.dependencies]1543 context = cls(1544 original_route=original_route,1545 frontend_prefix=include_context.prefix,1546 dependencies=dependencies,1547 dependency_overrides_provider=include_context.dependency_overrides_provider,1548 )1549 (1550 context.dependant,1551 _,1552 context._embed_body_fields,1553 ) = _build_dependant_with_parameterless_dependencies(1554 path="",1555 call=_frontend_dependency_endpoint,1556 dependencies=dependencies,1557 )1558 return context15591560 def matches(self, scope: Scope) -> tuple[Match, Scope]:1561 if isinstance(self.original_route, _FrontendRouteGroup):1562 return self.original_route.matches_with_prefix(scope, self.frontend_prefix)1563 if not isinstance(self.original_route, APIRoute):1564 assert self.starlette_route is not None1565 return self.starlette_route.matches(scope)1566 if scope["type"] != "http":1567 return Match.NONE, {}1568 route_path = get_route_path(scope)1569 match = self.path_regex.match(route_path)1570 if not match:1571 return Match.NONE, {}1572 matched_params = match.groupdict()1573 for key, value in matched_params.items():1574 matched_params[key] = self.param_convertors[key].convert(value)1575 path_params = dict(scope.get("path_params", {}))1576 path_params.update(matched_params)1577 child_scope = {"endpoint": self.endpoint, "path_params": path_params}1578 methods = self.methods1579 if methods and scope["method"] not in methods:1580 return Match.PARTIAL, child_scope1581 return Match.FULL, child_scope15821583 def url_path_for(self, name: str, /, **path_params: Any) -> Any:1584 if not isinstance(self.original_route, APIRoute):1585 assert self.starlette_route is not None1586 return self.starlette_route.url_path_for(name, **path_params)1587 seen_params = set(path_params.keys())1588 param_convertors = self.param_convertors1589 expected_params = set(param_convertors.keys())1590 if name != self.name or seen_params != expected_params:1591 raise routing.NoMatchFound(name, path_params)1592 path, remaining_params = routing.replace_params(1593 self.path_format, param_convertors, path_params1594 )1595 assert not remaining_params1596 return URLPath(path=path, protocol="http")159715981599@dataclass(frozen=True)1600class RouteContext:1601 route: BaseRoute1602 _route_context: _EffectiveRouteContext | None = field(default=None, repr=False)16031604 @property1605 def original_route(self) -> BaseRoute:1606 if self._route_context is not None:1607 return self._route_context.original_route1608 return self.route16091610 @property1611 def _effective_route(self) -> BaseRoute | _EffectiveRouteContext:1612 if self._route_context is not None:1613 if self._route_context.starlette_route is not None:1614 return self._route_context.starlette_route1615 return self._route_context1616 return self.route16171618 @property1619 def path(self) -> str | None:1620 return getattr(self._effective_route, "path", None)16211622 @property1623 def path_format(self) -> str | None:1624 return getattr(self._effective_route, "path_format", None)16251626 @property1627 def name(self) -> str | None:1628 return getattr(self._effective_route, "name", None)16291630 @property1631 def methods(self) -> set[str] | None:1632 return getattr(self._effective_route, "methods", None)16331634 @property1635 def endpoint(self) -> Callable[..., Any] | None:1636 return getattr(self._effective_route, "endpoint", None)16371638 def __getattr__(self, name: str) -> Any:1639 return getattr(self._effective_route, name)164016411642@dataclass1643class _IncludedRouter(BaseRoute):1644 original_router: "APIRouter"1645 include_context: _RouterIncludeContext1646 _effective_routes_lock: Any = field(1647 default_factory=threading.Lock, repr=False, compare=False1648 )1649 _effective_candidates: list["_EffectiveRouteContext | _IncludedRouter"] = field(1650 default_factory=list1651 )1652 _effective_candidates_version: int | None = None1653 _effective_low_priority_routes: list["_EffectiveRouteContext"] = field(1654 default_factory=list1655 )1656 _effective_low_priority_routes_version: int | None = None16571658 def effective_candidates(self) -> list["_EffectiveRouteContext | _IncludedRouter"]:1659 routes_version = self.original_router._get_routes_version()1660 if routes_version == self._effective_candidates_version:1661 return self._effective_candidates1662 with self._effective_routes_lock:1663 routes_version = self.original_router._get_routes_version()1664 if routes_version == self._effective_candidates_version:1665 return self._effective_candidates1666 effective_candidates: list[_EffectiveRouteContext | _IncludedRouter] = []1667 for route in self.original_router.routes:1668 if isinstance(route, _IncludedRouter):1669 child_context = self.include_context.combine(route.include_context)1670 child_branch = _IncludedRouter(1671 original_router=route.original_router,1672 include_context=child_context,1673 )1674 effective_candidates.append(child_branch)1675 continue1676 route_context = self._build_effective_context(route)1677 if route_context is not None:1678 effective_candidates.append(route_context)1679 self._effective_candidates = effective_candidates1680 self._effective_candidates_version = routes_version1681 return effective_candidates16821683 def effective_low_priority_routes(self) -> list["_EffectiveRouteContext"]:1684 routes_version = self.original_router._get_routes_version()1685 if routes_version == self._effective_low_priority_routes_version:1686 return self._effective_low_priority_routes1687 with self._effective_routes_lock:1688 routes_version = self.original_router._get_routes_version()1689 if routes_version == self._effective_low_priority_routes_version:1690 return self._effective_low_priority_routes1691 effective_low_priority_routes: list[_EffectiveRouteContext] = []1692 for route in self.original_router._low_priority_routes:1693 route_context = self._build_effective_context(route)1694 if route_context is not None:1695 effective_low_priority_routes.append(route_context)1696 for route in self.original_router.routes:1697 if isinstance(route, _IncludedRouter):1698 child_context = self.include_context.combine(route.include_context)1699 child_branch = _IncludedRouter(1700 original_router=route.original_router,1701 include_context=child_context,1702 )1703 effective_low_priority_routes.extend(1704 child_branch.effective_low_priority_routes()1705 )1706 self._effective_low_priority_routes = effective_low_priority_routes1707 self._effective_low_priority_routes_version = routes_version1708 return effective_low_priority_routes17091710 def _build_effective_context(1711 self, route: BaseRoute1712 ) -> _EffectiveRouteContext | None:1713 if isinstance(route, APIRoute):1714 return _EffectiveRouteContext.from_api_route(1715 original_route=route,1716 include_context=self.include_context,1717 )1718 if isinstance(route, _FrontendRouteGroup):1719 return _EffectiveRouteContext.from_frontend_route_group(1720 original_route=route,1721 include_context=self.include_context,1722 )1723 if isinstance(route, routing.Route):1724 starlette_route: BaseRoute = routing.Route(1725 self.include_context.path_for(route),1726 endpoint=route.endpoint,1727 methods=list(route.methods or []),1728 name=route.name,1729 include_in_schema=route.include_in_schema,1730 )1731 return _EffectiveRouteContext(1732 original_route=route,1733 starlette_route=starlette_route,1734 )1735 if isinstance(route, APIWebSocketRoute):1736 starlette_route = APIWebSocketRoute(1737 self.include_context.path_for(route),1738 endpoint=route.endpoint,1739 name=route.name,1740 dependencies=[*self.include_context.dependencies, *route.dependencies],1741 dependency_overrides_provider=(1742 self.include_context.dependency_overrides_provider1743 ),1744 )1745 return _EffectiveRouteContext(1746 original_route=route,1747 starlette_route=starlette_route,1748 )1749 if isinstance(route, routing.WebSocketRoute):1750 starlette_route = routing.WebSocketRoute(1751 self.include_context.path_for(route), route.endpoint, name=route.name1752 )1753 return _EffectiveRouteContext(1754 original_route=route,1755 starlette_route=starlette_route,1756 )1757 if isinstance(route, routing.Mount):1758 starlette_route = copy.copy(route)1759 starlette_route.path = self.include_context.path_for(route).rstrip("/")1760 (1761 starlette_route.path_regex,1762 starlette_route.path_format,1763 starlette_route.param_convertors,1764 ) = compile_path(starlette_route.path + "/{path:path}")1765 return _EffectiveRouteContext(1766 original_route=route,1767 starlette_route=starlette_route,1768 )1769 if isinstance(route, routing.Host):1770 if self.include_context.prefix:1771 prefixed_app: ASGIApp = routing.Router(1772 routes=[routing.Mount(self.include_context.prefix, app=route.app)]1773 )1774 else:1775 prefixed_app = route.app1776 starlette_route = routing.Host(1777 route.host, app=prefixed_app, name=route.name1778 )1779 return _EffectiveRouteContext(1780 original_route=route,1781 starlette_route=starlette_route,1782 )1783 return None17841785 def _match(1786 self, scope: Scope1787 ) -> tuple[Match, Scope, BaseRoute | None, _EffectiveRouteContext | None]:1788 partial: tuple[Scope, BaseRoute, _EffectiveRouteContext | None] | None = None1789 for candidate in self.effective_candidates():1790 if isinstance(candidate, _IncludedRouter):1791 match, child_scope = candidate.matches(scope)1792 route: BaseRoute = candidate1793 route_context = None1794 elif isinstance(candidate.original_route, APIRoute):1795 route_context = candidate1796 fastapi_scope = _get_fastapi_scope(scope)1797 previous_context = fastapi_scope.get(1798 _FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY, _SCOPE_MISSING1799 )1800 fastapi_scope[_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY] = route_context1801 try:1802 match, child_scope = candidate.original_route.matches(scope)1803 finally:1804 _restore_fastapi_scope_key(1805 scope, _FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY, previous_context1806 )1807 route = candidate.original_route1808 else:1809 route_context = candidate1810 match, child_scope = candidate.matches(scope)1811 route = candidate.starlette_route or candidate.original_route1812 if match == Match.FULL:1813 return match, child_scope, route, route_context1814 if match == Match.PARTIAL and partial is None:1815 partial = (child_scope, route, route_context)1816 if partial is not None:1817 child_scope, route, route_context = partial1818 return Match.PARTIAL, child_scope, route, route_context1819 return Match.NONE, {}, None, None18201821 def matches(self, scope: Scope) -> tuple[Match, Scope]:1822 fastapi_scope = _get_fastapi_scope(scope)1823 previous_router = fastapi_scope.get(1824 _FASTAPI_INCLUDED_ROUTER_KEY, _SCOPE_MISSING1825 )1826 fastapi_scope[_FASTAPI_INCLUDED_ROUTER_KEY] = self1827 try:1828 match, _ = self.original_router.matches(scope)1829 return match, {}1830 finally:1831 _restore_fastapi_scope_key(1832 scope, _FASTAPI_INCLUDED_ROUTER_KEY, previous_router1833 )18341835 async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:1836 _get_fastapi_scope(scope)[_FASTAPI_INCLUDED_ROUTER_KEY] = self1837 await self.original_router.handle(scope, receive, send)18381839 async def _handle_selected(1840 self, scope: Scope, receive: Receive, send: Send1841 ) -> None:1842 match, child_scope, route, effective_context = self._match(scope)1843 if match == Match.NONE or route is None:1844 await self.original_router.default(scope, receive, send)1845 return1846 scope.update(child_scope)1847 if isinstance(route, _IncludedRouter):1848 await route.handle(scope, receive, send)1849 return1850 if effective_context is not None:1851 _route_selected(1852 scope=scope,1853 path=getattr(effective_context.starlette_route, "path_format", None)1854 or effective_context.path_format,1855 mount=isinstance(route, routing.Mount),1856 )1857 _get_fastapi_scope(scope)[_FASTAPI_EFFECTIVE_ROUTE_CONTEXT_KEY] = (1858 effective_context1859 )1860 original_route = effective_context.original_route1861 if isinstance(original_route, APIRoute):1862 scope["route"] = original_route1863 await original_route.handle(scope, receive, send)1864 return1865 await route.handle(scope, receive, send)18661867 def effective_route_contexts(self) -> Iterator[_EffectiveRouteContext]:1868 for candidate in self.effective_candidates():1869 if isinstance(candidate, _IncludedRouter):1870 yield from candidate.effective_route_contexts()1871 else:1872 yield candidate18731874 def url_path_for(self, name: str, /, **path_params: Any) -> Any:1875 for route_context in self.effective_route_contexts():1876 try:1877 return route_context.url_path_for(name, **path_params)1878 except routing.NoMatchFound:1879 pass1880 raise routing.NoMatchFound(name, path_params)188118821883def _iter_included_route_candidates(routes: Sequence[BaseRoute]) -> Iterator[BaseRoute]:1884 for route, route_context in _iter_routes_with_context(routes):1885 if route_context is not None and route_context.starlette_route is not None:1886 yield route_context.starlette_route1887 else:1888 yield route188918901891def iter_route_contexts(1892 routes: Sequence[BaseRoute | RouteContext],1893) -> Iterator[RouteContext]:1894 for route in routes:1895 if isinstance(route, RouteContext):1896 yield route1897 continue1898 for original_route, route_context in _iter_routes_with_context([route]):1899 if route_context is None:1900 yield RouteContext(original_route)1901 else:1902 yield RouteContext(original_route, route_context)190319041905def _iter_routes_with_context(1906 routes: Sequence[BaseRoute],1907) -> Iterator[tuple[BaseRoute, _EffectiveRouteContext | None]]:1908 for route in routes:1909 if isinstance(route, _IncludedRouter):1910 for route_context in route.effective_route_contexts():1911 yield route_context.original_route, route_context1912 else:1913 yield route, None191419151916def _normalize_frontend_path(path: str) -> str:1917 if not path:1918 raise AssertionError("A frontend path cannot be empty")1919 if not path.startswith("/"):1920 raise AssertionError("A frontend path must start with '/'")1921 if path != "/":1922 path = path.rstrip("/")1923 return path192419251926def _join_frontend_paths(prefix: str, path: str) -> str:1927 if not prefix:1928 return path1929 if path == "/":1930 return prefix1931 return prefix + path193219331934def _frontend_path_specificity(path: str) -> int:1935 if path == "/":1936 return 01937 return len(path)193819391940def _get_resolved_absolute_path(path: str | os.PathLike[str]) -> str:1941 return os.path.realpath(os.fspath(path))194219431944def _resolve_frontend_check_dir(1945 *,1946 directory: str | os.PathLike[str],1947 check_dir: bool | Literal["auto"],1948) -> bool:1949 if check_dir != "auto":1950 return check_dir1951 if os.environ.get("FASTAPI_ENV") != "development":1952 return True1953 if not os.path.isdir(directory):1954 warnings.warn(1955 f"Frontend directory '{directory}' does not exist. "1956 f"Resolved absolute path: '{_get_resolved_absolute_path(directory)}'",1957 stacklevel=3,1958 )1959 return False196019611962class _FrontendStaticFiles(StaticFiles):1963 def __init__(1964 self,1965 *,1966 directory: str | os.PathLike[str],1967 fallback: Literal["auto", "index.html", "404.html"] | None,1968 check_dir: bool,1969 ) -> None:1970 self.fallback = fallback1971 if check_dir and not os.path.isdir(directory):1972 raise RuntimeError(1973 f"Frontend directory '{directory}' does not exist. "1974 f"Resolved absolute path: '{_get_resolved_absolute_path(directory)}'"1975 )1976 super().__init__(1977 directory=directory,1978 html=True,1979 check_dir=check_dir,1980 follow_symlink=False,1981 )1982 if check_dir and fallback in {"index.html", "404.html"}:1983 self._check_fallback_file(fallback)19841985 def _check_fallback_file(self, fallback: str) -> None:1986 _, stat_result = self.lookup_path(fallback)1987 if stat_result is None or not stat.S_ISREG(stat_result.st_mode):1988 raise RuntimeError(1989 f"Frontend fallback file '{fallback}' does not exist in "1990 f"directory '{self.directory}'. Resolved absolute directory: "1991 f"'{self._get_resolved_directory()}'"1992 )19931994 def _get_resolved_directory(self) -> str:1995 assert self.directory is not None1996 return _get_resolved_absolute_path(self.directory)19971998 def get_path(self, scope: Scope) -> str:1999 path = _get_fastapi_scope(scope).get(_FASTAPI_FRONTEND_PATH_KEY, "")2000 assert isinstance(path, str)
Findings
✓ No findings reported for this file.