use std::fmt;
use std::sync::{Arc, Mutex};
use pyo3::exceptions::PyTypeError;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use eggfetch_core::trace::{event_to_httpcore_name, OnEventAction, TraceEvent, TraceObserver};
#[derive(Debug)]
pub(crate) enum TraceBridgeError {
Callback(PyErr),
NotAwaited,
}
impl fmt::Display for TraceBridgeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Callback(_) => f.write_str("trace callback raised an exception"),
Self::NotAwaited => f.write_str(
"async trace callback was not awaited; sync transport cannot drive coroutines",
),
}
}
}
impl std::error::Error for TraceBridgeError {}
#[derive(Clone)]
pub(crate) struct CallbackErrorSlot {
inner: Arc<Mutex<Option<TraceBridgeError>>>,
}
impl CallbackErrorSlot {
pub(crate) fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(None)),
}
}
fn record(&self, err: TraceBridgeError) {
if let Ok(mut guard) = self.inner.lock() {
if guard.is_none() {
*guard = Some(err);
}
}
}
pub(crate) fn take(&self) -> Option<TraceBridgeError> {
self.inner.lock().ok().and_then(|mut g| g.take())
}
}
impl Default for CallbackErrorSlot {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for CallbackErrorSlot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CallbackErrorSlot").finish()
}
}
pub(crate) struct PyTraceObserver {
callback: Py<PyAny>,
error_slot: CallbackErrorSlot,
is_async: bool,
}
impl fmt::Debug for PyTraceObserver {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PyTraceObserver")
.field("callback", &"<py callable>")
.finish()
}
}
impl PyTraceObserver {
pub(crate) fn new(py: Python<'_>, callback: Bound<'_, PyAny>) -> (Self, CallbackErrorSlot) {
let is_async = py
.import("inspect")
.ok()
.and_then(|m| m.getattr("iscoroutinefunction").ok())
.and_then(|func| func.call1((callback.clone(),)).ok())
.and_then(|r| r.is_truthy().ok())
.unwrap_or(false);
let error_slot = CallbackErrorSlot::new();
if is_async {
error_slot.record(TraceBridgeError::NotAwaited);
}
let observer = Self {
callback: callback.unbind(),
error_slot: error_slot.clone(),
is_async,
};
(observer, error_slot)
}
}
impl TraceObserver for PyTraceObserver {
fn on_event(&self, event: &TraceEvent) -> OnEventAction {
if self.is_async {
return OnEventAction::Abort;
}
let result = Python::with_gil(|py| {
let (_prefix, name) = event_to_httpcore_name(event);
let info_dict = match build_info_dict(py, event) {
Ok(d) => d,
Err(e) => {
return Err(TraceBridgeError::Callback(e));
}
};
let cb = self.callback.bind(py);
let call_result = cb.call1((name.as_str(), info_dict.unbind()));
match call_result {
Ok(_) => Ok(()),
Err(e) => Err(TraceBridgeError::Callback(e)),
}
});
match result {
Ok(()) => OnEventAction::Continue,
Err(err) => {
self.error_slot.record(err);
OnEventAction::Abort
}
}
}
}
fn build_info_dict<'py>(py: Python<'py>, event: &TraceEvent) -> PyResult<Bound<'py, PyDict>> {
let dict = PyDict::new(py);
match event {
TraceEvent::ConnectTcp { host, port, .. } => {
dict.set_item("host", host.clone())?;
dict.set_item("port", *port)?;
}
TraceEvent::ConnectUnixSocket { path, .. } => {
dict.set_item("path", path.clone())?;
}
TraceEvent::StartTls {
server_hostname, ..
} => {
dict.set_item("server_hostname", server_hostname.clone())?;
}
TraceEvent::Retry { delay_ms, .. } => {
dict.set_item("delay_ms", *delay_ms)?;
}
TraceEvent::Close { .. }
| TraceEvent::SendRequestBody { .. }
| TraceEvent::ReceiveResponseBody { .. }
| TraceEvent::ResponseClosed { .. } => {}
TraceEvent::SendRequestHeaders { method, target, .. } => {
dict.set_item("method", method.clone())?;
dict.set_item("target", target.clone())?;
}
TraceEvent::ReceiveResponseHeaders { status, .. } => {
dict.set_item("status", *status)?;
}
}
Ok(dict)
}
pub(crate) fn bridge_error_to_pyerr(err: TraceBridgeError) -> PyErr {
match err {
TraceBridgeError::Callback(pyerr) => pyerr,
TraceBridgeError::NotAwaited => PyTypeError::new_err(
"async trace callback was not awaited; sync transport cannot drive coroutines. \
Use AsyncClient for async trace callbacks.",
),
}
}
pub(crate) fn take_callback_error(slot: &CallbackErrorSlot) -> Option<PyErr> {
slot.take().map(bridge_error_to_pyerr)
}