@@ -65,6 +65,12 @@ async def _call_next(request):
6565 return _call_next
6666
6767
68+ @pytest .fixture (autouse = True )
69+ def reset_rust_request_logging_module_state (monkeypatch ):
70+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware._RUST_REQUEST_LOGGING_MODULE" , None )
71+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware._RUST_REQUEST_LOGGING_IMPORT_FAILED" , False )
72+
73+
6874def make_request (body : bytes = b"{}" , headers = None , query_params = None ):
6975 scope : Scope = {
7076 "type" : "http" ,
@@ -125,16 +131,42 @@ def test_mask_sensitive_data_ignores_empty_normalized_keys():
125131 assert masked ["password" ] == "******"
126132
127133
128- def test_mask_sensitive_data_uses_pyo3_module_when_enabled (monkeypatch ):
129- rust_module = MagicMock ()
130- rust_module .mask_sensitive_data .return_value = {"password" : "******" , "username" : "user" } # pragma: allowlist secret
134+ def test_mask_sensitive_data_uses_native_extension_when_enabled (monkeypatch ):
135+ native_extension = MagicMock ()
136+ native_extension .mask_sensitive_data .return_value = {"password" : "******" , "username" : "user" } # pragma: allowlist secret
131137 monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled" , True , raising = False )
132138
133- with patch ("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module" , return_value = rust_module ):
139+ with patch ("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module" , return_value = native_extension ):
134140 masked = mask_sensitive_data ({"password" : "secret" , "username" : "user" }) # pragma: allowlist secret
135141
136142 assert masked == {"password" : "******" , "username" : "user" } # pragma: allowlist secret
137- rust_module .mask_sensitive_data .assert_called_once_with ({"password" : "secret" , "username" : "user" }, 10 ) # pragma: allowlist secret
143+ native_extension .mask_sensitive_data .assert_called_once_with ({"password" : "secret" , "username" : "user" }, 10 ) # pragma: allowlist secret
144+
145+
146+ def test_mask_sensitive_data_falls_back_to_python_when_native_extension_import_fails (monkeypatch , dummy_logger ):
147+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled" , True , raising = False )
148+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.importlib.import_module" , MagicMock (side_effect = ImportError ("boom" )))
149+
150+ masked = mask_sensitive_data ({"password" : "secret" , "username" : "user" }) # pragma: allowlist secret
151+
152+ assert masked == {"password" : "******" , "username" : "user" } # pragma: allowlist secret
153+ assert len (dummy_logger .warnings ) == 1
154+ assert "falling back to Python masking" in dummy_logger .warnings [0 ]
155+ assert "native extension" in dummy_logger .warnings [0 ]
156+
157+
158+ def test_mask_sensitive_data_caches_failed_native_extension_import (monkeypatch , dummy_logger ):
159+ import_module = MagicMock (side_effect = ImportError ("boom" ))
160+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled" , True , raising = False )
161+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.importlib.import_module" , import_module )
162+
163+ first = mask_sensitive_data ({"password" : "secret" }) # pragma: allowlist secret
164+ second = mask_sensitive_data ({"password" : "secret" }) # pragma: allowlist secret
165+
166+ assert first == {"password" : "******" } # pragma: allowlist secret
167+ assert second == {"password" : "******" } # pragma: allowlist secret
168+ assert import_module .call_count == 1
169+ assert len (dummy_logger .warnings ) == 1
138170
139171
140172# --- mask_jwt_in_cookies tests ---
@@ -196,16 +228,26 @@ def test_mask_sensitive_headers_respects_non_sensitive_suffixes():
196228 assert masked ["X-JWT_Status_Count" ] == "7"
197229
198230
199- def test_mask_sensitive_headers_uses_pyo3_module_when_enabled (monkeypatch ):
200- rust_module = MagicMock ()
201- rust_module .mask_sensitive_headers .return_value = {"Authorization" : "******" }
231+ def test_mask_sensitive_headers_uses_native_extension_when_enabled (monkeypatch ):
232+ native_extension = MagicMock ()
233+ native_extension .mask_sensitive_headers .return_value = {"Authorization" : "******" }
202234 monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled" , True , raising = False )
203235
204- with patch ("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module" , return_value = rust_module ):
236+ with patch ("mcpgateway.middleware.request_logging_middleware._load_rust_request_logging_module" , return_value = native_extension ):
205237 masked = mask_sensitive_headers ({"Authorization" : "Bearer abc" })
206238
207239 assert masked == {"Authorization" : "******" }
208- rust_module .mask_sensitive_headers .assert_called_once_with ({"Authorization" : "Bearer abc" })
240+ native_extension .mask_sensitive_headers .assert_called_once_with ({"Authorization" : "Bearer abc" })
241+
242+
243+ def test_mask_sensitive_headers_fall_back_to_python_when_native_extension_import_fails (monkeypatch , dummy_logger ):
244+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled" , True , raising = False )
245+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.importlib.import_module" , MagicMock (side_effect = ImportError ("boom" )))
246+
247+ masked = mask_sensitive_headers ({"Authorization" : "Bearer abc" , "X-Trace-Id" : "123" })
248+
249+ assert masked == {"Authorization" : "******" , "X-Trace-Id" : "123" }
250+ assert len (dummy_logger .warnings ) == 1
209251
210252
211253# --- RequestLoggingMiddleware tests ---
@@ -222,6 +264,23 @@ async def test_dispatch_logs_json_body(dummy_logger, mock_structured_logger, dum
222264 assert "******" in dummy_logger .logged [0 ][1 ]
223265
224266
267+ @pytest .mark .asyncio
268+ async def test_dispatch_falls_back_to_python_masking_when_native_extension_import_fails (dummy_logger , mock_structured_logger , dummy_call_next , monkeypatch ):
269+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.settings.experimental_rust_request_logging_masking_enabled" , True , raising = False )
270+ monkeypatch .setattr ("mcpgateway.middleware.request_logging_middleware.importlib.import_module" , MagicMock (side_effect = ImportError ("boom" )))
271+
272+ middleware = RequestLoggingMiddleware (app = None , enable_gateway_logging = False , log_detailed_requests = True )
273+ body = orjson .dumps ({"password" : "123" , "data" : "ok" })
274+ request = make_request (body = body , headers = {"Authorization" : "Bearer abc" })
275+
276+ response = await middleware .dispatch (request , dummy_call_next )
277+
278+ assert response .status_code == 200
279+ assert any ("📩 Incoming request" in msg for _ , msg in dummy_logger .logged )
280+ assert "******" in dummy_logger .logged [0 ][1 ]
281+ assert any ("falling back to Python masking" in warning for warning in dummy_logger .warnings )
282+
283+
225284@pytest .mark .asyncio
226285async def test_dispatch_logs_non_json_body (dummy_logger , mock_structured_logger , dummy_call_next ):
227286 middleware = RequestLoggingMiddleware (app = None , enable_gateway_logging = False , log_detailed_requests = True )
0 commit comments