Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions oxyroute/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ def include_router(
kw: dict[str, Any] = {k: v for k, v in merged.items() if k in allowed}
reg(self, full, **kw)(handler)

def add_exception_handler(
self, exc_type: type[BaseException], handler: Callable[..., Any]
) -> None:
"""
Register a global exception handler for a specific exception type.
"""
self._app.add_exception_handler(exc_type, handler)

def set_middleware(self, handler: Callable[..., Any] | None) -> None:
"""
One optional pre-route callback ``(scope, protocol)`` — return ``None`` to pass through.
Expand Down
201 changes: 183 additions & 18 deletions src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use jsonwebtoken::errors::ErrorKind;
use jsonwebtoken::{decode, Validation};
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyTuple};
use pyo3::IntoPyObjectExt;
use serde_json::Value as JsonValue;

use crate::config;
Expand Down Expand Up @@ -164,36 +165,76 @@ fn send_python_error_sync(
method: &str,
path: &str,
err: PyErr,
scope: Option<&pyo3::Bound<'_, PyAny>>,
state: Option<&std::sync::Arc<parking_lot::RwLock<crate::state::AppState>>>,
) -> PyResult<()> {
if let (Some(sc), Some(st)) = (scope, state) {
let snap = st.read().hot_snapshot();
for (exc_type, handler, is_async) in snap.exception_handlers.iter().rev() {
if let Ok(exc_obj) = err.clone_ref(py).into_bound_py_any(py) {
if exc_obj.is_instance(exc_type.bind(py)).unwrap_or(false) {
if *is_async {
log::error!(
"Async exception handler cannot be called in sync route fallback: {}",
method
);
continue;
}
let exc_obj_any = exc_obj.clone().into_any();
if let Ok(res) = handler.bind(py).call1((sc.clone(), exc_obj_any)) {
match map_handler_return(py, &res.clone().unbind()) {
Ok(mapped) => {
return send_handler_map_inline(
py,
protocol,
method == "HEAD",
mapped,
)
}
Err(e) => {
log::error!(
"Exception handler returned invalid type or map failed: {:?}",
e
);
}
}
}
}
}
}
}
if try_http_exception_sync(py, protocol, &err)? {
return Ok(());
}
send_internal_error_sync(py, protocol, method, path, err)
}

#[allow(clippy::too_many_arguments)]
fn run_trivial_sync_route(
py: Python<'_>,
protocol: &Py<PyAny>,
method: &str,
path: &str,
is_head: bool,
entry: &RouteEntry,
scope: &pyo3::Bound<'_, PyAny>,
state: &std::sync::Arc<parking_lot::RwLock<crate::state::AppState>>,
) -> PyResult<()> {
let handler = entry.handler.bind(py);
let out = match handler.call0() {
Ok(x) => x.unbind(),
Err(e) => {
return send_python_error_sync(py, protocol, method, path, e);
return send_python_error_sync(py, protocol, method, path, e, Some(scope), Some(state));
}
};
let mapped = match map_handler_return(py, &out) {
Ok(m) => m,
Err(e) => {
return send_python_error_sync(py, protocol, method, path, e);
return send_python_error_sync(py, protocol, method, path, e, Some(scope), Some(state));
}
};
if let Err(e) = send_handler_map_inline(py, protocol, is_head, mapped) {
return send_python_error_sync(py, protocol, method, path, e);
return send_python_error_sync(py, protocol, method, path, e, Some(scope), Some(state));
}
Ok(())
}
Expand All @@ -204,7 +245,59 @@ async fn send_python_error(
method: &str,
path: &str,
err: PyErr,
scope: Option<&Py<PyAny>>,
state: Option<&std::sync::Arc<parking_lot::RwLock<crate::state::AppState>>>,
) -> PyResult<PyObject> {
if let (Some(sc), Some(st)) = (scope, state) {
let snap = st.read().hot_snapshot();
let coro_or_res = Python::with_gil(|py| -> PyResult<Option<(Py<PyAny>, bool)>> {
for (exc_type, handler, is_async) in snap.exception_handlers.iter().rev() {
if let Ok(exc_obj) = err.clone_ref(py).into_bound_py_any(py) {
if exc_obj.is_instance(exc_type.bind(py)).unwrap_or(false) {
let exc_obj_any = exc_obj.clone().into_any();
if let Ok(res) = handler.bind(py).call1((sc.bind(py).clone(), exc_obj_any))
{
return Ok(Some((res.unbind(), *is_async)));
}
}
}
}
Ok(None)
});

if let Ok(Some((res_py, is_async))) = coro_or_res {
let final_res = if is_async {
let fut = Python::with_gil(|py| {
pyo3_async_runtimes::tokio::into_future(res_py.bind(py).clone())
});
if let Ok(f) = fut {
match f.await {
Ok(x) => x,
Err(e) => return send_internal_error(protocol, method, path, e).await,
}
} else {
res_py
}
} else {
res_py
};

let mapped_res = Python::with_gil(|py| map_handler_return(py, &final_res));
if let Ok(mapped) = mapped_res {
let res = Python::with_gil(|py| {
send_handler_map_inline(py, protocol, method == "HEAD", mapped)
});
if res.is_ok() {
return Ok(Python::with_gil(|py| py.None()));
}
} else {
log::error!(
"Async exception handler returned invalid type or map failed: {:?}",
mapped_res.err()
);
}
}
}
if let Some(res) = try_http_exception(protocol, &err).await? {
return Ok(res);
}
Expand Down Expand Up @@ -298,7 +391,16 @@ pub fn try_rsgi_sync_short_circuit(
return Err(pyo3::exceptions::PyRuntimeError::new_err("route index"));
};
if route_is_trivial_sync(entry) {
run_trivial_sync_route(py, &protocol_py, &method, &path, is_head, entry)?;
run_trivial_sync_route(
py,
&protocol_py,
&method,
&path,
is_head,
entry,
scope,
state,
)?;
return Ok(Some(py.None()));
}
Ok(None)
Expand Down Expand Up @@ -420,7 +522,8 @@ pub async fn run_rsgi(
}) {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await;
}
};
let skip = Python::with_gil(|py| out.bind(py).is_none());
Expand Down Expand Up @@ -493,7 +596,10 @@ pub async fn run_rsgi(
send_handler_map_inline(py, &protocol, is_head, mapped)
}) {
Ok(()) => Ok(Python::with_gil(|py| py.None())),
Err(e) => send_python_error(&protocol, &method, &path, e).await,
Err(e) => {
send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await
}
};
}
}
Expand Down Expand Up @@ -755,7 +861,15 @@ pub async fn run_rsgi(
let ct = match ct {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(
&protocol,
&method,
&path,
e,
Some(&scope),
Some(&state),
)
.await;
}
};
if ct.as_deref().map(str::is_empty) != Some(false) {
Expand Down Expand Up @@ -820,7 +934,8 @@ pub async fn run_rsgi(
}) {
Ok(o) => Some(o),
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await;
}
}
} else {
Expand Down Expand Up @@ -848,7 +963,15 @@ pub async fn run_rsgi(
}) {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(
&protocol,
&method,
&path,
e,
Some(&scope),
Some(&state),
)
.await;
}
};
let fut = match Python::with_gil(|py| {
Expand All @@ -857,13 +980,29 @@ pub async fn run_rsgi(
}) {
Ok(f) => f,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(
&protocol,
&method,
&path,
e,
Some(&scope),
Some(&state),
)
.await;
}
};
match fut.await {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(
&protocol,
&method,
&path,
e,
Some(&scope),
Some(&state),
)
.await;
}
}
} else {
Expand All @@ -886,7 +1025,15 @@ pub async fn run_rsgi(
}) {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(
&protocol,
&method,
&path,
e,
Some(&scope),
Some(&state),
)
.await;
}
}
};
Expand All @@ -905,7 +1052,17 @@ pub async fn run_rsgi(
if let Some(pool) = snapshot.db_pool.as_ref() {
match crate::db::execute_query(pool, &db_query).await {
Ok(res) => res,
Err(e) => return send_python_error(&protocol, &method, &path, e).await,
Err(e) => {
return send_python_error(
&protocol,
&method,
&path,
e,
Some(&scope),
Some(&state),
)
.await
}
}
} else {
return send_python_error(
Expand All @@ -915,12 +1072,17 @@ pub async fn run_rsgi(
pyo3::exceptions::PyRuntimeError::new_err(
"DBQuery returned by dependency but no database pool configured",
),
Some(&scope),
Some(&state),
)
.await;
}
}
Ok(None) => o,
Err(e) => return send_python_error(&protocol, &method, &path, e).await,
Err(e) => {
return send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await
}
};

dep_out.push(resolved);
Expand Down Expand Up @@ -1009,7 +1171,8 @@ pub async fn run_rsgi(
}) {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await;
}
};
let handler_out: PyObject = if run_async {
Expand All @@ -1019,13 +1182,15 @@ pub async fn run_rsgi(
}) {
Ok(f) => f,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await;
}
};
match fut.await {
Ok(x) => x,
Err(e) => {
return send_python_error(&protocol, &method, &path, e).await;
return send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state))
.await;
}
}
} else {
Expand Down Expand Up @@ -1100,7 +1265,7 @@ pub async fn run_rsgi(
send_handler_map_inline(py, &protocol, is_head, mapped)
}) {
Ok(()) => Ok(Python::with_gil(|py| py.None())),
Err(e) => send_python_error(&protocol, &method, &path, e).await,
Err(e) => send_python_error(&protocol, &method, &path, e, Some(&scope), Some(&state)).await,
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,26 @@ impl App {

/// Single optional pre-route hook. Return ``None`` to continue; otherwise the return value
/// is mapped like a route handler (e.g. :class:`oxyroute.Response`, ``dict`` with ``status`` / ``body`` / ``headers``).

#[pyo3(signature = (exc_type, handler))]
fn add_exception_handler(
&self,
exc_type: pyo3::Bound<'_, pyo3::types::PyType>,
handler: Py<PyAny>,
) -> PyResult<()> {
let mut st = self.state.write();
let is_async = pyo3::Python::with_gil(|py| -> PyResult<bool> {
let inspect = py.import("inspect")?;
inspect
.getattr("iscoroutinefunction")?
.call1((&handler,))?
.extract::<bool>()
})
.unwrap_or(false);
Arc::make_mut(&mut st.exception_handlers).push((exc_type.unbind(), handler, is_async));
Ok(())
}

fn set_middleware(&self, handler: Option<Py<PyAny>>) -> PyResult<()> {
let mut st = self.state.write();
if let Some(h) = handler {
Expand Down
Loading
Loading