ccproxy-api 0.1.7__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ccproxy/api/__init__.py +1 -15
- ccproxy/api/app.py +434 -219
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +144 -168
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +388 -524
- ccproxy/api/middleware/hooks.py +563 -0
- ccproxy/api/middleware/normalize_headers.py +59 -0
- ccproxy/api/middleware/request_id.py +35 -16
- ccproxy/api/middleware/streaming_hooks.py +292 -0
- ccproxy/api/routes/__init__.py +5 -14
- ccproxy/api/routes/health.py +39 -672
- ccproxy/api/routes/plugins.py +277 -0
- ccproxy/auth/__init__.py +2 -19
- ccproxy/auth/bearer.py +25 -15
- ccproxy/auth/dependencies.py +123 -157
- ccproxy/auth/exceptions.py +0 -12
- ccproxy/auth/manager.py +35 -49
- ccproxy/auth/managers/__init__.py +10 -0
- ccproxy/auth/managers/base.py +523 -0
- ccproxy/auth/managers/base_enhanced.py +63 -0
- ccproxy/auth/managers/token_snapshot.py +77 -0
- ccproxy/auth/models/base.py +65 -0
- ccproxy/auth/models/credentials.py +40 -0
- ccproxy/auth/oauth/__init__.py +4 -18
- ccproxy/auth/oauth/base.py +533 -0
- ccproxy/auth/oauth/cli_errors.py +37 -0
- ccproxy/auth/oauth/flows.py +430 -0
- ccproxy/auth/oauth/protocol.py +366 -0
- ccproxy/auth/oauth/registry.py +408 -0
- ccproxy/auth/oauth/router.py +396 -0
- ccproxy/auth/oauth/routes.py +186 -113
- ccproxy/auth/oauth/session.py +151 -0
- ccproxy/auth/oauth/templates.py +342 -0
- ccproxy/auth/storage/__init__.py +2 -5
- ccproxy/auth/storage/base.py +279 -5
- ccproxy/auth/storage/generic.py +134 -0
- ccproxy/cli/__init__.py +1 -2
- ccproxy/cli/_settings_help.py +351 -0
- ccproxy/cli/commands/auth.py +1519 -793
- ccproxy/cli/commands/config/commands.py +209 -276
- ccproxy/cli/commands/plugins.py +669 -0
- ccproxy/cli/commands/serve.py +75 -810
- ccproxy/cli/commands/status.py +254 -0
- ccproxy/cli/decorators.py +83 -0
- ccproxy/cli/helpers.py +22 -60
- ccproxy/cli/main.py +359 -10
- ccproxy/cli/options/claude_options.py +0 -25
- ccproxy/config/__init__.py +7 -11
- ccproxy/config/core.py +227 -0
- ccproxy/config/env_generator.py +232 -0
- ccproxy/config/runtime.py +67 -0
- ccproxy/config/security.py +36 -3
- ccproxy/config/settings.py +382 -441
- ccproxy/config/toml_generator.py +299 -0
- ccproxy/config/utils.py +452 -0
- ccproxy/core/__init__.py +7 -271
- ccproxy/{_version.py → core/_version.py} +16 -3
- ccproxy/core/async_task_manager.py +516 -0
- ccproxy/core/async_utils.py +47 -14
- ccproxy/core/auth/__init__.py +6 -0
- ccproxy/core/constants.py +16 -50
- ccproxy/core/errors.py +53 -0
- ccproxy/core/id_utils.py +20 -0
- ccproxy/core/interfaces.py +16 -123
- ccproxy/core/logging.py +473 -18
- ccproxy/core/plugins/__init__.py +77 -0
- ccproxy/core/plugins/cli_discovery.py +211 -0
- ccproxy/core/plugins/declaration.py +455 -0
- ccproxy/core/plugins/discovery.py +604 -0
- ccproxy/core/plugins/factories.py +967 -0
- ccproxy/core/plugins/hooks/__init__.py +30 -0
- ccproxy/core/plugins/hooks/base.py +58 -0
- ccproxy/core/plugins/hooks/events.py +46 -0
- ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
- ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
- ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
- ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
- ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
- ccproxy/core/plugins/hooks/layers.py +44 -0
- ccproxy/core/plugins/hooks/manager.py +186 -0
- ccproxy/core/plugins/hooks/registry.py +139 -0
- ccproxy/core/plugins/hooks/thread_manager.py +203 -0
- ccproxy/core/plugins/hooks/types.py +22 -0
- ccproxy/core/plugins/interfaces.py +416 -0
- ccproxy/core/plugins/loader.py +166 -0
- ccproxy/core/plugins/middleware.py +233 -0
- ccproxy/core/plugins/models.py +59 -0
- ccproxy/core/plugins/protocol.py +180 -0
- ccproxy/core/plugins/runtime.py +519 -0
- ccproxy/{observability/context.py → core/request_context.py} +137 -94
- ccproxy/core/status_report.py +211 -0
- ccproxy/core/transformers.py +13 -8
- ccproxy/data/claude_headers_fallback.json +540 -19
- ccproxy/data/codex_headers_fallback.json +114 -7
- ccproxy/http/__init__.py +30 -0
- ccproxy/http/base.py +95 -0
- ccproxy/http/client.py +323 -0
- ccproxy/http/hooks.py +642 -0
- ccproxy/http/pool.py +279 -0
- ccproxy/llms/formatters/__init__.py +7 -0
- ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
- ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
- ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
- ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
- ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
- ccproxy/llms/formatters/base.py +140 -0
- ccproxy/llms/formatters/base_model.py +33 -0
- ccproxy/llms/formatters/common/__init__.py +51 -0
- ccproxy/llms/formatters/common/identifiers.py +48 -0
- ccproxy/llms/formatters/common/streams.py +254 -0
- ccproxy/llms/formatters/common/thinking.py +74 -0
- ccproxy/llms/formatters/common/usage.py +135 -0
- ccproxy/llms/formatters/constants.py +55 -0
- ccproxy/llms/formatters/context.py +116 -0
- ccproxy/llms/formatters/mapping.py +33 -0
- ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
- ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
- ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
- ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
- ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
- ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
- ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
- ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
- ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
- ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
- ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
- ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
- ccproxy/llms/formatters/utils.py +306 -0
- ccproxy/llms/models/__init__.py +9 -0
- ccproxy/llms/models/anthropic.py +619 -0
- ccproxy/llms/models/openai.py +844 -0
- ccproxy/llms/streaming/__init__.py +26 -0
- ccproxy/llms/streaming/accumulators.py +1074 -0
- ccproxy/llms/streaming/formatters.py +251 -0
- ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
- ccproxy/models/__init__.py +8 -159
- ccproxy/models/detection.py +92 -193
- ccproxy/models/provider.py +75 -0
- ccproxy/plugins/access_log/README.md +32 -0
- ccproxy/plugins/access_log/__init__.py +20 -0
- ccproxy/plugins/access_log/config.py +33 -0
- ccproxy/plugins/access_log/formatter.py +126 -0
- ccproxy/plugins/access_log/hook.py +763 -0
- ccproxy/plugins/access_log/logger.py +254 -0
- ccproxy/plugins/access_log/plugin.py +137 -0
- ccproxy/plugins/access_log/writer.py +109 -0
- ccproxy/plugins/analytics/README.md +24 -0
- ccproxy/plugins/analytics/__init__.py +1 -0
- ccproxy/plugins/analytics/config.py +5 -0
- ccproxy/plugins/analytics/ingest.py +85 -0
- ccproxy/plugins/analytics/models.py +97 -0
- ccproxy/plugins/analytics/plugin.py +121 -0
- ccproxy/plugins/analytics/routes.py +163 -0
- ccproxy/plugins/analytics/service.py +284 -0
- ccproxy/plugins/claude_api/README.md +29 -0
- ccproxy/plugins/claude_api/__init__.py +10 -0
- ccproxy/plugins/claude_api/adapter.py +829 -0
- ccproxy/plugins/claude_api/config.py +52 -0
- ccproxy/plugins/claude_api/detection_service.py +461 -0
- ccproxy/plugins/claude_api/health.py +175 -0
- ccproxy/plugins/claude_api/hooks.py +284 -0
- ccproxy/plugins/claude_api/models.py +256 -0
- ccproxy/plugins/claude_api/plugin.py +298 -0
- ccproxy/plugins/claude_api/routes.py +118 -0
- ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
- ccproxy/plugins/claude_api/tasks.py +84 -0
- ccproxy/plugins/claude_sdk/README.md +35 -0
- ccproxy/plugins/claude_sdk/__init__.py +80 -0
- ccproxy/plugins/claude_sdk/adapter.py +749 -0
- ccproxy/plugins/claude_sdk/auth.py +57 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
- ccproxy/plugins/claude_sdk/config.py +210 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
- ccproxy/plugins/claude_sdk/detection_service.py +163 -0
- ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
- ccproxy/plugins/claude_sdk/health.py +113 -0
- ccproxy/plugins/claude_sdk/hooks.py +115 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
- ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
- ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
- ccproxy/plugins/claude_sdk/options.py +154 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
- ccproxy/plugins/claude_sdk/plugin.py +269 -0
- ccproxy/plugins/claude_sdk/routes.py +104 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
- ccproxy/plugins/claude_sdk/session_pool.py +700 -0
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
- ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
- ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
- ccproxy/plugins/claude_sdk/tasks.py +97 -0
- ccproxy/plugins/claude_shared/README.md +18 -0
- ccproxy/plugins/claude_shared/__init__.py +12 -0
- ccproxy/plugins/claude_shared/model_defaults.py +171 -0
- ccproxy/plugins/codex/README.md +35 -0
- ccproxy/plugins/codex/__init__.py +6 -0
- ccproxy/plugins/codex/adapter.py +635 -0
- ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
- ccproxy/plugins/codex/detection_service.py +544 -0
- ccproxy/plugins/codex/health.py +162 -0
- ccproxy/plugins/codex/hooks.py +263 -0
- ccproxy/plugins/codex/model_defaults.py +39 -0
- ccproxy/plugins/codex/models.py +263 -0
- ccproxy/plugins/codex/plugin.py +275 -0
- ccproxy/plugins/codex/routes.py +129 -0
- ccproxy/plugins/codex/streaming_metrics.py +324 -0
- ccproxy/plugins/codex/tasks.py +106 -0
- ccproxy/plugins/codex/utils/__init__.py +1 -0
- ccproxy/plugins/codex/utils/sse_parser.py +106 -0
- ccproxy/plugins/command_replay/README.md +34 -0
- ccproxy/plugins/command_replay/__init__.py +17 -0
- ccproxy/plugins/command_replay/config.py +133 -0
- ccproxy/plugins/command_replay/formatter.py +432 -0
- ccproxy/plugins/command_replay/hook.py +294 -0
- ccproxy/plugins/command_replay/plugin.py +161 -0
- ccproxy/plugins/copilot/README.md +39 -0
- ccproxy/plugins/copilot/__init__.py +11 -0
- ccproxy/plugins/copilot/adapter.py +465 -0
- ccproxy/plugins/copilot/config.py +155 -0
- ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
- ccproxy/plugins/copilot/detection_service.py +255 -0
- ccproxy/plugins/copilot/manager.py +275 -0
- ccproxy/plugins/copilot/model_defaults.py +284 -0
- ccproxy/plugins/copilot/models.py +148 -0
- ccproxy/plugins/copilot/oauth/__init__.py +16 -0
- ccproxy/plugins/copilot/oauth/client.py +494 -0
- ccproxy/plugins/copilot/oauth/models.py +385 -0
- ccproxy/plugins/copilot/oauth/provider.py +602 -0
- ccproxy/plugins/copilot/oauth/storage.py +170 -0
- ccproxy/plugins/copilot/plugin.py +360 -0
- ccproxy/plugins/copilot/routes.py +294 -0
- ccproxy/plugins/credential_balancer/README.md +124 -0
- ccproxy/plugins/credential_balancer/__init__.py +6 -0
- ccproxy/plugins/credential_balancer/config.py +270 -0
- ccproxy/plugins/credential_balancer/factory.py +415 -0
- ccproxy/plugins/credential_balancer/hook.py +51 -0
- ccproxy/plugins/credential_balancer/manager.py +587 -0
- ccproxy/plugins/credential_balancer/plugin.py +146 -0
- ccproxy/plugins/dashboard/README.md +25 -0
- ccproxy/plugins/dashboard/__init__.py +1 -0
- ccproxy/plugins/dashboard/config.py +8 -0
- ccproxy/plugins/dashboard/plugin.py +71 -0
- ccproxy/plugins/dashboard/routes.py +67 -0
- ccproxy/plugins/docker/README.md +32 -0
- ccproxy/{docker → plugins/docker}/__init__.py +3 -0
- ccproxy/{docker → plugins/docker}/adapter.py +108 -10
- ccproxy/plugins/docker/config.py +82 -0
- ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
- ccproxy/{docker → plugins/docker}/middleware.py +2 -2
- ccproxy/plugins/docker/plugin.py +198 -0
- ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
- ccproxy/plugins/duckdb_storage/README.md +26 -0
- ccproxy/plugins/duckdb_storage/__init__.py +1 -0
- ccproxy/plugins/duckdb_storage/config.py +22 -0
- ccproxy/plugins/duckdb_storage/plugin.py +128 -0
- ccproxy/plugins/duckdb_storage/routes.py +51 -0
- ccproxy/plugins/duckdb_storage/storage.py +633 -0
- ccproxy/plugins/max_tokens/README.md +38 -0
- ccproxy/plugins/max_tokens/__init__.py +12 -0
- ccproxy/plugins/max_tokens/adapter.py +235 -0
- ccproxy/plugins/max_tokens/config.py +86 -0
- ccproxy/plugins/max_tokens/models.py +53 -0
- ccproxy/plugins/max_tokens/plugin.py +200 -0
- ccproxy/plugins/max_tokens/service.py +271 -0
- ccproxy/plugins/max_tokens/token_limits.json +54 -0
- ccproxy/plugins/metrics/README.md +35 -0
- ccproxy/plugins/metrics/__init__.py +10 -0
- ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
- ccproxy/plugins/metrics/config.py +85 -0
- ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
- ccproxy/plugins/metrics/hook.py +403 -0
- ccproxy/plugins/metrics/plugin.py +268 -0
- ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
- ccproxy/plugins/metrics/routes.py +107 -0
- ccproxy/plugins/metrics/tasks.py +117 -0
- ccproxy/plugins/oauth_claude/README.md +35 -0
- ccproxy/plugins/oauth_claude/__init__.py +14 -0
- ccproxy/plugins/oauth_claude/client.py +270 -0
- ccproxy/plugins/oauth_claude/config.py +84 -0
- ccproxy/plugins/oauth_claude/manager.py +482 -0
- ccproxy/plugins/oauth_claude/models.py +266 -0
- ccproxy/plugins/oauth_claude/plugin.py +149 -0
- ccproxy/plugins/oauth_claude/provider.py +571 -0
- ccproxy/plugins/oauth_claude/storage.py +212 -0
- ccproxy/plugins/oauth_codex/README.md +38 -0
- ccproxy/plugins/oauth_codex/__init__.py +14 -0
- ccproxy/plugins/oauth_codex/client.py +224 -0
- ccproxy/plugins/oauth_codex/config.py +95 -0
- ccproxy/plugins/oauth_codex/manager.py +256 -0
- ccproxy/plugins/oauth_codex/models.py +239 -0
- ccproxy/plugins/oauth_codex/plugin.py +146 -0
- ccproxy/plugins/oauth_codex/provider.py +574 -0
- ccproxy/plugins/oauth_codex/storage.py +92 -0
- ccproxy/plugins/permissions/README.md +28 -0
- ccproxy/plugins/permissions/__init__.py +22 -0
- ccproxy/plugins/permissions/config.py +28 -0
- ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
- ccproxy/plugins/permissions/handlers/protocol.py +33 -0
- ccproxy/plugins/permissions/handlers/terminal.py +675 -0
- ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
- ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
- ccproxy/plugins/permissions/plugin.py +153 -0
- ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
- ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
- ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
- ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
- ccproxy/plugins/pricing/README.md +34 -0
- ccproxy/plugins/pricing/__init__.py +6 -0
- ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
- ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
- ccproxy/plugins/pricing/exceptions.py +35 -0
- ccproxy/plugins/pricing/loader.py +440 -0
- ccproxy/{pricing → plugins/pricing}/models.py +13 -23
- ccproxy/plugins/pricing/plugin.py +169 -0
- ccproxy/plugins/pricing/service.py +191 -0
- ccproxy/plugins/pricing/tasks.py +300 -0
- ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
- ccproxy/plugins/pricing/utils.py +99 -0
- ccproxy/plugins/request_tracer/README.md +40 -0
- ccproxy/plugins/request_tracer/__init__.py +7 -0
- ccproxy/plugins/request_tracer/config.py +120 -0
- ccproxy/plugins/request_tracer/hook.py +415 -0
- ccproxy/plugins/request_tracer/plugin.py +255 -0
- ccproxy/scheduler/__init__.py +2 -14
- ccproxy/scheduler/core.py +26 -41
- ccproxy/scheduler/manager.py +61 -105
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +268 -276
- ccproxy/services/__init__.py +0 -1
- ccproxy/services/adapters/__init__.py +11 -0
- ccproxy/services/adapters/base.py +123 -0
- ccproxy/services/adapters/chain_composer.py +88 -0
- ccproxy/services/adapters/chain_validation.py +44 -0
- ccproxy/services/adapters/chat_accumulator.py +200 -0
- ccproxy/services/adapters/delta_utils.py +142 -0
- ccproxy/services/adapters/format_adapter.py +136 -0
- ccproxy/services/adapters/format_context.py +11 -0
- ccproxy/services/adapters/format_registry.py +158 -0
- ccproxy/services/adapters/http_adapter.py +1045 -0
- ccproxy/services/adapters/mock_adapter.py +118 -0
- ccproxy/services/adapters/protocols.py +35 -0
- ccproxy/services/adapters/simple_converters.py +571 -0
- ccproxy/services/auth_registry.py +180 -0
- ccproxy/services/cache/__init__.py +6 -0
- ccproxy/services/cache/response_cache.py +261 -0
- ccproxy/services/cli_detection.py +437 -0
- ccproxy/services/config/__init__.py +6 -0
- ccproxy/services/config/proxy_configuration.py +111 -0
- ccproxy/services/container.py +256 -0
- ccproxy/services/factories.py +380 -0
- ccproxy/services/handler_config.py +76 -0
- ccproxy/services/interfaces.py +298 -0
- ccproxy/services/mocking/__init__.py +6 -0
- ccproxy/services/mocking/mock_handler.py +291 -0
- ccproxy/services/tracing/__init__.py +7 -0
- ccproxy/services/tracing/interfaces.py +61 -0
- ccproxy/services/tracing/null_tracer.py +57 -0
- ccproxy/streaming/__init__.py +23 -0
- ccproxy/streaming/buffer.py +1056 -0
- ccproxy/streaming/deferred.py +897 -0
- ccproxy/streaming/handler.py +117 -0
- ccproxy/streaming/interfaces.py +77 -0
- ccproxy/streaming/simple_adapter.py +39 -0
- ccproxy/streaming/sse.py +109 -0
- ccproxy/streaming/sse_parser.py +127 -0
- ccproxy/templates/__init__.py +6 -0
- ccproxy/templates/plugin_scaffold.py +695 -0
- ccproxy/testing/endpoints/__init__.py +33 -0
- ccproxy/testing/endpoints/cli.py +215 -0
- ccproxy/testing/endpoints/config.py +874 -0
- ccproxy/testing/endpoints/console.py +57 -0
- ccproxy/testing/endpoints/models.py +100 -0
- ccproxy/testing/endpoints/runner.py +1903 -0
- ccproxy/testing/endpoints/tools.py +308 -0
- ccproxy/testing/mock_responses.py +70 -1
- ccproxy/testing/response_handlers.py +20 -0
- ccproxy/utils/__init__.py +0 -6
- ccproxy/utils/binary_resolver.py +476 -0
- ccproxy/utils/caching.py +327 -0
- ccproxy/utils/cli_logging.py +101 -0
- ccproxy/utils/command_line.py +251 -0
- ccproxy/utils/headers.py +228 -0
- ccproxy/utils/model_mapper.py +120 -0
- ccproxy/utils/startup_helpers.py +68 -446
- ccproxy/utils/version_checker.py +273 -6
- ccproxy_api-0.2.0.dist-info/METADATA +212 -0
- ccproxy_api-0.2.0.dist-info/RECORD +417 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
- ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
- ccproxy/__init__.py +0 -4
- ccproxy/adapters/__init__.py +0 -11
- ccproxy/adapters/base.py +0 -80
- ccproxy/adapters/codex/__init__.py +0 -11
- ccproxy/adapters/openai/__init__.py +0 -42
- ccproxy/adapters/openai/adapter.py +0 -953
- ccproxy/adapters/openai/models.py +0 -412
- ccproxy/adapters/openai/response_adapter.py +0 -355
- ccproxy/adapters/openai/response_models.py +0 -178
- ccproxy/api/middleware/headers.py +0 -49
- ccproxy/api/middleware/logging.py +0 -180
- ccproxy/api/middleware/request_content_logging.py +0 -297
- ccproxy/api/middleware/server_header.py +0 -58
- ccproxy/api/responses.py +0 -89
- ccproxy/api/routes/claude.py +0 -371
- ccproxy/api/routes/codex.py +0 -1251
- ccproxy/api/routes/metrics.py +0 -1029
- ccproxy/api/routes/proxy.py +0 -211
- ccproxy/api/services/__init__.py +0 -6
- ccproxy/auth/conditional.py +0 -84
- ccproxy/auth/credentials_adapter.py +0 -93
- ccproxy/auth/models.py +0 -118
- ccproxy/auth/oauth/models.py +0 -48
- ccproxy/auth/openai/__init__.py +0 -13
- ccproxy/auth/openai/credentials.py +0 -166
- ccproxy/auth/openai/oauth_client.py +0 -334
- ccproxy/auth/openai/storage.py +0 -184
- ccproxy/auth/storage/json_file.py +0 -158
- ccproxy/auth/storage/keyring.py +0 -189
- ccproxy/claude_sdk/__init__.py +0 -18
- ccproxy/claude_sdk/options.py +0 -194
- ccproxy/claude_sdk/session_pool.py +0 -550
- ccproxy/cli/docker/__init__.py +0 -34
- ccproxy/cli/docker/adapter_factory.py +0 -157
- ccproxy/cli/docker/params.py +0 -274
- ccproxy/config/auth.py +0 -153
- ccproxy/config/claude.py +0 -348
- ccproxy/config/cors.py +0 -79
- ccproxy/config/discovery.py +0 -95
- ccproxy/config/docker_settings.py +0 -264
- ccproxy/config/observability.py +0 -158
- ccproxy/config/reverse_proxy.py +0 -31
- ccproxy/config/scheduler.py +0 -108
- ccproxy/config/server.py +0 -86
- ccproxy/config/validators.py +0 -231
- ccproxy/core/codex_transformers.py +0 -389
- ccproxy/core/http.py +0 -328
- ccproxy/core/http_transformers.py +0 -812
- ccproxy/core/proxy.py +0 -143
- ccproxy/core/validators.py +0 -288
- ccproxy/models/errors.py +0 -42
- ccproxy/models/messages.py +0 -269
- ccproxy/models/requests.py +0 -107
- ccproxy/models/responses.py +0 -270
- ccproxy/models/types.py +0 -102
- ccproxy/observability/__init__.py +0 -51
- ccproxy/observability/access_logger.py +0 -457
- ccproxy/observability/sse_events.py +0 -303
- ccproxy/observability/stats_printer.py +0 -753
- ccproxy/observability/storage/__init__.py +0 -1
- ccproxy/observability/storage/duckdb_simple.py +0 -677
- ccproxy/observability/storage/models.py +0 -70
- ccproxy/observability/streaming_response.py +0 -107
- ccproxy/pricing/__init__.py +0 -19
- ccproxy/pricing/loader.py +0 -251
- ccproxy/services/claude_detection_service.py +0 -243
- ccproxy/services/codex_detection_service.py +0 -252
- ccproxy/services/credentials/__init__.py +0 -55
- ccproxy/services/credentials/config.py +0 -105
- ccproxy/services/credentials/manager.py +0 -561
- ccproxy/services/credentials/oauth_client.py +0 -481
- ccproxy/services/proxy_service.py +0 -1827
- ccproxy/static/.keep +0 -0
- ccproxy/utils/cost_calculator.py +0 -210
- ccproxy/utils/disconnection_monitor.py +0 -83
- ccproxy/utils/model_mapping.py +0 -199
- ccproxy/utils/models_provider.py +0 -150
- ccproxy/utils/simple_request_logger.py +0 -284
- ccproxy/utils/streaming_metrics.py +0 -199
- ccproxy_api-0.1.7.dist-info/METADATA +0 -615
- ccproxy_api-0.1.7.dist-info/RECORD +0 -191
- ccproxy_api-0.1.7.dist-info/entry_points.txt +0 -4
- /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
- /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
- /ccproxy/{docker → plugins/docker}/models.py +0 -0
- /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
- /ccproxy/{docker → plugins/docker}/validators.py +0 -0
- /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
- /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
- {ccproxy_api-0.1.7.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
ccproxy/api/middleware/errors.py
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
"""Error handling middleware for CCProxy API Server."""
|
|
2
2
|
|
|
3
|
+
import traceback
|
|
4
|
+
from collections.abc import Awaitable, Callable
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
3
7
|
from fastapi import FastAPI, HTTPException, Request
|
|
8
|
+
from fastapi.exceptions import RequestValidationError
|
|
4
9
|
from fastapi.responses import JSONResponse
|
|
5
10
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
6
|
-
from structlog import get_logger
|
|
7
11
|
|
|
12
|
+
from ccproxy.core.constants import (
|
|
13
|
+
FORMAT_ANTHROPIC_MESSAGES,
|
|
14
|
+
FORMAT_OPENAI_CHAT,
|
|
15
|
+
FORMAT_OPENAI_RESPONSES,
|
|
16
|
+
)
|
|
8
17
|
from ccproxy.core.errors import (
|
|
9
18
|
AuthenticationError,
|
|
10
19
|
ClaudeProxyError,
|
|
@@ -23,552 +32,314 @@ from ccproxy.core.errors import (
|
|
|
23
32
|
TransformationError,
|
|
24
33
|
ValidationError,
|
|
25
34
|
)
|
|
26
|
-
from ccproxy.
|
|
35
|
+
from ccproxy.core.logging import get_logger
|
|
36
|
+
from ccproxy.llms.models import anthropic as anthropic_models
|
|
37
|
+
from ccproxy.llms.models import openai as openai_models
|
|
27
38
|
|
|
28
39
|
|
|
29
40
|
logger = get_logger(__name__)
|
|
30
41
|
|
|
31
42
|
|
|
32
|
-
def
|
|
33
|
-
"""
|
|
43
|
+
def _detect_format_from_path(path: str) -> str | None:
|
|
44
|
+
"""Detect the expected format from the request path.
|
|
34
45
|
|
|
35
46
|
Args:
|
|
36
|
-
|
|
37
|
-
"""
|
|
38
|
-
logger.debug("error_handlers_setup_start")
|
|
39
|
-
|
|
40
|
-
# Get metrics instance for error recording
|
|
41
|
-
try:
|
|
42
|
-
metrics = get_metrics()
|
|
43
|
-
logger.debug("error_handlers_metrics_loaded")
|
|
44
|
-
except Exception as e:
|
|
45
|
-
logger.warning("error_handlers_metrics_unavailable", error=str(e))
|
|
46
|
-
metrics = None
|
|
47
|
-
|
|
48
|
-
@app.exception_handler(ClaudeProxyError)
|
|
49
|
-
async def claude_proxy_error_handler(
|
|
50
|
-
request: Request, exc: ClaudeProxyError
|
|
51
|
-
) -> JSONResponse:
|
|
52
|
-
"""Handle Claude proxy specific errors."""
|
|
53
|
-
# Store status code in request state for access logging
|
|
54
|
-
if hasattr(request.state, "context") and hasattr(
|
|
55
|
-
request.state.context, "metadata"
|
|
56
|
-
):
|
|
57
|
-
request.state.context.metadata["status_code"] = exc.status_code
|
|
58
|
-
|
|
59
|
-
logger.error(
|
|
60
|
-
"Claude proxy error",
|
|
61
|
-
error_type="claude_proxy_error",
|
|
62
|
-
error_message=str(exc),
|
|
63
|
-
status_code=exc.status_code,
|
|
64
|
-
request_method=request.method,
|
|
65
|
-
request_url=str(request.url.path),
|
|
66
|
-
)
|
|
47
|
+
path: Request URL path
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
if
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
content={
|
|
79
|
-
"error": {
|
|
80
|
-
"type": exc.error_type,
|
|
81
|
-
"message": str(exc),
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
@app.exception_handler(ValidationError)
|
|
87
|
-
async def validation_error_handler(
|
|
88
|
-
request: Request, exc: ValidationError
|
|
89
|
-
) -> JSONResponse:
|
|
90
|
-
"""Handle validation errors."""
|
|
91
|
-
# Store status code in request state for access logging
|
|
92
|
-
if hasattr(request.state, "context") and hasattr(
|
|
93
|
-
request.state.context, "metadata"
|
|
94
|
-
):
|
|
95
|
-
request.state.context.metadata["status_code"] = 400
|
|
96
|
-
|
|
97
|
-
logger.error(
|
|
98
|
-
"Validation error",
|
|
99
|
-
error_type="validation_error",
|
|
100
|
-
error_message=str(exc),
|
|
101
|
-
status_code=400,
|
|
102
|
-
request_method=request.method,
|
|
103
|
-
request_url=str(request.url.path),
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
# Record error in metrics
|
|
107
|
-
if metrics:
|
|
108
|
-
metrics.record_error(
|
|
109
|
-
error_type="validation_error",
|
|
110
|
-
endpoint=str(request.url.path),
|
|
111
|
-
model=None,
|
|
112
|
-
service_type="middleware",
|
|
113
|
-
)
|
|
114
|
-
return JSONResponse(
|
|
115
|
-
status_code=400,
|
|
116
|
-
content={
|
|
117
|
-
"error": {
|
|
118
|
-
"type": "validation_error",
|
|
119
|
-
"message": str(exc),
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
@app.exception_handler(AuthenticationError)
|
|
125
|
-
async def authentication_error_handler(
|
|
126
|
-
request: Request, exc: AuthenticationError
|
|
127
|
-
) -> JSONResponse:
|
|
128
|
-
"""Handle authentication errors."""
|
|
129
|
-
logger.error(
|
|
130
|
-
"Authentication error",
|
|
131
|
-
error_type="authentication_error",
|
|
132
|
-
error_message=str(exc),
|
|
133
|
-
status_code=401,
|
|
134
|
-
request_method=request.method,
|
|
135
|
-
request_url=str(request.url.path),
|
|
136
|
-
client_ip=request.client.host if request.client else "unknown",
|
|
137
|
-
user_agent=request.headers.get("user-agent", "unknown"),
|
|
138
|
-
)
|
|
49
|
+
Returns:
|
|
50
|
+
Detected format or None if cannot determine
|
|
51
|
+
"""
|
|
52
|
+
if "/chat/completions" in path:
|
|
53
|
+
return FORMAT_OPENAI_CHAT
|
|
54
|
+
elif "/messages" in path:
|
|
55
|
+
return FORMAT_ANTHROPIC_MESSAGES
|
|
56
|
+
elif "/responses" in path:
|
|
57
|
+
return FORMAT_OPENAI_RESPONSES
|
|
58
|
+
return None
|
|
139
59
|
|
|
140
|
-
# Record error in metrics
|
|
141
|
-
if metrics:
|
|
142
|
-
metrics.record_error(
|
|
143
|
-
error_type="authentication_error",
|
|
144
|
-
endpoint=str(request.url.path),
|
|
145
|
-
model=None,
|
|
146
|
-
service_type="middleware",
|
|
147
|
-
)
|
|
148
|
-
return JSONResponse(
|
|
149
|
-
status_code=401,
|
|
150
|
-
content={
|
|
151
|
-
"error": {
|
|
152
|
-
"type": "authentication_error",
|
|
153
|
-
"message": str(exc),
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
)
|
|
157
60
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
"""Handle permission errors."""
|
|
163
|
-
logger.error(
|
|
164
|
-
"Permission error",
|
|
165
|
-
error_type="permission_error",
|
|
166
|
-
error_message=str(exc),
|
|
167
|
-
status_code=403,
|
|
168
|
-
request_method=request.method,
|
|
169
|
-
request_url=str(request.url.path),
|
|
170
|
-
client_ip=request.client.host if request.client else "unknown",
|
|
171
|
-
)
|
|
61
|
+
def _get_format_aware_error_content(
|
|
62
|
+
error_type: str, message: str, status_code: int, base_format: str | None
|
|
63
|
+
) -> dict[str, Any]:
|
|
64
|
+
"""Create format-aware error response content using proper models.
|
|
172
65
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
model=None,
|
|
179
|
-
service_type="middleware",
|
|
180
|
-
)
|
|
181
|
-
return JSONResponse(
|
|
182
|
-
status_code=403,
|
|
183
|
-
content={
|
|
184
|
-
"error": {
|
|
185
|
-
"type": "permission_error",
|
|
186
|
-
"message": str(exc),
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
)
|
|
66
|
+
Args:
|
|
67
|
+
error_type: Type of error for logging
|
|
68
|
+
message: Error message
|
|
69
|
+
status_code: HTTP status code
|
|
70
|
+
base_format: Base format from format_chain[0]
|
|
190
71
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
request_method=request.method,
|
|
202
|
-
request_url=str(request.url.path),
|
|
203
|
-
)
|
|
72
|
+
Returns:
|
|
73
|
+
Formatted error response content using proper models
|
|
74
|
+
"""
|
|
75
|
+
# Default CCProxy format
|
|
76
|
+
default_content = {
|
|
77
|
+
"error": {
|
|
78
|
+
"type": error_type,
|
|
79
|
+
"message": message,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
204
82
|
|
|
205
|
-
|
|
206
|
-
if
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
83
|
+
try:
|
|
84
|
+
if base_format in {FORMAT_OPENAI_CHAT, FORMAT_OPENAI_RESPONSES}:
|
|
85
|
+
# Use OpenAI error model
|
|
86
|
+
error_detail = openai_models.ErrorDetail(
|
|
87
|
+
message=message,
|
|
88
|
+
type=error_type,
|
|
89
|
+
code=error_type
|
|
90
|
+
if base_format == FORMAT_OPENAI_RESPONSES
|
|
91
|
+
else str(status_code),
|
|
92
|
+
param=None,
|
|
212
93
|
)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
content={
|
|
216
|
-
"error": {
|
|
217
|
-
"type": "not_found_error",
|
|
218
|
-
"message": str(exc),
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
)
|
|
94
|
+
error_response = openai_models.ErrorResponse(error=error_detail)
|
|
95
|
+
return error_response.model_dump()
|
|
222
96
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
"Rate limit error",
|
|
230
|
-
error_type="rate_limit_error",
|
|
231
|
-
error_message=str(exc),
|
|
232
|
-
status_code=429,
|
|
233
|
-
request_method=request.method,
|
|
234
|
-
request_url=str(request.url.path),
|
|
235
|
-
client_ip=request.client.host if request.client else "unknown",
|
|
236
|
-
)
|
|
97
|
+
elif base_format == FORMAT_ANTHROPIC_MESSAGES:
|
|
98
|
+
# Use Anthropic error model
|
|
99
|
+
# APIError has a fixed type field, so create a generic ErrorDetail instead
|
|
100
|
+
api_error = anthropic_models.ErrorDetail(message=message)
|
|
101
|
+
# Anthropic error format has 'type': 'error' at top level
|
|
102
|
+
return {"type": "error", "error": api_error.model_dump()}
|
|
237
103
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return JSONResponse(
|
|
247
|
-
status_code=429,
|
|
248
|
-
content={
|
|
249
|
-
"error": {
|
|
250
|
-
"type": "rate_limit_error",
|
|
251
|
-
"message": str(exc),
|
|
252
|
-
}
|
|
253
|
-
},
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Log the error but don't fail - fallback to default format
|
|
106
|
+
logger.warning(
|
|
107
|
+
"format_aware_error_creation_failed",
|
|
108
|
+
base_format=base_format,
|
|
109
|
+
error_type=error_type,
|
|
110
|
+
fallback_reason=str(e),
|
|
111
|
+
category="middleware",
|
|
254
112
|
)
|
|
255
113
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
request: Request, exc: ModelNotFoundError
|
|
259
|
-
) -> JSONResponse:
|
|
260
|
-
"""Handle model not found errors."""
|
|
261
|
-
logger.error(
|
|
262
|
-
"Model not found error",
|
|
263
|
-
error_type="model_not_found_error",
|
|
264
|
-
error_message=str(exc),
|
|
265
|
-
status_code=404,
|
|
266
|
-
request_method=request.method,
|
|
267
|
-
request_url=str(request.url.path),
|
|
268
|
-
)
|
|
114
|
+
# Fallback to default format
|
|
115
|
+
return default_content
|
|
269
116
|
|
|
270
|
-
# Record error in metrics
|
|
271
|
-
if metrics:
|
|
272
|
-
metrics.record_error(
|
|
273
|
-
error_type="model_not_found_error",
|
|
274
|
-
endpoint=str(request.url.path),
|
|
275
|
-
model=None,
|
|
276
|
-
service_type="middleware",
|
|
277
|
-
)
|
|
278
|
-
return JSONResponse(
|
|
279
|
-
status_code=404,
|
|
280
|
-
content={
|
|
281
|
-
"error": {
|
|
282
|
-
"type": "model_not_found_error",
|
|
283
|
-
"message": str(exc),
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
@app.exception_handler(TimeoutError)
|
|
289
|
-
async def timeout_error_handler(
|
|
290
|
-
request: Request, exc: TimeoutError
|
|
291
|
-
) -> JSONResponse:
|
|
292
|
-
"""Handle timeout errors."""
|
|
293
|
-
logger.error(
|
|
294
|
-
"Timeout error",
|
|
295
|
-
error_type="timeout_error",
|
|
296
|
-
error_message=str(exc),
|
|
297
|
-
status_code=408,
|
|
298
|
-
request_method=request.method,
|
|
299
|
-
request_url=str(request.url.path),
|
|
300
|
-
)
|
|
301
117
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
metrics.record_error(
|
|
305
|
-
error_type="timeout_error",
|
|
306
|
-
endpoint=str(request.url.path),
|
|
307
|
-
model=None,
|
|
308
|
-
service_type="middleware",
|
|
309
|
-
)
|
|
310
|
-
return JSONResponse(
|
|
311
|
-
status_code=408,
|
|
312
|
-
content={
|
|
313
|
-
"error": {
|
|
314
|
-
"type": "timeout_error",
|
|
315
|
-
"message": str(exc),
|
|
316
|
-
}
|
|
317
|
-
},
|
|
318
|
-
)
|
|
118
|
+
def setup_error_handlers(app: FastAPI) -> None:
|
|
119
|
+
"""Setup error handlers for the FastAPI application.
|
|
319
120
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
121
|
+
Args:
|
|
122
|
+
app: FastAPI application instance
|
|
123
|
+
"""
|
|
124
|
+
logger.debug("error_handlers_setup_start", category="lifecycle")
|
|
125
|
+
|
|
126
|
+
# Metrics are now handled by the metrics plugin via hooks
|
|
127
|
+
metrics = None
|
|
128
|
+
|
|
129
|
+
# Define error type mappings with status codes and error types
|
|
130
|
+
ERROR_MAPPINGS: dict[type[Exception], tuple[int | None, str]] = {
|
|
131
|
+
ClaudeProxyError: (None, "claude_proxy_error"), # Uses exc.status_code
|
|
132
|
+
ValidationError: (400, "validation_error"),
|
|
133
|
+
AuthenticationError: (401, "authentication_error"),
|
|
134
|
+
ProxyAuthenticationError: (401, "proxy_authentication_error"),
|
|
135
|
+
PermissionError: (403, "permission_error"),
|
|
136
|
+
NotFoundError: (404, "not_found_error"),
|
|
137
|
+
ModelNotFoundError: (404, "model_not_found_error"),
|
|
138
|
+
TimeoutError: (408, "timeout_error"),
|
|
139
|
+
RateLimitError: (429, "rate_limit_error"),
|
|
140
|
+
ProxyError: (500, "proxy_error"),
|
|
141
|
+
TransformationError: (500, "transformation_error"),
|
|
142
|
+
MiddlewareError: (500, "middleware_error"),
|
|
143
|
+
DockerError: (500, "docker_error"),
|
|
144
|
+
ProxyConnectionError: (502, "proxy_connection_error"),
|
|
145
|
+
ServiceUnavailableError: (503, "service_unavailable_error"),
|
|
146
|
+
ProxyTimeoutError: (504, "proxy_timeout_error"),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async def unified_error_handler(
|
|
150
|
+
request: Request,
|
|
151
|
+
exc: Exception,
|
|
152
|
+
status_code: int | None = None,
|
|
153
|
+
error_type: str | None = None,
|
|
154
|
+
include_client_info: bool = False,
|
|
323
155
|
) -> JSONResponse:
|
|
324
|
-
"""
|
|
325
|
-
logger.error(
|
|
326
|
-
"Service unavailable error",
|
|
327
|
-
error_type="service_unavailable_error",
|
|
328
|
-
error_message=str(exc),
|
|
329
|
-
status_code=503,
|
|
330
|
-
request_method=request.method,
|
|
331
|
-
request_url=str(request.url.path),
|
|
332
|
-
)
|
|
156
|
+
"""Unified error handler for all exception types.
|
|
333
157
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
status_code=
|
|
344
|
-
content={
|
|
345
|
-
"error": {
|
|
346
|
-
"type": "service_unavailable_error",
|
|
347
|
-
"message": str(exc),
|
|
348
|
-
}
|
|
349
|
-
},
|
|
350
|
-
)
|
|
158
|
+
Args:
|
|
159
|
+
request: The incoming request
|
|
160
|
+
exc: The exception that was raised
|
|
161
|
+
status_code: HTTP status code to return
|
|
162
|
+
error_type: Type of error for logging and response
|
|
163
|
+
include_client_info: Whether to include client IP in logs
|
|
164
|
+
"""
|
|
165
|
+
# Get status code from exception if it has one
|
|
166
|
+
if status_code is None:
|
|
167
|
+
status_code = getattr(exc, "status_code", 500)
|
|
351
168
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
logger.error(
|
|
356
|
-
"Docker error",
|
|
357
|
-
error_type="docker_error",
|
|
358
|
-
error_message=str(exc),
|
|
359
|
-
status_code=500,
|
|
360
|
-
request_method=request.method,
|
|
361
|
-
request_url=str(request.url.path),
|
|
362
|
-
)
|
|
169
|
+
# Determine error type if not provided
|
|
170
|
+
if error_type is None:
|
|
171
|
+
error_type = getattr(exc, "error_type", "unknown_error")
|
|
363
172
|
|
|
364
|
-
#
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
error_type="docker_error",
|
|
368
|
-
endpoint=str(request.url.path),
|
|
369
|
-
model=None,
|
|
370
|
-
service_type="middleware",
|
|
371
|
-
)
|
|
372
|
-
return JSONResponse(
|
|
373
|
-
status_code=500,
|
|
374
|
-
content={
|
|
375
|
-
"error": {
|
|
376
|
-
"type": "docker_error",
|
|
377
|
-
"message": str(exc),
|
|
378
|
-
}
|
|
379
|
-
},
|
|
173
|
+
# Get request ID from request state or headers
|
|
174
|
+
request_id = getattr(request.state, "request_id", None) or request.headers.get(
|
|
175
|
+
"x-request-id"
|
|
380
176
|
)
|
|
381
177
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
status_code=500,
|
|
405
|
-
content={
|
|
406
|
-
"error": {
|
|
407
|
-
"type": "proxy_error",
|
|
408
|
-
"message": str(exc),
|
|
409
|
-
}
|
|
410
|
-
},
|
|
411
|
-
)
|
|
412
|
-
|
|
413
|
-
@app.exception_handler(TransformationError)
|
|
414
|
-
async def transformation_error_handler(
|
|
415
|
-
request: Request, exc: TransformationError
|
|
416
|
-
) -> JSONResponse:
|
|
417
|
-
"""Handle transformation errors."""
|
|
418
|
-
logger.error(
|
|
419
|
-
"Transformation error",
|
|
420
|
-
error_type="transformation_error",
|
|
421
|
-
error_message=str(exc),
|
|
422
|
-
status_code=500,
|
|
423
|
-
request_method=request.method,
|
|
424
|
-
request_url=str(request.url.path),
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
# Record error in metrics
|
|
428
|
-
if metrics:
|
|
429
|
-
metrics.record_error(
|
|
430
|
-
error_type="transformation_error",
|
|
431
|
-
endpoint=str(request.url.path),
|
|
432
|
-
model=None,
|
|
433
|
-
service_type="middleware",
|
|
434
|
-
)
|
|
435
|
-
return JSONResponse(
|
|
436
|
-
status_code=500,
|
|
437
|
-
content={
|
|
438
|
-
"error": {
|
|
439
|
-
"type": "transformation_error",
|
|
440
|
-
"message": str(exc),
|
|
441
|
-
}
|
|
442
|
-
},
|
|
443
|
-
)
|
|
444
|
-
|
|
445
|
-
@app.exception_handler(MiddlewareError)
|
|
446
|
-
async def middleware_error_handler(
|
|
447
|
-
request: Request, exc: MiddlewareError
|
|
448
|
-
) -> JSONResponse:
|
|
449
|
-
"""Handle middleware errors."""
|
|
178
|
+
# Store status code in request state for access logging
|
|
179
|
+
if hasattr(request.state, "context") and hasattr(
|
|
180
|
+
request.state.context, "metadata"
|
|
181
|
+
):
|
|
182
|
+
request.state.context.metadata["status_code"] = status_code
|
|
183
|
+
|
|
184
|
+
# Build log kwargs
|
|
185
|
+
log_kwargs = {
|
|
186
|
+
"error_type": error_type,
|
|
187
|
+
"error_message": str(exc),
|
|
188
|
+
"status_code": status_code,
|
|
189
|
+
"request_method": request.method,
|
|
190
|
+
"request_url": str(request.url.path),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Add client info if needed (for auth errors)
|
|
194
|
+
if include_client_info and request.client:
|
|
195
|
+
log_kwargs["client_ip"] = request.client.host
|
|
196
|
+
if error_type in ("authentication_error", "proxy_authentication_error"):
|
|
197
|
+
log_kwargs["user_agent"] = request.headers.get("user-agent", "unknown")
|
|
198
|
+
|
|
199
|
+
# Log the error
|
|
450
200
|
logger.error(
|
|
451
|
-
"
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
status_code=500,
|
|
455
|
-
request_method=request.method,
|
|
456
|
-
request_url=str(request.url.path),
|
|
201
|
+
f"{error_type.replace('_', ' ').title()}",
|
|
202
|
+
**log_kwargs,
|
|
203
|
+
category="middleware",
|
|
457
204
|
)
|
|
458
205
|
|
|
459
206
|
# Record error in metrics
|
|
460
207
|
if metrics:
|
|
461
208
|
metrics.record_error(
|
|
462
|
-
error_type=
|
|
209
|
+
error_type=error_type,
|
|
463
210
|
endpoint=str(request.url.path),
|
|
464
211
|
model=None,
|
|
465
212
|
service_type="middleware",
|
|
466
213
|
)
|
|
467
|
-
return JSONResponse(
|
|
468
|
-
status_code=500,
|
|
469
|
-
content={
|
|
470
|
-
"error": {
|
|
471
|
-
"type": "middleware_error",
|
|
472
|
-
"message": str(exc),
|
|
473
|
-
}
|
|
474
|
-
},
|
|
475
|
-
)
|
|
476
214
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
215
|
+
# Prepare headers with x-request-id if available
|
|
216
|
+
headers = {}
|
|
217
|
+
if request_id:
|
|
218
|
+
headers["x-request-id"] = request_id
|
|
219
|
+
|
|
220
|
+
# Detect format from request context for format-aware error responses
|
|
221
|
+
base_format = None
|
|
222
|
+
try:
|
|
223
|
+
if hasattr(request.state, "context") and hasattr(
|
|
224
|
+
request.state.context, "format_chain"
|
|
225
|
+
):
|
|
226
|
+
format_chain = request.state.context.format_chain
|
|
227
|
+
if format_chain and len(format_chain) > 0:
|
|
228
|
+
base_format = format_chain[
|
|
229
|
+
0
|
|
230
|
+
] # First format is the client's expected format
|
|
231
|
+
logger.debug(
|
|
232
|
+
"format_aware_error_detected",
|
|
233
|
+
base_format=base_format,
|
|
234
|
+
format_chain=format_chain,
|
|
235
|
+
category="middleware",
|
|
236
|
+
)
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.debug("format_detection_failed", error=str(e), category="middleware")
|
|
239
|
+
|
|
240
|
+
# Get format-aware error content
|
|
241
|
+
error_content = _get_format_aware_error_content(
|
|
242
|
+
error_type=error_type,
|
|
243
|
+
message=str(exc),
|
|
244
|
+
status_code=status_code,
|
|
245
|
+
base_format=base_format,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Return JSON response with format-aware content
|
|
499
249
|
return JSONResponse(
|
|
500
|
-
status_code=
|
|
501
|
-
content=
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
250
|
+
status_code=status_code,
|
|
251
|
+
content=error_content,
|
|
252
|
+
headers=headers,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Register specific error handlers using the unified handler
|
|
256
|
+
for exc_class, (status, err_type) in ERROR_MAPPINGS.items():
|
|
257
|
+
# Determine if this error type should include client info
|
|
258
|
+
include_client = err_type in (
|
|
259
|
+
"authentication_error",
|
|
260
|
+
"proxy_authentication_error",
|
|
261
|
+
"permission_error",
|
|
262
|
+
"rate_limit_error",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Create a closure to capture the specific error configuration
|
|
266
|
+
def make_handler(
|
|
267
|
+
status_code: int | None, error_type: str, include_client_info: bool
|
|
268
|
+
) -> Callable[[Request, Exception], Awaitable[JSONResponse]]:
|
|
269
|
+
async def handler(request: Request, exc: Exception) -> JSONResponse:
|
|
270
|
+
return await unified_error_handler(
|
|
271
|
+
request, exc, status_code, error_type, include_client_info
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
return handler
|
|
275
|
+
|
|
276
|
+
# Register the handler
|
|
277
|
+
app.exception_handler(exc_class)(make_handler(status, err_type, include_client))
|
|
278
|
+
|
|
279
|
+
# FastAPI validation errors
|
|
280
|
+
@app.exception_handler(RequestValidationError)
|
|
281
|
+
async def validation_exception_handler(
|
|
282
|
+
request: Request, exc: RequestValidationError
|
|
512
283
|
) -> JSONResponse:
|
|
513
|
-
"""Handle
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
284
|
+
"""Handle FastAPI request validation errors with format awareness."""
|
|
285
|
+
# Get request ID from request state or headers
|
|
286
|
+
request_id = getattr(request.state, "request_id", None) or request.headers.get(
|
|
287
|
+
"x-request-id"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Try to get format from request context (set by middleware)
|
|
291
|
+
base_format = None
|
|
292
|
+
try:
|
|
293
|
+
if hasattr(request.state, "context") and hasattr(
|
|
294
|
+
request.state.context, "format_chain"
|
|
295
|
+
):
|
|
296
|
+
format_chain = request.state.context.format_chain
|
|
297
|
+
if format_chain and len(format_chain) > 0:
|
|
298
|
+
base_format = format_chain[0]
|
|
299
|
+
except Exception:
|
|
300
|
+
pass # Fallback to path detection if needed
|
|
301
|
+
|
|
302
|
+
# Fallback: detect format from path if context isn't available
|
|
303
|
+
if base_format is None:
|
|
304
|
+
base_format = _detect_format_from_path(str(request.url.path))
|
|
305
|
+
|
|
306
|
+
# Create a readable error message from validation errors
|
|
307
|
+
error_details = []
|
|
308
|
+
for error in exc.errors():
|
|
309
|
+
loc = " -> ".join(str(x) for x in error["loc"])
|
|
310
|
+
error_details.append(f"{loc}: {error['msg']}")
|
|
311
|
+
|
|
312
|
+
error_message = "; ".join(error_details)
|
|
313
|
+
|
|
314
|
+
# Log the validation error
|
|
315
|
+
logger.warning(
|
|
316
|
+
"Request validation error",
|
|
317
|
+
error_type="validation_error",
|
|
318
|
+
error_message=error_message,
|
|
319
|
+
status_code=422,
|
|
519
320
|
request_method=request.method,
|
|
520
321
|
request_url=str(request.url.path),
|
|
322
|
+
base_format=base_format,
|
|
323
|
+
category="middleware",
|
|
521
324
|
)
|
|
522
325
|
|
|
523
|
-
#
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
endpoint=str(request.url.path),
|
|
528
|
-
model=None,
|
|
529
|
-
service_type="middleware",
|
|
530
|
-
)
|
|
531
|
-
return JSONResponse(
|
|
532
|
-
status_code=504,
|
|
533
|
-
content={
|
|
534
|
-
"error": {
|
|
535
|
-
"type": "proxy_timeout_error",
|
|
536
|
-
"message": str(exc),
|
|
537
|
-
}
|
|
538
|
-
},
|
|
539
|
-
)
|
|
326
|
+
# Prepare headers with x-request-id if available
|
|
327
|
+
headers = {}
|
|
328
|
+
if request_id:
|
|
329
|
+
headers["x-request-id"] = request_id
|
|
540
330
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
"Proxy authentication error",
|
|
548
|
-
error_type="proxy_authentication_error",
|
|
549
|
-
error_message=str(exc),
|
|
550
|
-
status_code=401,
|
|
551
|
-
request_method=request.method,
|
|
552
|
-
request_url=str(request.url.path),
|
|
553
|
-
client_ip=request.client.host if request.client else "unknown",
|
|
331
|
+
# Get format-aware error content
|
|
332
|
+
error_content = _get_format_aware_error_content(
|
|
333
|
+
error_type="validation_error",
|
|
334
|
+
message=error_message,
|
|
335
|
+
status_code=422,
|
|
336
|
+
base_format=base_format,
|
|
554
337
|
)
|
|
555
338
|
|
|
556
|
-
# Record error in metrics
|
|
557
|
-
if metrics:
|
|
558
|
-
metrics.record_error(
|
|
559
|
-
error_type="proxy_authentication_error",
|
|
560
|
-
endpoint=str(request.url.path),
|
|
561
|
-
model=None,
|
|
562
|
-
service_type="middleware",
|
|
563
|
-
)
|
|
564
339
|
return JSONResponse(
|
|
565
|
-
status_code=
|
|
566
|
-
content=
|
|
567
|
-
|
|
568
|
-
"type": "proxy_authentication_error",
|
|
569
|
-
"message": str(exc),
|
|
570
|
-
}
|
|
571
|
-
},
|
|
340
|
+
status_code=422,
|
|
341
|
+
content=error_content,
|
|
342
|
+
headers=headers,
|
|
572
343
|
)
|
|
573
344
|
|
|
574
345
|
# Standard HTTP exceptions
|
|
@@ -577,6 +348,11 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
577
348
|
request: Request, exc: HTTPException
|
|
578
349
|
) -> JSONResponse:
|
|
579
350
|
"""Handle HTTP exceptions."""
|
|
351
|
+
# Get request ID from request state or headers
|
|
352
|
+
request_id = getattr(request.state, "request_id", None) or request.headers.get(
|
|
353
|
+
"x-request-id"
|
|
354
|
+
)
|
|
355
|
+
|
|
580
356
|
# Store status code in request state for access logging
|
|
581
357
|
if hasattr(request.state, "context") and hasattr(
|
|
582
358
|
request.state.context, "metadata"
|
|
@@ -585,7 +361,6 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
585
361
|
|
|
586
362
|
# Don't log stack trace for expected errors (404, 401)
|
|
587
363
|
if exc.status_code in (404, 401):
|
|
588
|
-
log_level = "debug" if exc.status_code == 404 else "warning"
|
|
589
364
|
log_func = logger.debug if exc.status_code == 404 else logger.warning
|
|
590
365
|
|
|
591
366
|
log_func(
|
|
@@ -595,13 +370,10 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
595
370
|
status_code=exc.status_code,
|
|
596
371
|
request_method=request.method,
|
|
597
372
|
request_url=str(request.url.path),
|
|
373
|
+
category="middleware",
|
|
598
374
|
)
|
|
599
375
|
else:
|
|
600
376
|
# Log with basic stack trace (no local variables)
|
|
601
|
-
stack_trace = None
|
|
602
|
-
# For structlog, we can always include traceback since structlog handles filtering
|
|
603
|
-
import traceback
|
|
604
|
-
|
|
605
377
|
stack_trace = traceback.format_exc(limit=5) # Limit to 5 frames
|
|
606
378
|
|
|
607
379
|
logger.error(
|
|
@@ -612,6 +384,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
612
384
|
request_method=request.method,
|
|
613
385
|
request_url=str(request.url.path),
|
|
614
386
|
stack_trace=stack_trace,
|
|
387
|
+
category="middleware",
|
|
615
388
|
)
|
|
616
389
|
|
|
617
390
|
# Record error in metrics
|
|
@@ -629,15 +402,43 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
629
402
|
service_type="middleware",
|
|
630
403
|
)
|
|
631
404
|
|
|
632
|
-
#
|
|
405
|
+
# Prepare headers with x-request-id if available
|
|
406
|
+
headers = {}
|
|
407
|
+
if request_id:
|
|
408
|
+
headers["x-request-id"] = request_id
|
|
409
|
+
|
|
410
|
+
# Detect format from request context for format-aware error responses
|
|
411
|
+
base_format = None
|
|
412
|
+
try:
|
|
413
|
+
if hasattr(request.state, "context") and hasattr(
|
|
414
|
+
request.state.context, "format_chain"
|
|
415
|
+
):
|
|
416
|
+
format_chain = request.state.context.format_chain
|
|
417
|
+
if format_chain and len(format_chain) > 0:
|
|
418
|
+
base_format = format_chain[0]
|
|
419
|
+
except Exception:
|
|
420
|
+
pass # Ignore format detection errors
|
|
421
|
+
|
|
422
|
+
# Determine error type for format-aware response
|
|
423
|
+
if exc.status_code == 404:
|
|
424
|
+
error_type = "not_found"
|
|
425
|
+
elif exc.status_code == 401:
|
|
426
|
+
error_type = "authentication_error"
|
|
427
|
+
else:
|
|
428
|
+
error_type = "http_error"
|
|
429
|
+
|
|
430
|
+
# Get format-aware error content
|
|
431
|
+
error_content = _get_format_aware_error_content(
|
|
432
|
+
error_type=error_type,
|
|
433
|
+
message=exc.detail,
|
|
434
|
+
status_code=exc.status_code,
|
|
435
|
+
base_format=base_format,
|
|
436
|
+
)
|
|
437
|
+
|
|
633
438
|
return JSONResponse(
|
|
634
439
|
status_code=exc.status_code,
|
|
635
|
-
content=
|
|
636
|
-
|
|
637
|
-
"type": "http_error",
|
|
638
|
-
"message": exc.detail,
|
|
639
|
-
}
|
|
640
|
-
},
|
|
440
|
+
content=error_content,
|
|
441
|
+
headers=headers,
|
|
641
442
|
)
|
|
642
443
|
|
|
643
444
|
@app.exception_handler(StarletteHTTPException)
|
|
@@ -645,6 +446,11 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
645
446
|
request: Request, exc: StarletteHTTPException
|
|
646
447
|
) -> JSONResponse:
|
|
647
448
|
"""Handle Starlette HTTP exceptions."""
|
|
449
|
+
# Get request ID from request state or headers
|
|
450
|
+
request_id = getattr(request.state, "request_id", None) or request.headers.get(
|
|
451
|
+
"x-request-id"
|
|
452
|
+
)
|
|
453
|
+
|
|
648
454
|
# Don't log stack trace for 404 errors as they're expected
|
|
649
455
|
if exc.status_code == 404:
|
|
650
456
|
logger.debug(
|
|
@@ -654,6 +460,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
654
460
|
status_code=404,
|
|
655
461
|
request_method=request.method,
|
|
656
462
|
request_url=str(request.url.path),
|
|
463
|
+
category="middleware",
|
|
657
464
|
)
|
|
658
465
|
else:
|
|
659
466
|
logger.error(
|
|
@@ -663,6 +470,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
663
470
|
status_code=exc.status_code,
|
|
664
471
|
request_method=request.method,
|
|
665
472
|
request_url=str(request.url.path),
|
|
473
|
+
category="middleware",
|
|
666
474
|
)
|
|
667
475
|
|
|
668
476
|
# Record error in metrics
|
|
@@ -678,14 +486,42 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
678
486
|
model=None,
|
|
679
487
|
service_type="middleware",
|
|
680
488
|
)
|
|
489
|
+
|
|
490
|
+
# Prepare headers with x-request-id if available
|
|
491
|
+
headers = {}
|
|
492
|
+
if request_id:
|
|
493
|
+
headers["x-request-id"] = request_id
|
|
494
|
+
|
|
495
|
+
# Detect format from request context for format-aware error responses
|
|
496
|
+
base_format = None
|
|
497
|
+
try:
|
|
498
|
+
if hasattr(request.state, "context") and hasattr(
|
|
499
|
+
request.state.context, "format_chain"
|
|
500
|
+
):
|
|
501
|
+
format_chain = request.state.context.format_chain
|
|
502
|
+
if format_chain and len(format_chain) > 0:
|
|
503
|
+
base_format = format_chain[0]
|
|
504
|
+
except Exception:
|
|
505
|
+
pass # Ignore format detection errors
|
|
506
|
+
|
|
507
|
+
# Determine error type for format-aware response
|
|
508
|
+
if exc.status_code == 404:
|
|
509
|
+
error_type = "not_found"
|
|
510
|
+
else:
|
|
511
|
+
error_type = "http_error"
|
|
512
|
+
|
|
513
|
+
# Get format-aware error content
|
|
514
|
+
error_content = _get_format_aware_error_content(
|
|
515
|
+
error_type=error_type,
|
|
516
|
+
message=exc.detail,
|
|
517
|
+
status_code=exc.status_code,
|
|
518
|
+
base_format=base_format,
|
|
519
|
+
)
|
|
520
|
+
|
|
681
521
|
return JSONResponse(
|
|
682
522
|
status_code=exc.status_code,
|
|
683
|
-
content=
|
|
684
|
-
|
|
685
|
-
"type": "http_error",
|
|
686
|
-
"message": exc.detail,
|
|
687
|
-
}
|
|
688
|
-
},
|
|
523
|
+
content=error_content,
|
|
524
|
+
headers=headers,
|
|
689
525
|
)
|
|
690
526
|
|
|
691
527
|
# Global exception handler
|
|
@@ -694,6 +530,11 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
694
530
|
request: Request, exc: Exception
|
|
695
531
|
) -> JSONResponse:
|
|
696
532
|
"""Handle all other unhandled exceptions."""
|
|
533
|
+
# Get request ID from request state or headers
|
|
534
|
+
request_id = getattr(request.state, "request_id", None) or request.headers.get(
|
|
535
|
+
"x-request-id"
|
|
536
|
+
)
|
|
537
|
+
|
|
697
538
|
# Store status code in request state for access logging
|
|
698
539
|
if hasattr(request.state, "context") and hasattr(
|
|
699
540
|
request.state.context, "metadata"
|
|
@@ -708,6 +549,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
708
549
|
request_method=request.method,
|
|
709
550
|
request_url=str(request.url.path),
|
|
710
551
|
exc_info=True,
|
|
552
|
+
category="middleware",
|
|
711
553
|
)
|
|
712
554
|
|
|
713
555
|
# Record error in metrics
|
|
@@ -718,14 +560,36 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
718
560
|
model=None,
|
|
719
561
|
service_type="middleware",
|
|
720
562
|
)
|
|
563
|
+
|
|
564
|
+
# Prepare headers with x-request-id if available
|
|
565
|
+
headers = {}
|
|
566
|
+
if request_id:
|
|
567
|
+
headers["x-request-id"] = request_id
|
|
568
|
+
|
|
569
|
+
# Detect format from request context for format-aware error responses
|
|
570
|
+
base_format = None
|
|
571
|
+
try:
|
|
572
|
+
if hasattr(request.state, "context") and hasattr(
|
|
573
|
+
request.state.context, "format_chain"
|
|
574
|
+
):
|
|
575
|
+
format_chain = request.state.context.format_chain
|
|
576
|
+
if format_chain and len(format_chain) > 0:
|
|
577
|
+
base_format = format_chain[0]
|
|
578
|
+
except Exception:
|
|
579
|
+
pass # Ignore format detection errors
|
|
580
|
+
|
|
581
|
+
# Get format-aware error content for internal server error
|
|
582
|
+
error_content = _get_format_aware_error_content(
|
|
583
|
+
error_type="internal_server_error",
|
|
584
|
+
message="An internal server error occurred",
|
|
585
|
+
status_code=500,
|
|
586
|
+
base_format=base_format,
|
|
587
|
+
)
|
|
588
|
+
|
|
721
589
|
return JSONResponse(
|
|
722
590
|
status_code=500,
|
|
723
|
-
content=
|
|
724
|
-
|
|
725
|
-
"type": "internal_server_error",
|
|
726
|
-
"message": "An internal server error occurred",
|
|
727
|
-
}
|
|
728
|
-
},
|
|
591
|
+
content=error_content,
|
|
592
|
+
headers=headers,
|
|
729
593
|
)
|
|
730
594
|
|
|
731
|
-
logger.debug("error_handlers_setup_completed")
|
|
595
|
+
logger.debug("error_handlers_setup_completed", category="lifecycle")
|