|
|
@ -380,7 +380,6 @@ def get_request_handler( |
|
|
actual_strict_content_type = strict_content_type |
|
|
actual_strict_content_type = strict_content_type |
|
|
|
|
|
|
|
|
async def app(request: Request) -> Response: |
|
|
async def app(request: Request) -> Response: |
|
|
response: Response | None = None |
|
|
|
|
|
file_stack = request.scope.get("fastapi_middleware_astack") |
|
|
file_stack = request.scope.get("fastapi_middleware_astack") |
|
|
assert isinstance(file_stack, AsyncExitStack), ( |
|
|
assert isinstance(file_stack, AsyncExitStack), ( |
|
|
"fastapi_middleware_astack not found in request scope" |
|
|
"fastapi_middleware_astack not found in request scope" |
|
|
@ -449,7 +448,6 @@ def get_request_handler( |
|
|
raise http_error from e |
|
|
raise http_error from e |
|
|
|
|
|
|
|
|
# Solve dependencies and run path operation function, auto-closing dependencies |
|
|
# Solve dependencies and run path operation function, auto-closing dependencies |
|
|
errors: list[Any] = [] |
|
|
|
|
|
async_exit_stack = request.scope.get("fastapi_inner_astack") |
|
|
async_exit_stack = request.scope.get("fastapi_inner_astack") |
|
|
assert isinstance(async_exit_stack, AsyncExitStack), ( |
|
|
assert isinstance(async_exit_stack, AsyncExitStack), ( |
|
|
"fastapi_inner_astack not found in request scope" |
|
|
"fastapi_inner_astack not found in request scope" |
|
|
@ -464,15 +462,17 @@ def get_request_handler( |
|
|
) |
|
|
) |
|
|
errors = solved_result.errors |
|
|
errors = solved_result.errors |
|
|
assert dependant.call # For types |
|
|
assert dependant.call # For types |
|
|
if not errors: |
|
|
if errors: |
|
|
|
|
|
raise RequestValidationError( |
|
|
|
|
|
errors, body=body, endpoint_ctx=endpoint_ctx |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
# Shared serializer for stream items (JSONL and SSE). |
|
|
# Shared serializer for stream items (JSONL and SSE). |
|
|
# Validates against stream_item_field when set, then |
|
|
# Validates against stream_item_field when set, then |
|
|
# serializes to JSON bytes. |
|
|
# serializes to JSON bytes. |
|
|
def _serialize_data(data: Any) -> bytes: |
|
|
def _serialize_data(data: Any) -> bytes: |
|
|
if stream_item_field: |
|
|
if stream_item_field: |
|
|
value, errors_ = stream_item_field.validate( |
|
|
value, errors_ = stream_item_field.validate(data, {}, loc=("response",)) |
|
|
data, {}, loc=("response",) |
|
|
|
|
|
) |
|
|
|
|
|
if errors_: |
|
|
if errors_: |
|
|
ctx = endpoint_ctx or EndpointContext() |
|
|
ctx = endpoint_ctx or EndpointContext() |
|
|
raise ResponseValidationError( |
|
|
raise ResponseValidationError( |
|
|
@ -489,7 +489,7 @@ def get_request_handler( |
|
|
exclude_defaults=response_model_exclude_defaults, |
|
|
exclude_defaults=response_model_exclude_defaults, |
|
|
exclude_none=response_model_exclude_none, |
|
|
exclude_none=response_model_exclude_none, |
|
|
) |
|
|
) |
|
|
else: |
|
|
|
|
|
data = jsonable_encoder(data) |
|
|
data = jsonable_encoder(data) |
|
|
return json.dumps(data).encode("utf-8") |
|
|
return json.dumps(data).encode("utf-8") |
|
|
|
|
|
|
|
|
@ -519,9 +519,7 @@ def get_request_handler( |
|
|
retry=item.retry, |
|
|
retry=item.retry, |
|
|
comment=item.comment, |
|
|
comment=item.comment, |
|
|
) |
|
|
) |
|
|
else: |
|
|
|
|
|
# Plain object: validate + serialize via |
|
|
|
|
|
# stream_item_field (if set) and wrap in data field |
|
|
|
|
|
return format_sse_event( |
|
|
return format_sse_event( |
|
|
data_str=_serialize_data(item).decode("utf-8") |
|
|
data_str=_serialize_data(item).decode("utf-8") |
|
|
) |
|
|
) |
|
|
@ -583,15 +581,9 @@ def get_request_handler( |
|
|
yield receive_keepalive |
|
|
yield receive_keepalive |
|
|
tg.cancel_scope.cancel() |
|
|
tg.cancel_scope.cancel() |
|
|
|
|
|
|
|
|
# Enter the SSE context manager on the request-scoped |
|
|
|
|
|
# exit stack. The stack outlives the streaming response, |
|
|
|
|
|
# so __aexit__ runs via proper structured teardown, not |
|
|
|
|
|
# via GeneratorExit thrown into an async generator. |
|
|
|
|
|
sse_receive_stream = await async_exit_stack.enter_async_context( |
|
|
sse_receive_stream = await async_exit_stack.enter_async_context( |
|
|
_sse_producer_cm() |
|
|
_sse_producer_cm() |
|
|
) |
|
|
) |
|
|
# Ensure the receive stream is closed when the exit stack |
|
|
|
|
|
# unwinds, preventing ResourceWarning from __del__. |
|
|
|
|
|
async_exit_stack.push_async_callback(sse_receive_stream.aclose) |
|
|
async_exit_stack.push_async_callback(sse_receive_stream.aclose) |
|
|
|
|
|
|
|
|
async def _sse_with_checkpoints( |
|
|
async def _sse_with_checkpoints( |
|
|
@ -607,7 +599,6 @@ def get_request_handler( |
|
|
sse_stream_content: AsyncIterator[bytes] | Iterator[bytes] = ( |
|
|
sse_stream_content: AsyncIterator[bytes] | Iterator[bytes] = ( |
|
|
_sse_with_checkpoints(sse_receive_stream) |
|
|
_sse_with_checkpoints(sse_receive_stream) |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
response = StreamingResponse( |
|
|
response = StreamingResponse( |
|
|
sse_stream_content, |
|
|
sse_stream_content, |
|
|
media_type="text/event-stream", |
|
|
media_type="text/event-stream", |
|
|
@ -617,7 +608,9 @@ def get_request_handler( |
|
|
# For Nginx proxies to not buffer server sent events |
|
|
# For Nginx proxies to not buffer server sent events |
|
|
response.headers["X-Accel-Buffering"] = "no" |
|
|
response.headers["X-Accel-Buffering"] = "no" |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
elif is_json_stream: |
|
|
return response |
|
|
|
|
|
|
|
|
|
|
|
if is_json_stream: |
|
|
# Generator endpoint: stream as JSONL |
|
|
# Generator endpoint: stream as JSONL |
|
|
gen = dependant.call(**solved_result.values) |
|
|
gen = dependant.call(**solved_result.values) |
|
|
|
|
|
|
|
|
@ -650,8 +643,10 @@ def get_request_handler( |
|
|
background=solved_result.background_tasks, |
|
|
background=solved_result.background_tasks, |
|
|
) |
|
|
) |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
elif dependant.is_async_gen_callable or dependant.is_gen_callable: |
|
|
return response |
|
|
# Raw streaming with explicit response_class (e.g. StreamingResponse) |
|
|
|
|
|
|
|
|
if dependant.is_async_gen_callable or dependant.is_gen_callable: |
|
|
|
|
|
# Raw streaming with explicit response_class |
|
|
gen = dependant.call(**solved_result.values) |
|
|
gen = dependant.call(**solved_result.values) |
|
|
if dependant.is_async_gen_callable: |
|
|
if dependant.is_async_gen_callable: |
|
|
|
|
|
|
|
|
@ -670,7 +665,8 @@ def get_request_handler( |
|
|
) |
|
|
) |
|
|
response = actual_response_class(content=gen, **response_args) |
|
|
response = actual_response_class(content=gen, **response_args) |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
else: |
|
|
return response |
|
|
|
|
|
|
|
|
raw_response = await run_endpoint_function( |
|
|
raw_response = await run_endpoint_function( |
|
|
dependant=dependant, |
|
|
dependant=dependant, |
|
|
values=solved_result.values, |
|
|
values=solved_result.values, |
|
|
@ -679,8 +675,8 @@ def get_request_handler( |
|
|
if isinstance(raw_response, Response): |
|
|
if isinstance(raw_response, Response): |
|
|
if raw_response.background is None: |
|
|
if raw_response.background is None: |
|
|
raw_response.background = solved_result.background_tasks |
|
|
raw_response.background = solved_result.background_tasks |
|
|
response = raw_response |
|
|
return raw_response |
|
|
else: |
|
|
|
|
|
response_args = _build_response_args( |
|
|
response_args = _build_response_args( |
|
|
status_code=status_code, solved_result=solved_result |
|
|
status_code=status_code, solved_result=solved_result |
|
|
) |
|
|
) |
|
|
@ -716,14 +712,6 @@ def get_request_handler( |
|
|
if not is_body_allowed_for_status_code(response.status_code): |
|
|
if not is_body_allowed_for_status_code(response.status_code): |
|
|
response.body = b"" |
|
|
response.body = b"" |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
response.headers.raw.extend(solved_result.response.headers.raw) |
|
|
if errors: |
|
|
|
|
|
validation_error = RequestValidationError( |
|
|
|
|
|
errors, body=body, endpoint_ctx=endpoint_ctx |
|
|
|
|
|
) |
|
|
|
|
|
raise validation_error |
|
|
|
|
|
|
|
|
|
|
|
# Return response |
|
|
|
|
|
assert response |
|
|
|
|
|
return response |
|
|
return response |
|
|
|
|
|
|
|
|
return app |
|
|
return app |
|
|
|