use crate::{BoundaryError, ExecutionCertainty};
use saddle_core::{
CaptureSite, Diagnostic, DiagnosticCategory, DiagnosticCause, DiagnosticCode, DiagnosticObject,
DiagnosticObjectKind, DiagnosticStage,
};
use std::{error::Error, io};
fn cause(stage: DiagnosticStage, code: &'static str, alias: &'static str) -> DiagnosticCause {
DiagnosticCause::new(
stage,
DiagnosticCode::new(code).expect("fixed diagnostic code"),
)
.with_object(
DiagnosticObject::new(DiagnosticObjectKind::TargetAlias, alias).expect("fixed alias"),
)
}
pub(crate) fn invalid_response() -> Diagnostic {
Diagnostic::capture(
DiagnosticCategory::UnexpectedError,
CaptureSite::FirstObserved,
cause(
DiagnosticStage::RequestOutbound,
"transport.response_invalid",
"profusecontract",
),
)
}
fn io_source<'a>(error: &'a (dyn Error + 'static)) -> Option<&'a io::Error> {
let mut current = Some(error);
for _ in 0..16 {
let error = current?;
if let Some(io) = error.downcast_ref::<io::Error>() {
return Some(io);
}
current = error.source();
}
None
}
pub(crate) fn connect_error(error: &(dyn Error + 'static)) -> BoundaryError {
let mut cause = cause(
DiagnosticStage::RequestOutbound,
"transport.connect_failed",
"profusecontract",
);
let mut category = DiagnosticCategory::UnexpectedError;
if let Some(io) = io_source(error) {
cause = cause.with_io(io);
if matches!(
io.kind(),
io::ErrorKind::TimedOut | io::ErrorKind::Interrupted
) {
category = DiagnosticCategory::ExpectedRejection;
}
}
BoundaryError::transport(
"profusecontract connection failed",
ExecutionCertainty::NotExecuted,
)
.with_diagnostic(Diagnostic::capture(
category,
CaptureSite::FirstObserved,
cause,
))
}
pub(crate) fn rpc_error(status: &tonic::Status) -> BoundaryError {
use tonic::Code;
let code = match status.code() {
Code::Ok => "transport.rpc.ok",
Code::Cancelled => "transport.rpc.cancelled",
Code::Unknown => "transport.rpc.unknown",
Code::InvalidArgument => "transport.rpc.invalid_argument",
Code::DeadlineExceeded => "transport.rpc.deadline_exceeded",
Code::NotFound => "transport.rpc.not_found",
Code::AlreadyExists => "transport.rpc.already_exists",
Code::PermissionDenied => "transport.rpc.permission_denied",
Code::ResourceExhausted => "transport.rpc.resource_exhausted",
Code::FailedPrecondition => "transport.rpc.failed_precondition",
Code::Aborted => "transport.rpc.aborted",
Code::OutOfRange => "transport.rpc.out_of_range",
Code::Unimplemented => "transport.rpc.unimplemented",
Code::Internal => "transport.rpc.internal",
Code::Unavailable => "transport.rpc.unavailable",
Code::DataLoss => "transport.rpc.data_loss",
Code::Unauthenticated => "transport.rpc.unauthenticated",
};
let category = if matches!(
status.code(),
Code::Cancelled
| Code::DeadlineExceeded
| Code::ResourceExhausted
| Code::InvalidArgument
| Code::Unauthenticated
| Code::PermissionDenied
) {
DiagnosticCategory::ExpectedRejection
} else {
DiagnosticCategory::UnexpectedError
};
let mut cause = cause(DiagnosticStage::RequestOutbound, code, "profusecontract");
if let Some(io) = io_source(status) {
cause = cause.with_io(io);
}
BoundaryError::transport(
"profusecontract RPC failed",
ExecutionCertainty::MayHaveExecuted,
)
.with_diagnostic(Diagnostic::capture(
category,
CaptureSite::FirstObserved,
cause,
))
}
#[derive(Clone, Copy)]
pub enum HttpIoStage {
Listener,
IngressRead,
ResponseWrite,
}
pub fn http_io(stage: HttpIoStage, error: &io::Error) -> Diagnostic {
let (stage, code) = match stage {
HttpIoStage::Listener => (DiagnosticStage::StartupListener, "transport.listener_io"),
HttpIoStage::IngressRead => (DiagnosticStage::RequestDecode, "transport.ingress_io"),
HttpIoStage::ResponseWrite => (DiagnosticStage::RequestResponse, "transport.response_io"),
};
let category = if matches!(
error.kind(),
io::ErrorKind::ConnectionReset
| io::ErrorKind::ConnectionAborted
| io::ErrorKind::BrokenPipe
| io::ErrorKind::UnexpectedEof
| io::ErrorKind::TimedOut
| io::ErrorKind::Interrupted
| io::ErrorKind::WouldBlock
| io::ErrorKind::InvalidData
) {
DiagnosticCategory::ExpectedRejection
} else {
DiagnosticCategory::UnexpectedError
};
Diagnostic::capture(
category,
CaptureSite::FirstObserved,
cause(stage, code, "profusegw").with_io(error),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_diagnostics_real_refused_connect() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
drop(listener);
let result = crate::TonicBoundary::connect(
crate::ProfuseContractEndpoint::new(format!("http://{addr}")).unwrap(),
)
.await;
let error = match result {
Err(error) => error,
Ok(_) => panic!("connection unexpectedly succeeded"),
};
let json = serde_json::to_string(error.diagnostic().unwrap()).unwrap();
assert!(json.contains("connection_refused"), "{json}");
assert!(json.contains("os_code"));
assert!(json.contains("profusecontract"));
assert!(!json.contains(&addr.to_string()));
});
}
#[test]
fn source_diagnostics_status_and_peer_are_safe() {
let status = tonic::Status::internal("Bearer DO_NOT_LOG https://secret.invalid user-body");
let error = rpc_error(&status);
let json = serde_json::to_string(error.diagnostic().unwrap()).unwrap();
assert!(json.contains("transport.rpc.internal"));
for secret in ["DO_NOT_LOG", "secret.invalid", "user-body"] {
assert!(!format!("{error:?}{error}{json}").contains(secret));
}
let cancelled = rpc_error(&tonic::Status::cancelled("DO_NOT_LOG"));
assert!(
serde_json::to_string(cancelled.diagnostic().unwrap())
.unwrap()
.contains("expected_rejection")
);
let (reader, writer) = std::os::unix::net::UnixStream::pair().unwrap();
drop(reader);
use std::io::Write;
let io = (&writer).write_all(b"response").unwrap_err();
let diagnostic = http_io(HttpIoStage::ResponseWrite, &io);
let json = serde_json::to_string(&diagnostic).unwrap();
assert!(json.contains("broken_pipe"));
assert!(json.contains("expected_rejection"));
}
}