ccproxy-api 0.1.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.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,703 @@
1
+ """Error handling middleware for CCProxy API Server."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ import structlog
7
+ from fastapi import FastAPI, HTTPException, Request
8
+ from fastapi.responses import JSONResponse
9
+ from starlette.exceptions import HTTPException as StarletteHTTPException
10
+ from structlog import get_logger
11
+
12
+ from ccproxy.core.errors import (
13
+ AuthenticationError,
14
+ ClaudeProxyError,
15
+ DockerError,
16
+ MiddlewareError,
17
+ ModelNotFoundError,
18
+ NotFoundError,
19
+ PermissionError,
20
+ ProxyAuthenticationError,
21
+ ProxyConnectionError,
22
+ ProxyError,
23
+ ProxyTimeoutError,
24
+ RateLimitError,
25
+ ServiceUnavailableError,
26
+ TimeoutError,
27
+ TransformationError,
28
+ ValidationError,
29
+ )
30
+ from ccproxy.observability.metrics import get_metrics
31
+
32
+
33
+ logger = get_logger(__name__)
34
+
35
+
36
+ def setup_error_handlers(app: FastAPI) -> None:
37
+ """Setup error handlers for the FastAPI application.
38
+
39
+ Args:
40
+ app: FastAPI application instance
41
+ """
42
+ logger.debug("error_handlers_setup_start")
43
+
44
+ # Get metrics instance for error recording
45
+ try:
46
+ metrics = get_metrics()
47
+ logger.debug("error_handlers_metrics_loaded")
48
+ except Exception as e:
49
+ logger.warning("error_handlers_metrics_unavailable", error=str(e))
50
+ metrics = None
51
+
52
+ @app.exception_handler(ClaudeProxyError)
53
+ async def claude_proxy_error_handler(
54
+ request: Request, exc: ClaudeProxyError
55
+ ) -> JSONResponse:
56
+ """Handle Claude proxy specific errors."""
57
+ logger.error(
58
+ "Claude proxy error",
59
+ error_type="claude_proxy_error",
60
+ error_message=str(exc),
61
+ status_code=exc.status_code,
62
+ request_method=request.method,
63
+ request_url=str(request.url.path),
64
+ )
65
+
66
+ # Record error in metrics
67
+ if metrics:
68
+ metrics.record_error(
69
+ error_type="claude_proxy_error",
70
+ endpoint=str(request.url.path),
71
+ model=None,
72
+ service_type="middleware",
73
+ )
74
+ return JSONResponse(
75
+ status_code=exc.status_code,
76
+ content={
77
+ "error": {
78
+ "type": exc.error_type,
79
+ "message": str(exc),
80
+ }
81
+ },
82
+ )
83
+
84
+ @app.exception_handler(ValidationError)
85
+ async def validation_error_handler(
86
+ request: Request, exc: ValidationError
87
+ ) -> JSONResponse:
88
+ """Handle validation errors."""
89
+ logger.error(
90
+ "Validation error",
91
+ error_type="validation_error",
92
+ error_message=str(exc),
93
+ status_code=400,
94
+ request_method=request.method,
95
+ request_url=str(request.url.path),
96
+ )
97
+
98
+ # Record error in metrics
99
+ if metrics:
100
+ metrics.record_error(
101
+ error_type="validation_error",
102
+ endpoint=str(request.url.path),
103
+ model=None,
104
+ service_type="middleware",
105
+ )
106
+ return JSONResponse(
107
+ status_code=400,
108
+ content={
109
+ "error": {
110
+ "type": "validation_error",
111
+ "message": str(exc),
112
+ }
113
+ },
114
+ )
115
+
116
+ @app.exception_handler(AuthenticationError)
117
+ async def authentication_error_handler(
118
+ request: Request, exc: AuthenticationError
119
+ ) -> JSONResponse:
120
+ """Handle authentication errors."""
121
+ logger.error(
122
+ "Authentication error",
123
+ error_type="authentication_error",
124
+ error_message=str(exc),
125
+ status_code=401,
126
+ request_method=request.method,
127
+ request_url=str(request.url.path),
128
+ client_ip=request.client.host if request.client else "unknown",
129
+ user_agent=request.headers.get("user-agent", "unknown"),
130
+ )
131
+
132
+ # Record error in metrics
133
+ if metrics:
134
+ metrics.record_error(
135
+ error_type="authentication_error",
136
+ endpoint=str(request.url.path),
137
+ model=None,
138
+ service_type="middleware",
139
+ )
140
+ return JSONResponse(
141
+ status_code=401,
142
+ content={
143
+ "error": {
144
+ "type": "authentication_error",
145
+ "message": str(exc),
146
+ }
147
+ },
148
+ )
149
+
150
+ @app.exception_handler(PermissionError)
151
+ async def permission_error_handler(
152
+ request: Request, exc: PermissionError
153
+ ) -> JSONResponse:
154
+ """Handle permission errors."""
155
+ logger.error(
156
+ "Permission error",
157
+ error_type="permission_error",
158
+ error_message=str(exc),
159
+ status_code=403,
160
+ request_method=request.method,
161
+ request_url=str(request.url.path),
162
+ client_ip=request.client.host if request.client else "unknown",
163
+ )
164
+
165
+ # Record error in metrics
166
+ if metrics:
167
+ metrics.record_error(
168
+ error_type="permission_error",
169
+ endpoint=str(request.url.path),
170
+ model=None,
171
+ service_type="middleware",
172
+ )
173
+ return JSONResponse(
174
+ status_code=403,
175
+ content={
176
+ "error": {
177
+ "type": "permission_error",
178
+ "message": str(exc),
179
+ }
180
+ },
181
+ )
182
+
183
+ @app.exception_handler(NotFoundError)
184
+ async def not_found_error_handler(
185
+ request: Request, exc: NotFoundError
186
+ ) -> JSONResponse:
187
+ """Handle not found errors."""
188
+ logger.error(
189
+ "Not found error",
190
+ error_type="not_found_error",
191
+ error_message=str(exc),
192
+ status_code=404,
193
+ request_method=request.method,
194
+ request_url=str(request.url.path),
195
+ )
196
+
197
+ # Record error in metrics
198
+ if metrics:
199
+ metrics.record_error(
200
+ error_type="not_found_error",
201
+ endpoint=str(request.url.path),
202
+ model=None,
203
+ service_type="middleware",
204
+ )
205
+ return JSONResponse(
206
+ status_code=404,
207
+ content={
208
+ "error": {
209
+ "type": "not_found_error",
210
+ "message": str(exc),
211
+ }
212
+ },
213
+ )
214
+
215
+ @app.exception_handler(RateLimitError)
216
+ async def rate_limit_error_handler(
217
+ request: Request, exc: RateLimitError
218
+ ) -> JSONResponse:
219
+ """Handle rate limit errors."""
220
+ logger.error(
221
+ "Rate limit error",
222
+ error_type="rate_limit_error",
223
+ error_message=str(exc),
224
+ status_code=429,
225
+ request_method=request.method,
226
+ request_url=str(request.url.path),
227
+ client_ip=request.client.host if request.client else "unknown",
228
+ )
229
+
230
+ # Record error in metrics
231
+ if metrics:
232
+ metrics.record_error(
233
+ error_type="rate_limit_error",
234
+ endpoint=str(request.url.path),
235
+ model=None,
236
+ service_type="middleware",
237
+ )
238
+ return JSONResponse(
239
+ status_code=429,
240
+ content={
241
+ "error": {
242
+ "type": "rate_limit_error",
243
+ "message": str(exc),
244
+ }
245
+ },
246
+ )
247
+
248
+ @app.exception_handler(ModelNotFoundError)
249
+ async def model_not_found_error_handler(
250
+ request: Request, exc: ModelNotFoundError
251
+ ) -> JSONResponse:
252
+ """Handle model not found errors."""
253
+ logger.error(
254
+ "Model not found error",
255
+ error_type="model_not_found_error",
256
+ error_message=str(exc),
257
+ status_code=404,
258
+ request_method=request.method,
259
+ request_url=str(request.url.path),
260
+ )
261
+
262
+ # Record error in metrics
263
+ if metrics:
264
+ metrics.record_error(
265
+ error_type="model_not_found_error",
266
+ endpoint=str(request.url.path),
267
+ model=None,
268
+ service_type="middleware",
269
+ )
270
+ return JSONResponse(
271
+ status_code=404,
272
+ content={
273
+ "error": {
274
+ "type": "model_not_found_error",
275
+ "message": str(exc),
276
+ }
277
+ },
278
+ )
279
+
280
+ @app.exception_handler(TimeoutError)
281
+ async def timeout_error_handler(
282
+ request: Request, exc: TimeoutError
283
+ ) -> JSONResponse:
284
+ """Handle timeout errors."""
285
+ logger.error(
286
+ "Timeout error",
287
+ error_type="timeout_error",
288
+ error_message=str(exc),
289
+ status_code=408,
290
+ request_method=request.method,
291
+ request_url=str(request.url.path),
292
+ )
293
+
294
+ # Record error in metrics
295
+ if metrics:
296
+ metrics.record_error(
297
+ error_type="timeout_error",
298
+ endpoint=str(request.url.path),
299
+ model=None,
300
+ service_type="middleware",
301
+ )
302
+ return JSONResponse(
303
+ status_code=408,
304
+ content={
305
+ "error": {
306
+ "type": "timeout_error",
307
+ "message": str(exc),
308
+ }
309
+ },
310
+ )
311
+
312
+ @app.exception_handler(ServiceUnavailableError)
313
+ async def service_unavailable_error_handler(
314
+ request: Request, exc: ServiceUnavailableError
315
+ ) -> JSONResponse:
316
+ """Handle service unavailable errors."""
317
+ logger.error(
318
+ "Service unavailable error",
319
+ error_type="service_unavailable_error",
320
+ error_message=str(exc),
321
+ status_code=503,
322
+ request_method=request.method,
323
+ request_url=str(request.url.path),
324
+ )
325
+
326
+ # Record error in metrics
327
+ if metrics:
328
+ metrics.record_error(
329
+ error_type="service_unavailable_error",
330
+ endpoint=str(request.url.path),
331
+ model=None,
332
+ service_type="middleware",
333
+ )
334
+ return JSONResponse(
335
+ status_code=503,
336
+ content={
337
+ "error": {
338
+ "type": "service_unavailable_error",
339
+ "message": str(exc),
340
+ }
341
+ },
342
+ )
343
+
344
+ @app.exception_handler(DockerError)
345
+ async def docker_error_handler(request: Request, exc: DockerError) -> JSONResponse:
346
+ """Handle Docker errors."""
347
+ logger.error(
348
+ "Docker error",
349
+ error_type="docker_error",
350
+ error_message=str(exc),
351
+ status_code=500,
352
+ request_method=request.method,
353
+ request_url=str(request.url.path),
354
+ )
355
+
356
+ # Record error in metrics
357
+ if metrics:
358
+ metrics.record_error(
359
+ error_type="docker_error",
360
+ endpoint=str(request.url.path),
361
+ model=None,
362
+ service_type="middleware",
363
+ )
364
+ return JSONResponse(
365
+ status_code=500,
366
+ content={
367
+ "error": {
368
+ "type": "docker_error",
369
+ "message": str(exc),
370
+ }
371
+ },
372
+ )
373
+
374
+ # Core proxy errors
375
+ @app.exception_handler(ProxyError)
376
+ async def proxy_error_handler(request: Request, exc: ProxyError) -> JSONResponse:
377
+ """Handle proxy errors."""
378
+ logger.error(
379
+ "Proxy error",
380
+ error_type="proxy_error",
381
+ error_message=str(exc),
382
+ status_code=500,
383
+ request_method=request.method,
384
+ request_url=str(request.url.path),
385
+ )
386
+
387
+ # Record error in metrics
388
+ if metrics:
389
+ metrics.record_error(
390
+ error_type="proxy_error",
391
+ endpoint=str(request.url.path),
392
+ model=None,
393
+ service_type="middleware",
394
+ )
395
+ return JSONResponse(
396
+ status_code=500,
397
+ content={
398
+ "error": {
399
+ "type": "proxy_error",
400
+ "message": str(exc),
401
+ }
402
+ },
403
+ )
404
+
405
+ @app.exception_handler(TransformationError)
406
+ async def transformation_error_handler(
407
+ request: Request, exc: TransformationError
408
+ ) -> JSONResponse:
409
+ """Handle transformation errors."""
410
+ logger.error(
411
+ "Transformation error",
412
+ error_type="transformation_error",
413
+ error_message=str(exc),
414
+ status_code=500,
415
+ request_method=request.method,
416
+ request_url=str(request.url.path),
417
+ )
418
+
419
+ # Record error in metrics
420
+ if metrics:
421
+ metrics.record_error(
422
+ error_type="transformation_error",
423
+ endpoint=str(request.url.path),
424
+ model=None,
425
+ service_type="middleware",
426
+ )
427
+ return JSONResponse(
428
+ status_code=500,
429
+ content={
430
+ "error": {
431
+ "type": "transformation_error",
432
+ "message": str(exc),
433
+ }
434
+ },
435
+ )
436
+
437
+ @app.exception_handler(MiddlewareError)
438
+ async def middleware_error_handler(
439
+ request: Request, exc: MiddlewareError
440
+ ) -> JSONResponse:
441
+ """Handle middleware errors."""
442
+ logger.error(
443
+ "Middleware error",
444
+ error_type="middleware_error",
445
+ error_message=str(exc),
446
+ status_code=500,
447
+ request_method=request.method,
448
+ request_url=str(request.url.path),
449
+ )
450
+
451
+ # Record error in metrics
452
+ if metrics:
453
+ metrics.record_error(
454
+ error_type="middleware_error",
455
+ endpoint=str(request.url.path),
456
+ model=None,
457
+ service_type="middleware",
458
+ )
459
+ return JSONResponse(
460
+ status_code=500,
461
+ content={
462
+ "error": {
463
+ "type": "middleware_error",
464
+ "message": str(exc),
465
+ }
466
+ },
467
+ )
468
+
469
+ @app.exception_handler(ProxyConnectionError)
470
+ async def proxy_connection_error_handler(
471
+ request: Request, exc: ProxyConnectionError
472
+ ) -> JSONResponse:
473
+ """Handle proxy connection errors."""
474
+ logger.error(
475
+ "Proxy connection error",
476
+ error_type="proxy_connection_error",
477
+ error_message=str(exc),
478
+ status_code=502,
479
+ request_method=request.method,
480
+ request_url=str(request.url.path),
481
+ )
482
+
483
+ # Record error in metrics
484
+ if metrics:
485
+ metrics.record_error(
486
+ error_type="proxy_connection_error",
487
+ endpoint=str(request.url.path),
488
+ model=None,
489
+ service_type="middleware",
490
+ )
491
+ return JSONResponse(
492
+ status_code=502,
493
+ content={
494
+ "error": {
495
+ "type": "proxy_connection_error",
496
+ "message": str(exc),
497
+ }
498
+ },
499
+ )
500
+
501
+ @app.exception_handler(ProxyTimeoutError)
502
+ async def proxy_timeout_error_handler(
503
+ request: Request, exc: ProxyTimeoutError
504
+ ) -> JSONResponse:
505
+ """Handle proxy timeout errors."""
506
+ logger.error(
507
+ "Proxy timeout error",
508
+ error_type="proxy_timeout_error",
509
+ error_message=str(exc),
510
+ status_code=504,
511
+ request_method=request.method,
512
+ request_url=str(request.url.path),
513
+ )
514
+
515
+ # Record error in metrics
516
+ if metrics:
517
+ metrics.record_error(
518
+ error_type="proxy_timeout_error",
519
+ endpoint=str(request.url.path),
520
+ model=None,
521
+ service_type="middleware",
522
+ )
523
+ return JSONResponse(
524
+ status_code=504,
525
+ content={
526
+ "error": {
527
+ "type": "proxy_timeout_error",
528
+ "message": str(exc),
529
+ }
530
+ },
531
+ )
532
+
533
+ @app.exception_handler(ProxyAuthenticationError)
534
+ async def proxy_authentication_error_handler(
535
+ request: Request, exc: ProxyAuthenticationError
536
+ ) -> JSONResponse:
537
+ """Handle proxy authentication errors."""
538
+ logger.error(
539
+ "Proxy authentication error",
540
+ error_type="proxy_authentication_error",
541
+ error_message=str(exc),
542
+ status_code=401,
543
+ request_method=request.method,
544
+ request_url=str(request.url.path),
545
+ client_ip=request.client.host if request.client else "unknown",
546
+ )
547
+
548
+ # Record error in metrics
549
+ if metrics:
550
+ metrics.record_error(
551
+ error_type="proxy_authentication_error",
552
+ endpoint=str(request.url.path),
553
+ model=None,
554
+ service_type="middleware",
555
+ )
556
+ return JSONResponse(
557
+ status_code=401,
558
+ content={
559
+ "error": {
560
+ "type": "proxy_authentication_error",
561
+ "message": str(exc),
562
+ }
563
+ },
564
+ )
565
+
566
+ # Standard HTTP exceptions
567
+ @app.exception_handler(HTTPException)
568
+ async def http_exception_handler(
569
+ request: Request, exc: HTTPException
570
+ ) -> JSONResponse:
571
+ """Handle HTTP exceptions."""
572
+ # Don't log stack trace for 404 errors as they're expected
573
+ if exc.status_code == 404:
574
+ logger.info(
575
+ "HTTP 404 error",
576
+ error_type="http_404",
577
+ error_message=exc.detail,
578
+ status_code=404,
579
+ request_method=request.method,
580
+ request_url=str(request.url.path),
581
+ )
582
+ else:
583
+ # Log with basic stack trace (no local variables)
584
+ stack_trace = None
585
+ if logger.isEnabledFor(logging.DEBUG):
586
+ import traceback
587
+
588
+ stack_trace = traceback.format_exc()
589
+
590
+ logger.error(
591
+ "HTTP exception",
592
+ error_type="http_error",
593
+ error_message=exc.detail,
594
+ status_code=exc.status_code,
595
+ request_method=request.method,
596
+ request_url=str(request.url.path),
597
+ stack_trace=stack_trace,
598
+ )
599
+
600
+ # Record error in metrics
601
+ if metrics:
602
+ error_type = "http_404" if exc.status_code == 404 else "http_error"
603
+ metrics.record_error(
604
+ error_type=error_type,
605
+ endpoint=str(request.url.path),
606
+ model=None,
607
+ service_type="middleware",
608
+ )
609
+
610
+ # TODO: Add when in prod hide details in response
611
+ return JSONResponse(
612
+ status_code=exc.status_code,
613
+ content={
614
+ "error": {
615
+ "type": "http_error",
616
+ "message": exc.detail,
617
+ }
618
+ },
619
+ )
620
+
621
+ @app.exception_handler(StarletteHTTPException)
622
+ async def starlette_http_exception_handler(
623
+ request: Request, exc: StarletteHTTPException
624
+ ) -> JSONResponse:
625
+ """Handle Starlette HTTP exceptions."""
626
+ # Don't log stack trace for 404 errors as they're expected
627
+ if exc.status_code == 404:
628
+ logger.info(
629
+ "Starlette HTTP 404 error",
630
+ error_type="starlette_http_404",
631
+ error_message=exc.detail,
632
+ status_code=404,
633
+ request_method=request.method,
634
+ request_url=str(request.url.path),
635
+ )
636
+ else:
637
+ logger.error(
638
+ "Starlette HTTP exception",
639
+ error_type="starlette_http_error",
640
+ error_message=exc.detail,
641
+ status_code=exc.status_code,
642
+ request_method=request.method,
643
+ request_url=str(request.url.path),
644
+ )
645
+
646
+ # Record error in metrics
647
+ if metrics:
648
+ error_type = (
649
+ "starlette_http_404"
650
+ if exc.status_code == 404
651
+ else "starlette_http_error"
652
+ )
653
+ metrics.record_error(
654
+ error_type=error_type,
655
+ endpoint=str(request.url.path),
656
+ model=None,
657
+ service_type="middleware",
658
+ )
659
+ return JSONResponse(
660
+ status_code=exc.status_code,
661
+ content={
662
+ "error": {
663
+ "type": "http_error",
664
+ "message": exc.detail,
665
+ }
666
+ },
667
+ )
668
+
669
+ # Global exception handler
670
+ @app.exception_handler(Exception)
671
+ async def global_exception_handler(
672
+ request: Request, exc: Exception
673
+ ) -> JSONResponse:
674
+ """Handle all other unhandled exceptions."""
675
+ logger.error(
676
+ "Unhandled exception",
677
+ error_type="unhandled_exception",
678
+ error_message=str(exc),
679
+ status_code=500,
680
+ request_method=request.method,
681
+ request_url=str(request.url.path),
682
+ exc_info=True,
683
+ )
684
+
685
+ # Record error in metrics
686
+ if metrics:
687
+ metrics.record_error(
688
+ error_type="unhandled_exception",
689
+ endpoint=str(request.url.path),
690
+ model=None,
691
+ service_type="middleware",
692
+ )
693
+ return JSONResponse(
694
+ status_code=500,
695
+ content={
696
+ "error": {
697
+ "type": "internal_server_error",
698
+ "message": "An internal server error occurred",
699
+ }
700
+ },
701
+ )
702
+
703
+ logger.debug("error_handlers_setup_completed")