1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use tracing::error;
use crate::RequestId;
use crate::builder::SharedHandler;
/// A stable class of connection-level failure observed by an error hook.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectionFailureCategory {
/// The Transport could not recover an accepted message frame.
Framing,
/// A decoded JSON-RPC message violated the protocol contract.
Protocol,
/// A Transport read or write operation failed.
Transport,
/// Framework panic isolation caught a panic from user dispatch.
PanicIsolation,
/// A connection-owned resource budget rejected work.
Overload,
/// Final Transport shutdown failed while the connection was closing.
Close,
}
/// The direction in which a reported connection failure occurred.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionDirection {
/// Traffic received from the client.
Inbound,
/// Traffic sent to the client.
Outbound,
}
/// Non-sensitive identity for a JSON-RPC request.
///
/// Numeric IDs retain their value for correlation. String IDs are represented
/// only by their kind because their peer-controlled contents may be sensitive.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ConnectionRequestId {
/// A numeric JSON-RPC request ID.
Number(i32),
/// A string JSON-RPC request ID, with its contents redacted.
String,
}
impl From<&RequestId> for ConnectionRequestId {
fn from(value: &RequestId) -> Self {
match value {
RequestId::Number(number) => Self::Number(*number),
RequestId::String(_) => Self::String,
}
}
}
/// Non-sensitive identity attached to a connection failure when available.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConnectionFailureContext {
/// Monotonic process-local identity of the affected connection.
pub connection_id: u64,
/// Traffic direction, when the failure belongs to one side of the wire.
pub direction: Option<ConnectionDirection>,
/// Framework-owned, registered, or locally declared typed outbound method,
/// when known without trusting an unvalidated peer-controlled method name.
pub method: Option<String>,
/// JSON-RPC request ID, when the event belongs to a request.
pub request_id: Option<ConnectionRequestId>,
}
/// One connection-level failure delivered to [`ServerBuilder::on_error`](crate::ServerBuilder::on_error).
///
/// The report deliberately excludes parameters, results, document text, wire
/// payloads, panic payloads, and underlying error messages.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ConnectionFailure {
/// Stable category suitable for metrics and policy decisions.
pub category: ConnectionFailureCategory,
/// Non-sensitive connection and call identity.
pub context: ConnectionFailureContext,
}
pub(crate) type ErrorHook = Arc<dyn SharedHandler<(ConnectionFailure,), ()> + Send + Sync>;
#[derive(Clone)]
pub(crate) struct FailureReporter {
hook: Option<ErrorHook>,
connection_id: u64,
}
impl FailureReporter {
pub(crate) fn new(hook: Option<ErrorHook>, connection_id: u64) -> Self {
Self {
hook,
connection_id,
}
}
pub(crate) fn report(
&self,
category: ConnectionFailureCategory,
direction: Option<ConnectionDirection>,
method: Option<&str>,
request_id: Option<&RequestId>,
) {
let Some(hook) = &self.hook else { return };
let failure = ConnectionFailure {
category,
context: ConnectionFailureContext {
connection_id: self.connection_id,
direction,
method: method.map(str::to_owned),
request_id: request_id.map(ConnectionRequestId::from),
},
};
if catch_unwind(AssertUnwindSafe(|| hook.invoke((failure,)))).is_err() {
error!("panic isolated while invoking connection error hook");
}
}
/// Report an inbound event whose method name has not been matched against
/// a framework-owned or registered method. Peer-controlled method names
/// are payload-adjacent data, so they are deliberately omitted.
pub(crate) fn report_unvalidated_inbound_method(
&self,
category: ConnectionFailureCategory,
request_id: Option<&RequestId>,
) {
self.report(
category,
Some(ConnectionDirection::Inbound),
None,
request_id,
);
}
}