pub enum Refusal {
Show 16 variants
MalformedSessionId,
MissingSession,
UnknownSession,
MethodNotAllowed {
method: &'static str,
reason: &'static str,
},
ProtocolHeader {
reason: String,
},
BodyTooLarge,
Unauthenticated,
Forbidden,
ForeignOrigin,
RateLimited {
retry_after: NonZeroU64,
},
Timeout,
MalformedBody {
reason: String,
},
StreamGap {
requested: EventId,
evicted_through: EventId,
last: EventId,
},
UnknownCursor {
requested: EventId,
last: EventId,
},
MalformedCursor,
Handler(String),
}Expand description
A request the adapter refuses before or instead of answering it.
Not an Error in the std::error::Error sense for its own sake — it is
one so it composes with ? in a handler whose other failure is a genuine
error, and so thiserror writes the Display text that goes in the body.
Variants§
MalformedSessionId
The Mcp-Session-Id header was present but not a session id.
MissingSession
A stateful request arrived with no session id and was not initialize.
UnknownSession
The session id named a session this server does not have.
MethodNotAllowed
The endpoint does not offer this method here.
Fields
ProtocolHeader
MCP-Protocol-Version disagreed with the negotiated version, or named
something this build cannot parse.
BodyTooLarge
The body was larger than the configured ceiling.
Unauthenticated
The request carried no credentials and the endpoint requires some.
Forbidden
The request was authenticated but lacks a required scope.
ForeignOrigin
The Origin header named an origin this server does not serve.
The one refusal in this enum that is a specification MUST rather than a JSON-RPC-shaped complaint:
Servers MUST validate the
Originheader on all incoming connections to prevent DNS rebinding attacks
Cited: https://modelcontextprotocol.io/specification/2024-11-05/basic/transports
Carries nothing from the header. The value is attacker-chosen, and a refusal that echoes it into a response body is reflecting untrusted input back to whoever sent it — for no diagnostic gain, since the sender knows exactly what they sent. The operator reads the refusal type; the attacker learns nothing they did not send.
Distinct from Forbidden, which is an
authorization decision about a credential. This is a decision about
where the request came from, made before the request is authenticated,
because the point is to refuse the connection rather than to reject an
identity.
RateLimited
The subject exceeded the configured request rate.
OWASP API Security Top 10:2023, API4:2023 (Unrestricted Resource
Consumption). The one refusal here that answers with a delay rather
than a denial, which is why it carries retry_after: RFC 9110 §10.2.3
makes Retry-After a 429 SHOULD, and a client told only “429” is a
client that retries immediately and keeps the rate up.
Distinct from Forbidden, which is about a
credential, and from ForeignOrigin, which is
about where the request came from. This is about how often one subject
asked.
Fields
retry_after: NonZeroU64Whole seconds the caller should wait. Never zero: a Retry-After: 0
is a client told to retry now, which is the opposite of the point.
Timeout
The handler did not answer inside the configured request deadline.
503 rather than 504. A 504 names a gateway that timed out talking
to an upstream, and this server is the thing that timed out — it is
not forwarding to anything. OWASP API Security Top 10:2023, API4:2023.
MalformedBody
The body was not a JSON-RPC frame this build can decode.
StreamGap
A Last-Event-ID names an event the ring has dropped.
The whole point of the variant is the two watermarks. A client that
resumes from requested and is told evicted_through knows exactly
which of its events are gone and can re-issue them; a client that is
handed a short stream and no explanation believes it is current.
Fields
UnknownCursor
A Last-Event-ID names an event the stream never issued.
Fields
MalformedCursor
The Last-Event-ID header was present but is not a cursor this stream
can read.
Separate from UnknownCursor because the two
faults have different fixes and the client has to tell them apart to
make one: a cursor ahead of the stream is a well-formed id the server
cannot serve, while a cursor it cannot parse is a malformed header. A
server that answered a corrupt header by quietly starting the client
from the top would hand it a stream it believes it has already seen, so
this refuses rather than guessing — including for the empty string,
which is a client that quoted a cursor and quoted nothing.
Handler(String)
The protocol layer failed in a way the adapter does not model.
Implementations§
Source§impl Refusal
impl Refusal
Sourcepub const fn status(&self) -> StatusCode
pub const fn status(&self) -> StatusCode
The status this refusal is answered with.
Sourcepub const fn code(&self) -> i64
pub const fn code(&self) -> i64
The JSON-RPC error code sent alongside the status.
Separate from status because the two answer different
questions and are allowed to disagree: the status is what a proxy, a
browser or curl -I sees, and the code is what a conforming MCP client
branches on. Collapsing them would force one compromise on both.
Where the JSON-RPC specification reserves a code that fits, the
reserved code is used. The rest live in -32000..=-32099, the range
JSON-RPC 2.0 defines as “implementation-defined server errors” — which
is exactly what a transport-level refusal is: not a malformed JSON-RPC
message (nothing was wrong with the message), but a server declining
for a reason of its own. Each arm is a distinct code because a client
recovering from one of these has a different move for each.
Sourcepub fn body(&self) -> Value
pub fn body(&self) -> Value
The JSON body sent alongside the status.
A well-formed JSON-RPC 2.0 error object, because this is a
machine-readable protocol and a client that receives a bare
409 Conflict cannot recover — nothing in it says what was lost. Three
members carry that:
error.codeis the integer fromcode, which a client switches on.error.messageis the human text, which is this type’sDisplay— the same text the log line carries, so the two can never disagree.error.data.erroris the stable slug fromslug, the member a client branches on when it wants a name rather than a number, and where the resume watermarks live.
id is always null. A refusal is frequently the answer to a request
whose body was never parsed, so there may be no id to echo; sending
null rather than omitting the member keeps the document valid
against the JSON-RPC schema, which requires the member on an error.
Sourcepub fn body_with_id(&self, id: Option<&WireId<'_>>) -> Value
pub fn body_with_id(&self, id: Option<&WireId<'_>>) -> Value
The body, echoing the JSON-RPC id of the request that earned the refusal.
SEP-2575 requires it and JSON-RPC has always allowed it: “the id must
be Null if there was an error in detecting the id”. A body this
adapter has already read is not that case — the id was detected, and
the refusal happened afterwards. Answering id: null for a request
that named 4 makes the client’s own pending request unmatchable,
which is the one thing an error reply is supposed to prevent.
None still means null, and deliberately so: a GET or a DELETE has
no JSON-RPC envelope to take an id from, and a body that would not
decode genuinely did not detect one.
Source§impl Refusal
impl Refusal
Sourcepub fn into_response_with_id(self, id: Option<&WireId<'_>>) -> Response
pub fn into_response_with_id(self, id: Option<&WireId<'_>>) -> Response
The response, echoing the JSON-RPC id of the refused request.
Same rule as body_with_id: an id that has been
read off a body is carried into the answer, and an id that was never
detected is null.
Trait Implementations§
impl Eq for Refusal
Source§impl Error for Refusal
impl Error for Refusal
1.30.0 · Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl IntoResponse for Refusal
impl IntoResponse for Refusal
Source§fn into_response(self) -> Response
fn into_response(self) -> Response
Renders the refusal, with the security layer still applied on top.
This impl exists so a handler can ? a Refusal into a Response. It
sets only the status, the content type and — for the two refusals where
a header is meaningful — Allow and Retry-After; the security headers
come from the router layer, which is the only place that can guarantee
they are on every response including this one.
impl StructuralPartialEq for Refusal
Auto Trait Implementations§
impl Freeze for Refusal
impl RefUnwindSafe for Refusal
impl Send for Refusal
impl Sync for Refusal
impl Unpin for Refusal
impl UnsafeUnpin for Refusal
impl UnwindSafe for Refusal
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T, S> Handler<IntoResponseHandler, S> for T
impl<T, S> Handler<IntoResponseHandler, S> for T
Source§fn call(
self,
_req: Request<Body>,
_state: S,
) -> <T as Handler<IntoResponseHandler, S>>::Future
fn call( self, _req: Request<Body>, _state: S, ) -> <T as Handler<IntoResponseHandler, S>>::Future
Source§fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>where
L: Layer<HandlerService<Self, T, S>> + Clone,
<L as Layer<HandlerService<Self, T, S>>>::Service: Service<Request<Body>>,
fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>where
L: Layer<HandlerService<Self, T, S>> + Clone,
<L as Layer<HandlerService<Self, T, S>>>::Service: Service<Request<Body>>,
tower::Layer to the handler. Read moreSource§fn with_state(self, state: S) -> HandlerService<Self, T, S>
fn with_state(self, state: S) -> HandlerService<Self, T, S>
Service by providing the stateSource§impl<H, T> HandlerWithoutStateExt<T> for H
impl<H, T> HandlerWithoutStateExt<T> for H
Source§fn into_service(self) -> HandlerService<H, T, ()>
fn into_service(self) -> HandlerService<H, T, ()>
Service and no state.Source§fn into_make_service(self) -> IntoMakeService<HandlerService<H, T, ()>>
fn into_make_service(self) -> IntoMakeService<HandlerService<H, T, ()>>
MakeService and no state. Read moreSource§fn into_make_service_with_connect_info<C>(
self,
) -> IntoMakeServiceWithConnectInfo<HandlerService<H, T, ()>, C>
fn into_make_service_with_connect_info<C>( self, ) -> IntoMakeServiceWithConnectInfo<HandlerService<H, T, ()>, C>
MakeService which stores information
about the incoming connection and has no state. Read more