d3107b1
commit d3107b1
1 file changed
+38 -8
Showing 1 changed file+38-8
▼crates/justapi-py/src/native/handlers.rs
+38-8
| @@ -85,6 +85,7 @@ pub(crate) fn call_python_handler( | ||
85 | 85 | app: Option<Py<PyAny>>, |
86 | 86 | needs_request: bool, |
87 | 87 | http_version: String, |
88 | + auth_claims: Option<String>, | |
88 | 89 | ) -> NativeResponse { |
89 | 90 | let helper = get_helper(py); |
90 | 91 | |
| @@ -167,6 +168,7 @@ pub(crate) fn call_python_handler( | ||
167 | 168 | client, |
168 | 169 | app, |
169 | 170 | http_version, |
171 | + auth_claims, | |
170 | 172 | ); |
171 | 173 | if let Some(pb) = parsed_body { |
172 | 174 | req.set_parsed_body(py, pb.into_bound(py)); |
| @@ -305,13 +307,28 @@ fn trace_enabled() -> bool { | ||
305 | 307 | /// Cached `orjson.dumps(obj, default=str)` (or `json.dumps` fallback) callable, |
306 | 308 | /// resolved once on first use. Mirrors `_native_helper._dumps` but lets Rust |
307 | 309 | /// invoke orjson directly, skipping the Python `wrap_result` bytecode. |
310 | +/// | |
311 | +/// When the `orjson` Cargo feature is enabled, `orjson` is required — the | |
312 | +/// function will panic at first call if the Python `orjson` module is not | |
313 | +/// available. Without the feature, it silently falls back to `json.dumps`. | |
308 | 314 | pub(crate) fn fast_dumps(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>> { |
309 | 315 | static DUMPS: std::sync::OnceLock<Py<PyAny>> = std::sync::OnceLock::new(); |
316 | + | |
317 | + #[cfg(not(feature = "orjson"))] | |
310 | 318 | let expr = if py.import("orjson").is_ok() { |
311 | 319 | "lambda o: __import__('orjson').dumps(o, default=str)" |
312 | 320 | } else { |
313 | 321 | "lambda o: __import__('json').dumps(o, default=str).encode('utf-8')" |
314 | 322 | }; |
323 | + | |
324 | + #[cfg(feature = "orjson")] | |
325 | + let expr = { | |
326 | + let _ = py.import("orjson").unwrap_or_else(|e| { | |
327 | + panic!("orjson feature enabled but Python 'orjson' module not available: {e}") | |
328 | + }); | |
329 | + "lambda o: __import__('orjson').dumps(o, default=str)" | |
330 | + }; | |
331 | + | |
315 | 332 | // SAFETY: globals/locals are left as None, which Python fills with the |
316 | 333 | // builtins dict, so `__import__` and `str` are available. The cached |
317 | 334 | // lambda borrows no local state and is leaked for the process lifetime. |
| @@ -725,6 +742,13 @@ where | ||
725 | 742 | .map(|s| s.to_string()); |
726 | 743 | let is_multipart = |
727 | 744 | content_type.as_deref().unwrap_or("").starts_with("multipart/form-data"); |
745 | + | |
746 | + // Extract JWT claims from the Rust middleware before consuming the | |
747 | + // request body (which moves `req`). The claims are stored as a | |
748 | + // `serde_json::Value` in `req.extensions()` by `JwtAuth` middleware. | |
749 | + let auth_claims: Option<String> = | |
750 | + req.extensions().get::<serde_json::Value>().map(|v| v.to_string()); | |
751 | + | |
728 | 752 | let req_body = req.into_body(); |
729 | 753 | let (body_bytes, multipart_form_res) = if is_multipart { |
730 | 754 | let ct = content_type.unwrap(); |
| @@ -913,12 +937,12 @@ where | ||
913 | 937 | if let Some(Ok(form)) = multipart_form_res { |
914 | 938 | let d = pyo3::types::PyDict::new(py); |
915 | 939 | for (k, v) in form.fields.iter() { |
916 | - d.set_item(k, v).unwrap(); | |
940 | + d.set_item(k, v).ok(); | |
917 | 941 | } |
918 | 942 | for f in form.files.iter() { |
919 | 943 | let headers_dict = pyo3::types::PyDict::new(py); |
920 | 944 | for (k, v) in f.headers.iter() { |
921 | - headers_dict.set_item(k, v).unwrap(); | |
945 | + headers_dict.set_item(k, v).ok(); | |
922 | 946 | } |
923 | 947 | let upload_file = crate::multipart::UploadFile::new( |
924 | 948 | f.filename.clone().unwrap_or_default(), |
| @@ -927,8 +951,10 @@ where | ||
927 | 951 | headers_dict.into(), |
928 | 952 | f.temp_path.clone(), |
929 | 953 | ); |
930 | - let p = pyo3::Bound::new(py, upload_file).unwrap(); | |
931 | - d.set_item(&f.field_name, p).unwrap(); | |
954 | + let Ok(p) = pyo3::Bound::new(py, upload_file) else { | |
955 | + continue; | |
956 | + }; | |
957 | + d.set_item(&f.field_name, p).ok(); | |
932 | 958 | } |
933 | 959 | form_dict_py = Some(d.into()); |
934 | 960 | } |
| @@ -951,6 +977,7 @@ where | ||
951 | 977 | app.as_ref().as_ref().map(|a| a.clone_ref(py)), |
952 | 978 | needs_request_clone[handler_id], |
953 | 979 | http_version.clone(), |
980 | + auth_claims, | |
954 | 981 | ) |
955 | 982 | }) |
956 | 983 | .await |
| @@ -1176,12 +1203,12 @@ where | ||
1176 | 1203 | if let Some(Ok(form)) = &multipart_form_res { |
1177 | 1204 | let d = pyo3::types::PyDict::new(py); |
1178 | 1205 | for (k, v) in form.fields.iter() { |
1179 | - d.set_item(k, v).unwrap(); | |
1206 | + d.set_item(k, v).ok(); | |
1180 | 1207 | } |
1181 | 1208 | for f in form.files.iter() { |
1182 | 1209 | let headers_dict = pyo3::types::PyDict::new(py); |
1183 | 1210 | for (k, v) in f.headers.iter() { |
1184 | - headers_dict.set_item(k, v).unwrap(); | |
1211 | + headers_dict.set_item(k, v).ok(); | |
1185 | 1212 | } |
1186 | 1213 | let upload_file = crate::multipart::UploadFile::new( |
1187 | 1214 | f.filename.clone().unwrap_or_default(), |
| @@ -1190,8 +1217,10 @@ where | ||
1190 | 1217 | headers_dict.into(), |
1191 | 1218 | f.temp_path.clone(), |
1192 | 1219 | ); |
1193 | - let p = pyo3::Bound::new(py, upload_file).unwrap(); | |
1194 | - d.set_item(&f.field_name, p).unwrap(); | |
1220 | + let Ok(p) = pyo3::Bound::new(py, upload_file) else { | |
1221 | + continue; | |
1222 | + }; | |
1223 | + d.set_item(&f.field_name, p).ok(); | |
1195 | 1224 | } |
1196 | 1225 | form_dict_py = Some(d.into()); |
1197 | 1226 | } |
| @@ -1214,6 +1243,7 @@ where | ||
1214 | 1243 | app.as_ref().as_ref().map(|a| a.clone_ref(py)), |
1215 | 1244 | needs_request_clone[handler_id], |
1216 | 1245 | http_version.clone(), |
1246 | + None, // auth_claims: test client has no middleware | |
1217 | 1247 | ) |
1218 | 1248 | }) |
1219 | 1249 | .await |
1220 | 1250 | |