ccproxy-api 0.1.6__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 +439 -212
- ccproxy/api/bootstrap.py +30 -0
- ccproxy/api/decorators.py +85 -0
- ccproxy/api/dependencies.py +145 -176
- ccproxy/api/format_validation.py +54 -0
- ccproxy/api/middleware/cors.py +6 -3
- ccproxy/api/middleware/errors.py +402 -530
- 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 +558 -0
- ccproxy/data/codex_headers_fallback.json +121 -0
- 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 +63 -107
- ccproxy/scheduler/registry.py +6 -32
- ccproxy/scheduler/tasks.py +346 -314
- 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 +95 -342
- ccproxy/utils/version_checker.py +279 -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.6.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 -1231
- 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 -269
- ccproxy/services/codex_detection_service.py +0 -263
- 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.6.dist-info/METADATA +0 -615
- ccproxy_api-0.1.6.dist-info/RECORD +0 -189
- ccproxy_api-0.1.6.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.6.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
|
|
47
|
+
path: Request URL path
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if metrics:
|
|
70
|
-
metrics.record_error(
|
|
71
|
-
error_type="claude_proxy_error",
|
|
72
|
-
endpoint=str(request.url.path),
|
|
73
|
-
model=None,
|
|
74
|
-
service_type="middleware",
|
|
75
|
-
)
|
|
76
|
-
return JSONResponse(
|
|
77
|
-
status_code=exc.status_code,
|
|
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
|
-
)
|
|
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
|
|
123
59
|
|
|
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
|
-
)
|
|
139
60
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
)
|
|
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.
|
|
157
65
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
)
|
|
172
|
-
|
|
173
|
-
# Record error in metrics
|
|
174
|
-
if metrics:
|
|
175
|
-
metrics.record_error(
|
|
176
|
-
error_type="permission_error",
|
|
177
|
-
endpoint=str(request.url.path),
|
|
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
|
-
)
|
|
237
|
-
|
|
238
|
-
# Record error in metrics
|
|
239
|
-
if metrics:
|
|
240
|
-
metrics.record_error(
|
|
241
|
-
error_type="rate_limit_error",
|
|
242
|
-
endpoint=str(request.url.path),
|
|
243
|
-
model=None,
|
|
244
|
-
service_type="middleware",
|
|
245
|
-
)
|
|
246
|
-
return JSONResponse(
|
|
247
|
-
status_code=429,
|
|
248
|
-
content={
|
|
249
|
-
"error": {
|
|
250
|
-
"type": "rate_limit_error",
|
|
251
|
-
"message": str(exc),
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
)
|
|
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()}
|
|
255
103
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
error_message=str(exc),
|
|
265
|
-
status_code=404,
|
|
266
|
-
request_method=request.method,
|
|
267
|
-
request_url=str(request.url.path),
|
|
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",
|
|
268
112
|
)
|
|
269
113
|
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
)
|
|
114
|
+
# Fallback to default format
|
|
115
|
+
return default_content
|
|
287
116
|
|
|
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
|
-
},
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
# Core proxy errors
|
|
383
|
-
@app.exception_handler(ProxyError)
|
|
384
|
-
async def proxy_error_handler(request: Request, exc: ProxyError) -> JSONResponse:
|
|
385
|
-
"""Handle proxy errors."""
|
|
386
|
-
logger.error(
|
|
387
|
-
"Proxy error",
|
|
388
|
-
error_type="proxy_error",
|
|
389
|
-
error_message=str(exc),
|
|
390
|
-
status_code=500,
|
|
391
|
-
request_method=request.method,
|
|
392
|
-
request_url=str(request.url.path),
|
|
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"
|
|
393
176
|
)
|
|
394
177
|
|
|
395
|
-
#
|
|
396
|
-
if
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
"""Handle transformation 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
|
|
418
200
|
logger.error(
|
|
419
|
-
"
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
status_code=500,
|
|
423
|
-
request_method=request.method,
|
|
424
|
-
request_url=str(request.url.path),
|
|
201
|
+
f"{error_type.replace('_', ' ').title()}",
|
|
202
|
+
**log_kwargs,
|
|
203
|
+
category="middleware",
|
|
425
204
|
)
|
|
426
205
|
|
|
427
206
|
# Record error in metrics
|
|
428
207
|
if metrics:
|
|
429
208
|
metrics.record_error(
|
|
430
|
-
error_type=
|
|
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."""
|
|
450
|
-
logger.error(
|
|
451
|
-
"Middleware error",
|
|
452
|
-
error_type="middleware_error",
|
|
453
|
-
error_message=str(exc),
|
|
454
|
-
status_code=500,
|
|
455
|
-
request_method=request.method,
|
|
456
|
-
request_url=str(request.url.path),
|
|
457
|
-
)
|
|
458
|
-
|
|
459
|
-
# Record error in metrics
|
|
460
|
-
if metrics:
|
|
461
|
-
metrics.record_error(
|
|
462
|
-
error_type="middleware_error",
|
|
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,28 +348,32 @@ 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"
|
|
583
359
|
):
|
|
584
360
|
request.state.context.metadata["status_code"] = exc.status_code
|
|
585
361
|
|
|
586
|
-
# Don't log stack trace for
|
|
587
|
-
if exc.status_code
|
|
588
|
-
logger.debug
|
|
589
|
-
|
|
590
|
-
|
|
362
|
+
# Don't log stack trace for expected errors (404, 401)
|
|
363
|
+
if exc.status_code in (404, 401):
|
|
364
|
+
log_func = logger.debug if exc.status_code == 404 else logger.warning
|
|
365
|
+
|
|
366
|
+
log_func(
|
|
367
|
+
f"HTTP {exc.status_code} error",
|
|
368
|
+
error_type=f"http_{exc.status_code}",
|
|
591
369
|
error_message=exc.detail,
|
|
592
|
-
status_code=
|
|
370
|
+
status_code=exc.status_code,
|
|
593
371
|
request_method=request.method,
|
|
594
372
|
request_url=str(request.url.path),
|
|
373
|
+
category="middleware",
|
|
595
374
|
)
|
|
596
375
|
else:
|
|
597
376
|
# Log with basic stack trace (no local variables)
|
|
598
|
-
stack_trace = None
|
|
599
|
-
# For structlog, we can always include traceback since structlog handles filtering
|
|
600
|
-
import traceback
|
|
601
|
-
|
|
602
377
|
stack_trace = traceback.format_exc(limit=5) # Limit to 5 frames
|
|
603
378
|
|
|
604
379
|
logger.error(
|
|
@@ -609,11 +384,17 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
609
384
|
request_method=request.method,
|
|
610
385
|
request_url=str(request.url.path),
|
|
611
386
|
stack_trace=stack_trace,
|
|
387
|
+
category="middleware",
|
|
612
388
|
)
|
|
613
389
|
|
|
614
390
|
# Record error in metrics
|
|
615
391
|
if metrics:
|
|
616
|
-
|
|
392
|
+
if exc.status_code == 404:
|
|
393
|
+
error_type = "http_404"
|
|
394
|
+
elif exc.status_code == 401:
|
|
395
|
+
error_type = "http_401"
|
|
396
|
+
else:
|
|
397
|
+
error_type = "http_error"
|
|
617
398
|
metrics.record_error(
|
|
618
399
|
error_type=error_type,
|
|
619
400
|
endpoint=str(request.url.path),
|
|
@@ -621,15 +402,43 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
621
402
|
service_type="middleware",
|
|
622
403
|
)
|
|
623
404
|
|
|
624
|
-
#
|
|
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
|
+
|
|
625
438
|
return JSONResponse(
|
|
626
439
|
status_code=exc.status_code,
|
|
627
|
-
content=
|
|
628
|
-
|
|
629
|
-
"type": "http_error",
|
|
630
|
-
"message": exc.detail,
|
|
631
|
-
}
|
|
632
|
-
},
|
|
440
|
+
content=error_content,
|
|
441
|
+
headers=headers,
|
|
633
442
|
)
|
|
634
443
|
|
|
635
444
|
@app.exception_handler(StarletteHTTPException)
|
|
@@ -637,6 +446,11 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
637
446
|
request: Request, exc: StarletteHTTPException
|
|
638
447
|
) -> JSONResponse:
|
|
639
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
|
+
|
|
640
454
|
# Don't log stack trace for 404 errors as they're expected
|
|
641
455
|
if exc.status_code == 404:
|
|
642
456
|
logger.debug(
|
|
@@ -646,6 +460,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
646
460
|
status_code=404,
|
|
647
461
|
request_method=request.method,
|
|
648
462
|
request_url=str(request.url.path),
|
|
463
|
+
category="middleware",
|
|
649
464
|
)
|
|
650
465
|
else:
|
|
651
466
|
logger.error(
|
|
@@ -655,6 +470,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
655
470
|
status_code=exc.status_code,
|
|
656
471
|
request_method=request.method,
|
|
657
472
|
request_url=str(request.url.path),
|
|
473
|
+
category="middleware",
|
|
658
474
|
)
|
|
659
475
|
|
|
660
476
|
# Record error in metrics
|
|
@@ -670,14 +486,42 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
670
486
|
model=None,
|
|
671
487
|
service_type="middleware",
|
|
672
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
|
+
|
|
673
521
|
return JSONResponse(
|
|
674
522
|
status_code=exc.status_code,
|
|
675
|
-
content=
|
|
676
|
-
|
|
677
|
-
"type": "http_error",
|
|
678
|
-
"message": exc.detail,
|
|
679
|
-
}
|
|
680
|
-
},
|
|
523
|
+
content=error_content,
|
|
524
|
+
headers=headers,
|
|
681
525
|
)
|
|
682
526
|
|
|
683
527
|
# Global exception handler
|
|
@@ -686,6 +530,11 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
686
530
|
request: Request, exc: Exception
|
|
687
531
|
) -> JSONResponse:
|
|
688
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
|
+
|
|
689
538
|
# Store status code in request state for access logging
|
|
690
539
|
if hasattr(request.state, "context") and hasattr(
|
|
691
540
|
request.state.context, "metadata"
|
|
@@ -700,6 +549,7 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
700
549
|
request_method=request.method,
|
|
701
550
|
request_url=str(request.url.path),
|
|
702
551
|
exc_info=True,
|
|
552
|
+
category="middleware",
|
|
703
553
|
)
|
|
704
554
|
|
|
705
555
|
# Record error in metrics
|
|
@@ -710,14 +560,36 @@ def setup_error_handlers(app: FastAPI) -> None:
|
|
|
710
560
|
model=None,
|
|
711
561
|
service_type="middleware",
|
|
712
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
|
+
|
|
713
589
|
return JSONResponse(
|
|
714
590
|
status_code=500,
|
|
715
|
-
content=
|
|
716
|
-
|
|
717
|
-
"type": "internal_server_error",
|
|
718
|
-
"message": "An internal server error occurred",
|
|
719
|
-
}
|
|
720
|
-
},
|
|
591
|
+
content=error_content,
|
|
592
|
+
headers=headers,
|
|
721
593
|
)
|
|
722
594
|
|
|
723
|
-
logger.debug("error_handlers_setup_completed")
|
|
595
|
+
logger.debug("error_handlers_setup_completed", category="lifecycle")
|