sf-veritas 0.11.10__cp314-cp314-manylinux_2_28_x86_64.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 (141) hide show
  1. sf_veritas/__init__.py +46 -0
  2. sf_veritas/_auto_preload.py +73 -0
  3. sf_veritas/_sfconfig.c +162 -0
  4. sf_veritas/_sfconfig.cpython-314-x86_64-linux-gnu.so +0 -0
  5. sf_veritas/_sfcrashhandler.c +267 -0
  6. sf_veritas/_sfcrashhandler.cpython-314-x86_64-linux-gnu.so +0 -0
  7. sf_veritas/_sffastlog.c +953 -0
  8. sf_veritas/_sffastlog.cpython-314-x86_64-linux-gnu.so +0 -0
  9. sf_veritas/_sffastnet.c +994 -0
  10. sf_veritas/_sffastnet.cpython-314-x86_64-linux-gnu.so +0 -0
  11. sf_veritas/_sffastnetworkrequest.c +727 -0
  12. sf_veritas/_sffastnetworkrequest.cpython-314-x86_64-linux-gnu.so +0 -0
  13. sf_veritas/_sffuncspan.c +2791 -0
  14. sf_veritas/_sffuncspan.cpython-314-x86_64-linux-gnu.so +0 -0
  15. sf_veritas/_sffuncspan_config.c +730 -0
  16. sf_veritas/_sffuncspan_config.cpython-314-x86_64-linux-gnu.so +0 -0
  17. sf_veritas/_sfheadercheck.c +341 -0
  18. sf_veritas/_sfheadercheck.cpython-314-x86_64-linux-gnu.so +0 -0
  19. sf_veritas/_sfnetworkhop.c +1454 -0
  20. sf_veritas/_sfnetworkhop.cpython-314-x86_64-linux-gnu.so +0 -0
  21. sf_veritas/_sfservice.c +1223 -0
  22. sf_veritas/_sfservice.cpython-314-x86_64-linux-gnu.so +0 -0
  23. sf_veritas/_sfteepreload.c +6227 -0
  24. sf_veritas/app_config.py +57 -0
  25. sf_veritas/cli.py +336 -0
  26. sf_veritas/constants.py +10 -0
  27. sf_veritas/custom_excepthook.py +304 -0
  28. sf_veritas/custom_log_handler.py +146 -0
  29. sf_veritas/custom_output_wrapper.py +153 -0
  30. sf_veritas/custom_print.py +153 -0
  31. sf_veritas/django_app.py +5 -0
  32. sf_veritas/env_vars.py +186 -0
  33. sf_veritas/exception_handling_middleware.py +18 -0
  34. sf_veritas/exception_metaclass.py +69 -0
  35. sf_veritas/fast_frame_info.py +116 -0
  36. sf_veritas/fast_network_hop.py +293 -0
  37. sf_veritas/frame_tools.py +112 -0
  38. sf_veritas/funcspan_config_loader.py +693 -0
  39. sf_veritas/function_span_profiler.py +1313 -0
  40. sf_veritas/get_preload_path.py +34 -0
  41. sf_veritas/import_hook.py +62 -0
  42. sf_veritas/infra_details/__init__.py +3 -0
  43. sf_veritas/infra_details/get_infra_details.py +24 -0
  44. sf_veritas/infra_details/kubernetes/__init__.py +3 -0
  45. sf_veritas/infra_details/kubernetes/get_cluster_name.py +147 -0
  46. sf_veritas/infra_details/kubernetes/get_details.py +7 -0
  47. sf_veritas/infra_details/running_on/__init__.py +17 -0
  48. sf_veritas/infra_details/running_on/kubernetes.py +11 -0
  49. sf_veritas/interceptors.py +543 -0
  50. sf_veritas/libsfnettee.so +0 -0
  51. sf_veritas/local_env_detect.py +118 -0
  52. sf_veritas/package_metadata.py +6 -0
  53. sf_veritas/patches/__init__.py +0 -0
  54. sf_veritas/patches/_patch_tracker.py +74 -0
  55. sf_veritas/patches/concurrent_futures.py +19 -0
  56. sf_veritas/patches/constants.py +1 -0
  57. sf_veritas/patches/exceptions.py +82 -0
  58. sf_veritas/patches/multiprocessing.py +32 -0
  59. sf_veritas/patches/network_libraries/__init__.py +99 -0
  60. sf_veritas/patches/network_libraries/aiohttp.py +294 -0
  61. sf_veritas/patches/network_libraries/curl_cffi.py +363 -0
  62. sf_veritas/patches/network_libraries/http_client.py +670 -0
  63. sf_veritas/patches/network_libraries/httpcore.py +580 -0
  64. sf_veritas/patches/network_libraries/httplib2.py +315 -0
  65. sf_veritas/patches/network_libraries/httpx.py +557 -0
  66. sf_veritas/patches/network_libraries/niquests.py +218 -0
  67. sf_veritas/patches/network_libraries/pycurl.py +399 -0
  68. sf_veritas/patches/network_libraries/requests.py +595 -0
  69. sf_veritas/patches/network_libraries/ssl_socket.py +822 -0
  70. sf_veritas/patches/network_libraries/tornado.py +360 -0
  71. sf_veritas/patches/network_libraries/treq.py +270 -0
  72. sf_veritas/patches/network_libraries/urllib_request.py +483 -0
  73. sf_veritas/patches/network_libraries/utils.py +598 -0
  74. sf_veritas/patches/os.py +17 -0
  75. sf_veritas/patches/threading.py +231 -0
  76. sf_veritas/patches/web_frameworks/__init__.py +54 -0
  77. sf_veritas/patches/web_frameworks/aiohttp.py +798 -0
  78. sf_veritas/patches/web_frameworks/async_websocket_consumer.py +337 -0
  79. sf_veritas/patches/web_frameworks/blacksheep.py +532 -0
  80. sf_veritas/patches/web_frameworks/bottle.py +513 -0
  81. sf_veritas/patches/web_frameworks/cherrypy.py +683 -0
  82. sf_veritas/patches/web_frameworks/cors_utils.py +122 -0
  83. sf_veritas/patches/web_frameworks/django.py +963 -0
  84. sf_veritas/patches/web_frameworks/eve.py +401 -0
  85. sf_veritas/patches/web_frameworks/falcon.py +931 -0
  86. sf_veritas/patches/web_frameworks/fastapi.py +738 -0
  87. sf_veritas/patches/web_frameworks/flask.py +526 -0
  88. sf_veritas/patches/web_frameworks/klein.py +501 -0
  89. sf_veritas/patches/web_frameworks/litestar.py +616 -0
  90. sf_veritas/patches/web_frameworks/pyramid.py +440 -0
  91. sf_veritas/patches/web_frameworks/quart.py +841 -0
  92. sf_veritas/patches/web_frameworks/robyn.py +708 -0
  93. sf_veritas/patches/web_frameworks/sanic.py +874 -0
  94. sf_veritas/patches/web_frameworks/starlette.py +742 -0
  95. sf_veritas/patches/web_frameworks/strawberry.py +1446 -0
  96. sf_veritas/patches/web_frameworks/tornado.py +485 -0
  97. sf_veritas/patches/web_frameworks/utils.py +170 -0
  98. sf_veritas/print_override.py +13 -0
  99. sf_veritas/regular_data_transmitter.py +444 -0
  100. sf_veritas/request_interceptor.py +401 -0
  101. sf_veritas/request_utils.py +550 -0
  102. sf_veritas/segfault_handler.py +116 -0
  103. sf_veritas/server_status.py +1 -0
  104. sf_veritas/shutdown_flag.py +11 -0
  105. sf_veritas/subprocess_startup.py +3 -0
  106. sf_veritas/test_cli.py +145 -0
  107. sf_veritas/thread_local.py +1319 -0
  108. sf_veritas/timeutil.py +114 -0
  109. sf_veritas/transmit_exception_to_sailfish.py +28 -0
  110. sf_veritas/transmitter.py +132 -0
  111. sf_veritas/types.py +47 -0
  112. sf_veritas/unified_interceptor.py +1678 -0
  113. sf_veritas/utils.py +39 -0
  114. sf_veritas-0.11.10.dist-info/METADATA +97 -0
  115. sf_veritas-0.11.10.dist-info/RECORD +141 -0
  116. sf_veritas-0.11.10.dist-info/WHEEL +5 -0
  117. sf_veritas-0.11.10.dist-info/entry_points.txt +2 -0
  118. sf_veritas-0.11.10.dist-info/top_level.txt +1 -0
  119. sf_veritas.libs/libbrotlicommon-6ce2a53c.so.1.0.6 +0 -0
  120. sf_veritas.libs/libbrotlidec-811d1be3.so.1.0.6 +0 -0
  121. sf_veritas.libs/libcom_err-730ca923.so.2.1 +0 -0
  122. sf_veritas.libs/libcrypt-52aca757.so.1.1.0 +0 -0
  123. sf_veritas.libs/libcrypto-bdaed0ea.so.1.1.1k +0 -0
  124. sf_veritas.libs/libcurl-eaa3cf66.so.4.5.0 +0 -0
  125. sf_veritas.libs/libgssapi_krb5-323bbd21.so.2.2 +0 -0
  126. sf_veritas.libs/libidn2-2f4a5893.so.0.3.6 +0 -0
  127. sf_veritas.libs/libk5crypto-9a74ff38.so.3.1 +0 -0
  128. sf_veritas.libs/libkeyutils-2777d33d.so.1.6 +0 -0
  129. sf_veritas.libs/libkrb5-a55300e8.so.3.3 +0 -0
  130. sf_veritas.libs/libkrb5support-e6594cfc.so.0.1 +0 -0
  131. sf_veritas.libs/liblber-2-d20824ef.4.so.2.10.9 +0 -0
  132. sf_veritas.libs/libldap-2-cea2a960.4.so.2.10.9 +0 -0
  133. sf_veritas.libs/libnghttp2-39367a22.so.14.17.0 +0 -0
  134. sf_veritas.libs/libpcre2-8-516f4c9d.so.0.7.1 +0 -0
  135. sf_veritas.libs/libpsl-99becdd3.so.5.3.1 +0 -0
  136. sf_veritas.libs/libsasl2-7de4d792.so.3.0.0 +0 -0
  137. sf_veritas.libs/libselinux-d0805dcb.so.1 +0 -0
  138. sf_veritas.libs/libssh-c11d285b.so.4.8.7 +0 -0
  139. sf_veritas.libs/libssl-60250281.so.1.1.1k +0 -0
  140. sf_veritas.libs/libunistring-05abdd40.so.2.1.0 +0 -0
  141. sf_veritas.libs/libuuid-95b83d40.so.1.3.0 +0 -0
@@ -0,0 +1,963 @@
1
+ import asyncio
2
+ import inspect
3
+ import os
4
+ import sys
5
+ import threading
6
+ import traceback
7
+ from typing import List, Optional
8
+
9
+ from ... import _sffuncspan
10
+ from .cors_utils import inject_sailfish_headers, should_inject_headers
11
+ from .utils import _is_user_code, _unwrap_user_func, should_skip_route, reinitialize_log_print_capture_for_worker
12
+
13
+ try:
14
+ from django.utils.deprecation import MiddlewareMixin
15
+ except ImportError:
16
+ MiddlewareMixin = object # fallback for non-Django environments
17
+
18
+ import traceback
19
+
20
+ from ... import app_config
21
+ from ...constants import (
22
+ FUNCSPAN_OVERRIDE_HEADER,
23
+ FUNCSPAN_OVERRIDE_HEADER_BYTES,
24
+ SAILFISH_TRACING_HEADER,
25
+ SAILFISH_TRACING_HEADER_BYTES,
26
+ )
27
+ from ...custom_excepthook import custom_excepthook
28
+ from ...env_vars import (
29
+ PRINT_CONFIGURATION_STATUSES,
30
+ SF_DEBUG,
31
+ SF_NETWORKHOP_CAPTURE_REQUEST_BODY,
32
+ SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS,
33
+ SF_NETWORKHOP_CAPTURE_RESPONSE_BODY,
34
+ SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS,
35
+ SF_NETWORKHOP_REQUEST_LIMIT_MB,
36
+ SF_NETWORKHOP_RESPONSE_LIMIT_MB,
37
+ )
38
+ from ...fast_network_hop import fast_send_network_hop_fast, register_endpoint
39
+ from ...thread_local import (
40
+ clear_c_tls_parent_trace_id,
41
+ clear_current_request_path,
42
+ clear_funcspan_override,
43
+ clear_outbound_header_base,
44
+ clear_trace_id,
45
+ generate_new_trace_id,
46
+ get_or_set_sf_trace_id,
47
+ get_sf_trace_id,
48
+ set_current_request_path,
49
+ set_funcspan_override,
50
+ set_outbound_header_base,
51
+ )
52
+
53
+ # Registry mapping view function id → endpoint_id (for fast C path)
54
+ _ENDPOINT_REGISTRY = {}
55
+
56
+ # Module-level variable for routes to skip (set by patch_django_middleware)
57
+ _ROUTES_TO_SKIP = []
58
+
59
+
60
+ def find_and_modify_output_wrapper():
61
+ """
62
+ Monkey-patch Django's OutputWrapper to always use the current sys.stdout/stderr
63
+ instead of storing a reference at init time. This ensures Django management
64
+ commands (like migrate) output is captured by our UnifiedInterceptor even if
65
+ OutputWrapper was instantiated before setup_interceptors() ran.
66
+ """
67
+ if PRINT_CONFIGURATION_STATUSES:
68
+ print("find_and_modify_output_wrapper", log=False)
69
+
70
+ try:
71
+ from django.core.management.base import OutputWrapper
72
+ except ImportError:
73
+ if PRINT_CONFIGURATION_STATUSES:
74
+ print("Django not found; skipping OutputWrapper patch", log=False)
75
+ return
76
+
77
+ # Check if already patched (idempotent)
78
+ if hasattr(OutputWrapper, "_sf_patched"):
79
+ if PRINT_CONFIGURATION_STATUSES:
80
+ print("OutputWrapper already patched; skipping", log=False)
81
+ return
82
+
83
+ # Save original methods
84
+ _original_init = OutputWrapper.__init__
85
+ _original_write = OutputWrapper.write
86
+
87
+ def patched_init(self, out, ending="\n"):
88
+ """Patched __init__ that tracks if this wrapper is wrapping stdout/stderr."""
89
+ # Call original init
90
+ _original_init(self, out, ending)
91
+ # Track if this wrapper is for stdout or stderr (so we can redirect to current stream)
92
+ self._sf_is_stdout = out is sys.stdout or out is sys.__stdout__
93
+ self._sf_is_stderr = out is sys.stderr or out is sys.__stderr__
94
+ if SF_DEBUG and app_config._interceptors_initialized:
95
+ print(
96
+ f"[Django OutputWrapper] Created: stdout={self._sf_is_stdout}, stderr={self._sf_is_stderr}",
97
+ log=False,
98
+ )
99
+
100
+ def patched_write(self, msg="", style_func=None, ending=None):
101
+ """
102
+ Patched write that uses CURRENT sys.stdout/stderr instead of the stored reference.
103
+ This ensures our UnifiedInterceptor captures Django output.
104
+ """
105
+ # If this wrapper was created for stdout, redirect to CURRENT sys.stdout
106
+ if getattr(self, "_sf_is_stdout", False):
107
+ original_out = self._out
108
+ self._out = sys.stdout
109
+ try:
110
+ return _original_write(self, msg, style_func, ending)
111
+ finally:
112
+ self._out = original_out
113
+
114
+ # If this wrapper was created for stderr, redirect to CURRENT sys.stderr
115
+ elif getattr(self, "_sf_is_stderr", False):
116
+ original_out = self._out
117
+ self._out = sys.stderr
118
+ try:
119
+ return _original_write(self, msg, style_func, ending)
120
+ finally:
121
+ self._out = original_out
122
+
123
+ # Otherwise use original behavior
124
+ return _original_write(self, msg, style_func, ending)
125
+
126
+ # Apply patches
127
+ OutputWrapper.__init__ = patched_init
128
+ OutputWrapper.write = patched_write
129
+ OutputWrapper._sf_patched = True
130
+
131
+ if PRINT_CONFIGURATION_STATUSES:
132
+ print("find_and_modify_output_wrapper...DONE (monkey-patched)", log=False)
133
+
134
+
135
+ class SailfishMiddleware(MiddlewareMixin):
136
+ """
137
+ • process_request – capture inbound SAILFISH_TRACING_HEADER header.
138
+ • process_view – emit one NetworkHop per view (skip Strawberry).
139
+ • __call__ override – last-chance catcher for uncaught exceptions.
140
+ • got_request_exception signal – main hook for 500-level errors.
141
+ • process_exception – fallback for view-raised exceptions.
142
+ """
143
+
144
+ # ------------------------------------------------------------------ #
145
+ # 0 | Signal registration (called once at server start-up)
146
+ # ------------------------------------------------------------------ #
147
+ def __init__(self, get_response):
148
+ super().__init__(get_response)
149
+
150
+ # Note: Profiler is already installed by unified_interceptor.py, no need to reinstall here
151
+
152
+ # CRITICAL: Reinitialize log/print capture in each Django worker process
153
+ # When Supervisor forks workers (numprocs=2), daemon threads don't survive the fork
154
+ # but global flags do, so we must force re-initialization per worker
155
+ reinitialize_log_print_capture_for_worker()
156
+
157
+ # Attach to Django's global exception signal so we ALWAYS
158
+ # see real exceptions that become HTTP-500 responses.
159
+ from django.core.signals import got_request_exception
160
+
161
+ got_request_exception.disconnect( # avoid dupes on reload
162
+ self._on_exception_signal, dispatch_uid="sf_veritas_signal"
163
+ )
164
+ got_request_exception.connect(
165
+ self._on_exception_signal,
166
+ weak=False,
167
+ dispatch_uid="sf_veritas_signal",
168
+ )
169
+
170
+ # ------------------------------------------------------------------ #
171
+ # 1 | Signal handler ← FIXED
172
+ # ------------------------------------------------------------------ #
173
+ def _on_exception_signal(self, sender, request, **kwargs):
174
+ """
175
+ Handle django.core.signals.got_request_exception.
176
+
177
+ The signal doesn't pass the exception object; per Django's own
178
+ implementation (and Sentry's approach) we fetch it from
179
+ sys.exc_info().
180
+ """
181
+
182
+ exc_type, exc_value, exc_tb = sys.exc_info()
183
+
184
+ if SF_DEBUG and app_config._interceptors_initialized:
185
+ print(
186
+ f"[[SailfishMiddleware._on_exception_signal]] "
187
+ f"exc_value={exc_value!r}",
188
+ log=False,
189
+ )
190
+
191
+ if exc_value:
192
+ custom_excepthook(exc_type, exc_value, exc_tb)
193
+
194
+ # ------------------------------------------------------------------ #
195
+ # 2 | Last-chance wrapper (rarely triggered in WSGI but free)
196
+ # ------------------------------------------------------------------ #
197
+ def __call__(self, request):
198
+ try:
199
+ return super().__call__(request)
200
+ except Exception as exc:
201
+ custom_excepthook(type(exc), exc, exc.__traceback__)
202
+ raise # preserve default Django 500
203
+
204
+ # ------------------------------------------------------------------ #
205
+ # 3 | Header capture
206
+ # ------------------------------------------------------------------ #
207
+ def process_request(self, request):
208
+ # CRITICAL: Clear trace_id FIRST at request start to ensure fresh start
209
+ # Django reuses threads, so we must clear the ContextVar from previous request
210
+ try:
211
+ clear_trace_id()
212
+ clear_outbound_header_base()
213
+ clear_c_tls_parent_trace_id()
214
+ clear_current_request_path()
215
+ if SF_DEBUG and app_config._interceptors_initialized:
216
+ print(
217
+ f"[[SailfishMiddleware.process_request]] Cleared all context at request start",
218
+ log=False,
219
+ )
220
+ except Exception as e:
221
+ if SF_DEBUG and app_config._interceptors_initialized:
222
+ print(
223
+ f"[[SailfishMiddleware.process_request]] Failed to clear context: {e}",
224
+ log=False,
225
+ )
226
+
227
+ # Set current request path for route-based suppression (SF_DISABLE_INBOUND_NETWORK_TRACING_ON_ROUTES)
228
+ set_current_request_path(request.path)
229
+
230
+ # PERFORMANCE: Single-pass bytes-level header scan (convert Django META to bytes for consistent scanning)
231
+ # Django stores headers in META with HTTP_ prefix, scan once and extract what we need
232
+ incoming_trace_raw = None
233
+ funcspan_override_header = None
234
+
235
+ # Django uses string headers in META, scan for our headers
236
+ header_key = f"HTTP_{SAILFISH_TRACING_HEADER.upper().replace('-', '_')}"
237
+ incoming_trace_raw = request.META.get(header_key)
238
+
239
+ funcspan_override_key = f"HTTP_{FUNCSPAN_OVERRIDE_HEADER.upper().replace('-', '_')}"
240
+ funcspan_override_header = request.META.get(funcspan_override_key)
241
+
242
+ # CRITICAL: Seed/ensure trace_id immediately (BEFORE any outbound work)
243
+ if incoming_trace_raw:
244
+ # Incoming X-Sf3-Rid header provided - use it
245
+ get_or_set_sf_trace_id(
246
+ incoming_trace_raw, is_associated_with_inbound_request=True
247
+ )
248
+ if SF_DEBUG and app_config._interceptors_initialized:
249
+ trace_id = get_sf_trace_id()
250
+ print(
251
+ f"[[SailfishMiddleware.process_request]] "
252
+ f"Using incoming trace: {incoming_trace_raw} → trace_id={trace_id}",
253
+ log=False,
254
+ )
255
+ else:
256
+ # No incoming X-Sf3-Rid header - generate fresh trace_id for this request
257
+ new_trace = generate_new_trace_id()
258
+ if SF_DEBUG and app_config._interceptors_initialized:
259
+ trace_id = get_sf_trace_id()
260
+ print(
261
+ f"[[SailfishMiddleware.process_request]] "
262
+ f"Generated new trace_id: {new_trace} → trace_id={trace_id}",
263
+ log=False,
264
+ )
265
+
266
+ # Optional funcspan override
267
+ if funcspan_override_header:
268
+ try:
269
+ set_funcspan_override(funcspan_override_header)
270
+ if SF_DEBUG and app_config._interceptors_initialized:
271
+ print(
272
+ f"[[SailfishMiddleware.process_request]] Set function span override from header: {funcspan_override_header}",
273
+ log=False,
274
+ )
275
+ except Exception as e:
276
+ if SF_DEBUG and app_config._interceptors_initialized:
277
+ print(
278
+ f"[[SailfishMiddleware.process_request]] Failed to set function span override: {e}",
279
+ log=False,
280
+ )
281
+
282
+ # Initialize outbound header base with parent trace ID
283
+ try:
284
+ trace_id = get_sf_trace_id()
285
+ if trace_id:
286
+ s = str(trace_id)
287
+ i = s.find("/") # session
288
+ j = s.find("/", i + 1) if i != -1 else -1 # page
289
+ if j != -1:
290
+ base_trace = s[:j] # "session/page"
291
+ set_outbound_header_base(
292
+ base_trace=base_trace,
293
+ parent_trace_id=s, # "session/page/uuid"
294
+ funcspan=funcspan_override_header,
295
+ )
296
+ if SF_DEBUG and app_config._interceptors_initialized:
297
+ print(
298
+ f"[[SailfishMiddleware.process_request]] Initialized outbound header base (base={base_trace[:16] if len(base_trace) > 16 else base_trace}...)",
299
+ log=False,
300
+ )
301
+ except Exception as e:
302
+ if SF_DEBUG and app_config._interceptors_initialized:
303
+ print(
304
+ f"[[SailfishMiddleware.process_request]] Failed to initialize outbound header base: {e}",
305
+ log=False,
306
+ )
307
+
308
+ # ------------------------------------------------------------------ #
309
+ # 4 | Network-hop emission (unchanged)
310
+ # ------------------------------------------------------------------ #
311
+ def process_view(self, request, view_func, view_args, view_kwargs):
312
+ if SF_DEBUG and app_config._interceptors_initialized:
313
+ print(
314
+ f"[[SailfishMiddleware.process_view]] view_func={view_func.__name__ if hasattr(view_func, '__name__') else view_func}, "
315
+ f"path={request.path}",
316
+ log=False,
317
+ )
318
+
319
+ module = getattr(view_func, "__module__", "")
320
+ if module.startswith("strawberry"):
321
+ if SF_DEBUG and app_config._interceptors_initialized:
322
+ print(
323
+ f"[[Django.process_view]] Skipping Strawberry GraphQL view",
324
+ log=False,
325
+ )
326
+ return None
327
+
328
+ # Unwrap decorated views to get the actual user code
329
+ # Django decorators (csrf_exempt, require_http_methods, etc.) wrap views
330
+ actual_view = _unwrap_user_func(view_func)
331
+
332
+ if (
333
+ actual_view is not view_func
334
+ and SF_DEBUG
335
+ and app_config._interceptors_initialized
336
+ ):
337
+ print(
338
+ f"[[Django.process_view]] Unwrapped decorator: "
339
+ f"{view_func.__name__} → {getattr(actual_view, '__name__', 'unknown')}",
340
+ log=False,
341
+ )
342
+
343
+ # Get code object and verify it's user code
344
+ code = getattr(actual_view, "__code__", None)
345
+ if not code:
346
+ if SF_DEBUG and app_config._interceptors_initialized:
347
+ print(
348
+ f"[[Django.process_view]] No code object for view_func", log=False
349
+ )
350
+ return None
351
+
352
+ fname, lno = code.co_filename, code.co_firstlineno
353
+
354
+ # The unwrap function already checks for user code, but double-check
355
+ if not _is_user_code(fname):
356
+ if SF_DEBUG and app_config._interceptors_initialized:
357
+ print(f"[[Django.process_view]] Not user code: {fname}", log=False)
358
+ return None
359
+
360
+ # Extract route pattern from Django's resolver
361
+ route_pattern = None
362
+ if hasattr(request, "resolver_match") and request.resolver_match:
363
+ route_pattern = getattr(request.resolver_match, "route", None)
364
+
365
+ # Check if route should be skipped
366
+ if should_skip_route(route_pattern, _ROUTES_TO_SKIP):
367
+ if SF_DEBUG and app_config._interceptors_initialized:
368
+ print(
369
+ f"[[Django.process_view]] Skipping view (route matches skip pattern): {route_pattern}",
370
+ log=False,
371
+ )
372
+ return None
373
+
374
+ # Get or register endpoint_id (use actual_view for consistent tracking)
375
+ view_id = id(actual_view)
376
+ endpoint_id = _ENDPOINT_REGISTRY.get(view_id)
377
+
378
+ if endpoint_id is None:
379
+ # First time seeing this view - register it
380
+ view_name = getattr(actual_view, "__name__", "unknown")
381
+ endpoint_id = register_endpoint(
382
+ line=str(lno),
383
+ column="0",
384
+ name=view_name,
385
+ entrypoint=fname,
386
+ route=route_pattern,
387
+ )
388
+
389
+ if endpoint_id >= 0:
390
+ _ENDPOINT_REGISTRY[view_id] = endpoint_id
391
+ if SF_DEBUG and app_config._interceptors_initialized:
392
+ print(
393
+ f"[[Django]] Registered endpoint: {view_name} "
394
+ f"({fname}:{lno}) → id={endpoint_id}",
395
+ log=False,
396
+ )
397
+ else:
398
+ # Failed to register, don't track
399
+ return None
400
+
401
+ # Store endpoint_id for process_response()
402
+ request._sf_endpoint_id = endpoint_id
403
+
404
+ # Capture request headers if enabled
405
+ if SF_NETWORKHOP_CAPTURE_REQUEST_HEADERS:
406
+ try:
407
+ # Django stores headers in request.META with HTTP_ prefix
408
+ headers = {}
409
+ for key, value in request.META.items():
410
+ if key.startswith("HTTP_"):
411
+ # Remove HTTP_ prefix and convert to standard format
412
+ header_name = key[5:].replace("_", "-")
413
+ headers[header_name] = str(value)
414
+ elif key in ("CONTENT_TYPE", "CONTENT_LENGTH"):
415
+ headers[key.replace("_", "-")] = str(value)
416
+ request._sf_request_headers = headers
417
+ except Exception:
418
+ request._sf_request_headers = None
419
+
420
+ # Capture request body if enabled
421
+ if SF_NETWORKHOP_CAPTURE_REQUEST_BODY:
422
+ try:
423
+ # Read body (Django caches it, so this is safe)
424
+ limit = SF_NETWORKHOP_REQUEST_LIMIT_MB * 1024 * 1024
425
+ body = request.body if hasattr(request, "body") else b""
426
+ if body and len(body) > limit:
427
+ body = body[:limit]
428
+ request._sf_request_body = body if body else None
429
+ except Exception as e:
430
+ if SF_DEBUG and app_config._interceptors_initialized:
431
+ print(f"[[Django]] Failed to capture request body: {e}", log=False)
432
+ request._sf_request_body = None
433
+
434
+ return None
435
+
436
+ # ------------------------------------------------------------------ #
437
+ # 5 | View-level exception hook (unchanged)
438
+ # ------------------------------------------------------------------ #
439
+ def process_response(self, request, response):
440
+ """
441
+ Emit network hop AFTER response is built (OTEL-style zero-overhead).
442
+ Uses pre-registered endpoint_id for ultra-fast C path.
443
+ Captures response headers/body if enabled.
444
+ """
445
+ endpoint_id = getattr(request, "_sf_endpoint_id", None)
446
+
447
+ if SF_DEBUG and app_config._interceptors_initialized:
448
+ print(
449
+ f"[[SailfishMiddleware.process_response]] endpoint_id={endpoint_id}, "
450
+ f"has_endpoint_attr={hasattr(request, '_sf_endpoint_id')}",
451
+ log=False,
452
+ )
453
+
454
+ if endpoint_id is not None and endpoint_id >= 0:
455
+ try:
456
+ _, session_id = get_or_set_sf_trace_id()
457
+
458
+ if SF_DEBUG and app_config._interceptors_initialized:
459
+ print(
460
+ f"[[SailfishMiddleware.process_response]] session_id={session_id}, "
461
+ f"endpoint_id={endpoint_id}, path={request.path}",
462
+ log=False,
463
+ )
464
+
465
+ # Capture response headers if enabled
466
+ resp_headers = None
467
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_HEADERS:
468
+ try:
469
+ resp_headers = dict(response.items())
470
+ except Exception as e:
471
+ if SF_DEBUG and app_config._interceptors_initialized:
472
+ print(
473
+ f"[[Django]] Failed to capture response headers: {e}",
474
+ log=False,
475
+ )
476
+
477
+ # Capture response body if enabled
478
+ resp_body = None
479
+ if SF_NETWORKHOP_CAPTURE_RESPONSE_BODY:
480
+ try:
481
+ limit = SF_NETWORKHOP_RESPONSE_LIMIT_MB * 1024 * 1024
482
+ if hasattr(response, "content"):
483
+ resp_body = response.content[:limit]
484
+ except Exception as e:
485
+ if SF_DEBUG and app_config._interceptors_initialized:
486
+ print(
487
+ f"[[Django]] Failed to capture response body: {e}",
488
+ log=False,
489
+ )
490
+
491
+ # Get request data if captured
492
+ req_headers = getattr(request, "_sf_request_headers", None)
493
+ req_body = getattr(request, "_sf_request_body", None)
494
+
495
+ # Extract raw path and query string for C to parse
496
+ raw_path = request.path # e.g., "/log"
497
+ raw_query = request.META.get("QUERY_STRING", "").encode(
498
+ "utf-8"
499
+ ) # e.g., b"foo=5"
500
+
501
+ if SF_DEBUG and app_config._interceptors_initialized:
502
+ print(
503
+ f"[[Django]] About to emit network hop: endpoint_id={endpoint_id}, "
504
+ f"req_headers={'present' if req_headers else 'None'}, "
505
+ f"req_body={len(req_body) if req_body else 0} bytes, "
506
+ f"resp_headers={'present' if resp_headers else 'None'}, "
507
+ f"resp_body={len(resp_body) if resp_body else 0} bytes",
508
+ log=False,
509
+ )
510
+
511
+ # Direct C call - queues to background worker, returns instantly
512
+ # C will parse route and query_params from raw data
513
+ fast_send_network_hop_fast(
514
+ session_id=session_id,
515
+ endpoint_id=endpoint_id,
516
+ raw_path=raw_path,
517
+ raw_query_string=raw_query,
518
+ request_headers=req_headers,
519
+ request_body=req_body,
520
+ response_headers=resp_headers,
521
+ response_body=resp_body,
522
+ )
523
+
524
+ if SF_DEBUG and app_config._interceptors_initialized:
525
+ print(
526
+ f"[[Django]] Emitted network hop: endpoint_id={endpoint_id} "
527
+ f"session={session_id}",
528
+ log=False,
529
+ )
530
+ except Exception as e: # noqa: BLE001 S110
531
+ if SF_DEBUG and app_config._interceptors_initialized:
532
+ print(f"[[Django]] Failed to emit network hop: {e}", log=False)
533
+
534
+ traceback.print_exc()
535
+
536
+ # Clear function span override for this request (ContextVar cleanup - also syncs C thread-local)
537
+ try:
538
+ clear_funcspan_override()
539
+ except Exception:
540
+ pass
541
+
542
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
543
+ try:
544
+ clear_c_tls_parent_trace_id()
545
+ except Exception:
546
+ pass
547
+
548
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
549
+ # ContextVar does NOT automatically clean up in thread pools - must clear explicitly
550
+ try:
551
+ clear_outbound_header_base()
552
+ except Exception:
553
+ pass
554
+
555
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
556
+ # Without this, get_or_set_sf_trace_id() reuses trace_id from previous request
557
+ # causing X-Sf4-Prid to stay constant when no incoming X-Sf3-Rid header
558
+ try:
559
+ clear_trace_id()
560
+ except Exception:
561
+ pass
562
+
563
+ return response
564
+
565
+ # ------------------------------------------------------------------ #
566
+ # 6 | View-level exception hook (unchanged)
567
+ # ------------------------------------------------------------------ #
568
+ def process_exception(self, request, exception):
569
+ print("[[SailfishMiddleware.process_exception]]", log=False)
570
+ custom_excepthook(type(exception), exception, exception.__traceback__)
571
+
572
+
573
+ # --------------------------------------------------------------------------- #
574
+ # Helper – patch django.core.wsgi.get_wsgi_application once
575
+ # --------------------------------------------------------------------------- #
576
+ # --------------------------------------------------------------------------- #
577
+ # Helper – patch django.core.asgi.get_asgi_application once
578
+ # --------------------------------------------------------------------------- #
579
+ def _patch_get_asgi_application() -> None:
580
+ """
581
+ Replace ``django.core.asgi.get_asgi_application`` with a wrapper that:
582
+
583
+ 1. Runs ``django.setup()`` (as the original does),
584
+ 2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
585
+ *after* settings are configured but *before* the first ``ASGIHandler``
586
+ is built,
587
+ 3. Returns the handler (ASGI handlers handle exceptions internally).
588
+
589
+ This mirrors the WSGI patching approach.
590
+ """
591
+ try:
592
+ from django.core import asgi as _asgi_mod
593
+ except ImportError: # pragma: no cover
594
+ return
595
+
596
+ if getattr(_asgi_mod, "_sf_patched", False):
597
+ return # idempotent
598
+
599
+ _orig_get_asgi = _asgi_mod.get_asgi_application
600
+ _MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
601
+
602
+ def _sf_get_asgi_application(*args, **kwargs):
603
+ # --- Step 1: exactly replicate original behaviour -----------------
604
+ import django
605
+
606
+ django.setup(set_prefix=False) # configures settings & apps
607
+
608
+ # --- Step 2: inject middleware *now* (settings are configured) ----
609
+ from django.conf import settings
610
+
611
+ if (
612
+ hasattr(settings, "MIDDLEWARE")
613
+ and isinstance(settings.MIDDLEWARE, list)
614
+ and _MW_PATH not in settings.MIDDLEWARE
615
+ ):
616
+ settings.MIDDLEWARE.insert(0, _MW_PATH)
617
+ if SF_DEBUG and app_config._interceptors_initialized:
618
+ print(f"[[_patch_get_asgi_application]] Injected {_MW_PATH}", log=False)
619
+
620
+ # --- Step 2.5: inject CORS headers if configured ----
621
+ if hasattr(settings, "CORS_ALLOW_HEADERS"):
622
+ original_headers = settings.CORS_ALLOW_HEADERS
623
+
624
+ if SF_DEBUG and app_config._interceptors_initialized:
625
+ print(
626
+ f"[[_patch_get_asgi_application]] Found CORS_ALLOW_HEADERS: {original_headers}",
627
+ log=False,
628
+ )
629
+
630
+ if should_inject_headers(original_headers):
631
+ patched_headers = inject_sailfish_headers(original_headers)
632
+ settings.CORS_ALLOW_HEADERS = patched_headers
633
+
634
+ if SF_DEBUG and app_config._interceptors_initialized:
635
+ print(
636
+ f"[[_patch_get_asgi_application]] Injected Sailfish headers into CORS_ALLOW_HEADERS: {patched_headers}",
637
+ log=False,
638
+ )
639
+
640
+ # --- Step 3: build and return ASGI handler ----
641
+ return _orig_get_asgi(*args, **kwargs)
642
+
643
+ _asgi_mod.get_asgi_application = _sf_get_asgi_application
644
+ _asgi_mod._sf_patched = True
645
+
646
+
647
+ # --------------------------------------------------------------------------- #
648
+ # Helper – patch django.core.wsgi.get_wsgi_application once
649
+ # --------------------------------------------------------------------------- #
650
+ def _patch_get_wsgi_application() -> None:
651
+ """
652
+ Replace ``django.core.wsgi.get_wsgi_application`` with a wrapper that:
653
+
654
+ 1. Runs ``django.setup()`` (as the original does),
655
+ 2. **Then** injects ``SailfishMiddleware`` into *settings.MIDDLEWARE*
656
+ *after* settings are configured but *before* the first ``WSGIHandler``
657
+ is built,
658
+ 3. Wraps the returned handler in our ``CustomExceptionMiddleware`` so we
659
+ still have a last-chance catcher outside Django's stack.
660
+
661
+ This mirrors the flow used by Sentry's Django integration.
662
+ """
663
+ try:
664
+ from django.core import wsgi as _wsgi_mod
665
+ except ImportError: # pragma: no cover
666
+ return
667
+
668
+ if getattr(_wsgi_mod, "_sf_patched", False):
669
+ return # idempotent
670
+
671
+ _orig_get_wsgi = _wsgi_mod.get_wsgi_application
672
+ _MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
673
+
674
+ def _sf_get_wsgi_application(*args, **kwargs):
675
+ # --- Step 1: exactly replicate original behaviour -----------------
676
+ import django
677
+
678
+ django.setup(set_prefix=False) # configures settings & apps
679
+
680
+ # --- Step 2: inject middleware *now* (settings are configured) ----
681
+ from django.conf import settings
682
+
683
+ if (
684
+ hasattr(settings, "MIDDLEWARE")
685
+ and isinstance(settings.MIDDLEWARE, list)
686
+ and _MW_PATH not in settings.MIDDLEWARE
687
+ ):
688
+ settings.MIDDLEWARE.insert(0, _MW_PATH)
689
+
690
+ # --- Step 2.5: inject CORS headers if configured ----
691
+ if hasattr(settings, "CORS_ALLOW_HEADERS"):
692
+ original_headers = settings.CORS_ALLOW_HEADERS
693
+
694
+ if SF_DEBUG and app_config._interceptors_initialized:
695
+ print(
696
+ f"[[_patch_get_wsgi_application]] Found CORS_ALLOW_HEADERS: {original_headers}",
697
+ log=False,
698
+ )
699
+
700
+ if should_inject_headers(original_headers):
701
+ patched_headers = inject_sailfish_headers(original_headers)
702
+ settings.CORS_ALLOW_HEADERS = patched_headers
703
+
704
+ if SF_DEBUG and app_config._interceptors_initialized:
705
+ print(
706
+ f"[[_patch_get_wsgi_application]] Injected Sailfish headers into CORS_ALLOW_HEADERS: {patched_headers}",
707
+ log=False,
708
+ )
709
+
710
+ # --- Step 3: build handler and wrap for last-chance exceptions ----
711
+ from django.core.handlers.wsgi import WSGIHandler
712
+ from sf_veritas.patches.web_frameworks.django import CustomExceptionMiddleware
713
+
714
+ handler = WSGIHandler()
715
+ return CustomExceptionMiddleware(handler)
716
+
717
+ _wsgi_mod.get_wsgi_application = _sf_get_wsgi_application
718
+ _wsgi_mod._sf_patched = True
719
+
720
+
721
+ def patch_django_middleware(routes_to_skip: Optional[List[str]] = None) -> None:
722
+ """
723
+ Public entry-point called by ``setup_interceptors``.
724
+
725
+ • Inserts ``SailfishMiddleware`` for *already-configured* settings
726
+ (run-server or ASGI).
727
+ • Patches ``get_wsgi_application`` so *future* WSGI handlers created
728
+ by third-party code inherit the middleware without relying on a
729
+ configured settings object at import time.
730
+ """
731
+ global _ROUTES_TO_SKIP
732
+ _ROUTES_TO_SKIP = routes_to_skip or []
733
+
734
+ try:
735
+ from django.conf import settings
736
+ from django.core.exceptions import ImproperlyConfigured
737
+ except ImportError: # Django not installed
738
+ return
739
+
740
+ _MW_PATH = "sf_veritas.patches.web_frameworks.django.SailfishMiddleware"
741
+
742
+ # ---------- If settings are *already* configured, patch immediately ---
743
+ try:
744
+ if settings.configured and isinstance(
745
+ getattr(settings, "MIDDLEWARE", None), list
746
+ ):
747
+ if _MW_PATH not in settings.MIDDLEWARE:
748
+ settings.MIDDLEWARE.insert(0, _MW_PATH)
749
+ except ImproperlyConfigured:
750
+ # Settings not yet configured – safe to ignore; the WSGI patch below
751
+ # will handle insertion once ``django.setup()`` runs.
752
+ pass
753
+
754
+ # ---------- Always patch get_wsgi/asgi_application (idempotent) ------------
755
+ _patch_get_wsgi_application()
756
+ _patch_get_asgi_application()
757
+
758
+ # ---------- Patch CORS to inject Sailfish headers (idempotent) ------------
759
+ patch_django_cors()
760
+
761
+ if SF_DEBUG and app_config._interceptors_initialized:
762
+ print(
763
+ "[[patch_django_middleware]] Sailfish Django integration ready", log=False
764
+ )
765
+
766
+
767
+ class CustomExceptionMiddleware:
768
+ """
769
+ A universal last-chance exception wrapper that works for either
770
+ • ASGI call signature: (scope, receive, send) → coroutine
771
+ • WSGI call signature: (environ, start_response) → iterable
772
+ Every un-handled exception is funneled through ``custom_excepthook`` once.
773
+ """
774
+
775
+ def __init__(self, app):
776
+ self.app = app
777
+
778
+ # ------------------------------------------------------------------ #
779
+ # Dispatcher – routes ASGI vs WSGI based on arity / argument shape
780
+ # ------------------------------------------------------------------ #
781
+ def __call__(self, *args, **kwargs):
782
+ if len(args) == 3:
783
+ # Heuristic: (scope, receive, send) for ASGI
784
+ return self._asgi_call(*args) # returns coroutine
785
+ # Else assume classic WSGI: (environ, start_response)
786
+ return self._wsgi_call(*args) # returns iterable
787
+
788
+ # ------------------------------------------------------------------ #
789
+ # ASGI branch
790
+ # ------------------------------------------------------------------ #
791
+ async def _asgi_call(self, scope, receive, send):
792
+ try:
793
+ await self.app(scope, receive, send)
794
+ except Exception as exc: # noqa: BLE001
795
+ custom_excepthook(type(exc), exc, exc.__traceback__)
796
+ raise
797
+ finally:
798
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
799
+ try:
800
+ clear_c_tls_parent_trace_id()
801
+ except Exception:
802
+ pass
803
+
804
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
805
+ try:
806
+ clear_outbound_header_base()
807
+ except Exception:
808
+ pass
809
+
810
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
811
+ try:
812
+ clear_trace_id()
813
+ except Exception:
814
+ pass
815
+
816
+ # ------------------------------------------------------------------ #
817
+ # WSGI branch
818
+ # ------------------------------------------------------------------ #
819
+ def _wsgi_call(self, environ, start_response):
820
+ try:
821
+ return self.app(environ, start_response)
822
+ except Exception as exc: # noqa: BLE001
823
+ custom_excepthook(type(exc), exc, exc.__traceback__)
824
+ raise
825
+ finally:
826
+ # CRITICAL: Clear C TLS to prevent stale data in thread pools
827
+ try:
828
+ clear_c_tls_parent_trace_id()
829
+ except Exception:
830
+ pass
831
+
832
+ # CRITICAL: Clear outbound header base to prevent stale cached headers
833
+ try:
834
+ clear_outbound_header_base()
835
+ except Exception:
836
+ pass
837
+
838
+ # CRITICAL: Clear trace_id to ensure fresh generation for next request
839
+ try:
840
+ clear_trace_id()
841
+ except Exception:
842
+ pass
843
+
844
+ # ------------------------------------------------------------------ #
845
+ # Delegate attribute access so the wrapped app still behaves normally
846
+ # ------------------------------------------------------------------ #
847
+ def __getattr__(self, attr):
848
+ return getattr(self.app, attr)
849
+
850
+
851
+ # --------------------------------------------------------------------------- #
852
+ # CORS Header Injection – django-cors-headers
853
+ # --------------------------------------------------------------------------- #
854
+ def patch_django_cors():
855
+ """
856
+ Patch django-cors-headers to automatically inject Sailfish headers.
857
+
858
+ Two-pronged approach:
859
+ 1. Directly modify Django settings.CORS_ALLOW_HEADERS if already configured
860
+ 2. Patch corsheaders.conf property to inject headers dynamically
861
+
862
+ SAFE: Only modifies CORS if django-cors-headers is installed and configured.
863
+ """
864
+ try:
865
+ from django.conf import settings
866
+ except ImportError:
867
+ if SF_DEBUG and app_config._interceptors_initialized:
868
+ print(
869
+ "[[patch_django_cors]] Django not available, skipping",
870
+ log=False,
871
+ )
872
+ return
873
+
874
+ try:
875
+ from corsheaders import conf as cors_conf
876
+ except ImportError:
877
+ # django-cors-headers not installed, skip patching
878
+ if SF_DEBUG and app_config._interceptors_initialized:
879
+ print(
880
+ "[[patch_django_cors]] django-cors-headers not installed, skipping",
881
+ log=False,
882
+ )
883
+ return
884
+
885
+ # Check if already patched
886
+ if hasattr(cors_conf, "_sf_cors_patched"):
887
+ if SF_DEBUG and app_config._interceptors_initialized:
888
+ print("[[patch_django_cors]] Already patched, skipping", log=False)
889
+ return
890
+
891
+ # APPROACH 1: Directly modify Django settings if CORS is configured
892
+ try:
893
+ if hasattr(settings, "CORS_ALLOW_HEADERS"):
894
+ original_headers = settings.CORS_ALLOW_HEADERS
895
+
896
+ if SF_DEBUG and app_config._interceptors_initialized:
897
+ print(
898
+ f"[[patch_django_cors]] Found CORS_ALLOW_HEADERS in settings: {original_headers}",
899
+ log=False,
900
+ )
901
+
902
+ if should_inject_headers(original_headers):
903
+ patched_headers = inject_sailfish_headers(original_headers)
904
+ settings.CORS_ALLOW_HEADERS = patched_headers
905
+
906
+ if SF_DEBUG and app_config._interceptors_initialized:
907
+ print(
908
+ f"[[patch_django_cors]] Modified settings.CORS_ALLOW_HEADERS to: {patched_headers}",
909
+ log=False,
910
+ )
911
+ except Exception as e:
912
+ if SF_DEBUG and app_config._interceptors_initialized:
913
+ print(f"[[patch_django_cors]] Failed to modify settings: {e}", log=False)
914
+
915
+ # APPROACH 2: Patch the Conf class property for dynamic access
916
+ try:
917
+ conf_class = type(cors_conf)
918
+
919
+ if hasattr(conf_class, "CORS_ALLOW_HEADERS"):
920
+ original_property = getattr(conf_class, "CORS_ALLOW_HEADERS")
921
+
922
+ if isinstance(original_property, property):
923
+ original_fget = original_property.fget
924
+
925
+ def patched_fget(self):
926
+ original_headers = original_fget(self)
927
+
928
+ if should_inject_headers(original_headers):
929
+ patched_headers = inject_sailfish_headers(original_headers)
930
+
931
+ if SF_DEBUG and app_config._interceptors_initialized:
932
+ print(
933
+ f"[[patch_django_cors]] Property access: injected headers -> {patched_headers}",
934
+ log=False,
935
+ )
936
+
937
+ return patched_headers
938
+
939
+ return original_headers
940
+
941
+ setattr(
942
+ conf_class,
943
+ "CORS_ALLOW_HEADERS",
944
+ property(
945
+ patched_fget, original_property.fset, original_property.fdel
946
+ ),
947
+ )
948
+
949
+ if SF_DEBUG and app_config._interceptors_initialized:
950
+ print(
951
+ "[[patch_django_cors]] Successfully patched CORS_ALLOW_HEADERS property",
952
+ log=False,
953
+ )
954
+ except Exception as e:
955
+ if SF_DEBUG and app_config._interceptors_initialized:
956
+ print(f"[[patch_django_cors]] Failed to patch property: {e}", log=False)
957
+
958
+ cors_conf._sf_cors_patched = True
959
+
960
+ if SF_DEBUG and app_config._interceptors_initialized:
961
+ print(
962
+ "[[patch_django_cors]] Successfully patched django-cors-headers", log=False
963
+ )