///|
/// Why an outbound event stream is malformed. ASGI pins the order an application
/// may emit messages in — a body before the response starts, a second start, a
/// websocket frame before the handshake is accepted, and so on are all protocol
/// violations. `validate_events` walks a stream and names the first violation it
/// finds; a well-formed stream returns `None`. A server can run this over its own
/// emissions to catch a buggy app early, and the conformance harness drives a
/// negative table through it.
pub(all) enum EventOrderError {
// HTTP response ordering.
BodyBeforeStart
DuplicateResponseStart
EarlyHintAfterStart
BodyAfterComplete
UnexpectedTrailers
TrailersBeforeBody
MissingResponseStart
IncompleteBody
MissingTrailers
EventAfterComplete
// WebSocket ordering.
FrameBeforeAccept
DuplicateAccept
EventAfterClose
DenialBodyBeforeStart
DenialAfterAccept
AcceptAfterDenial
IncompleteDenial
MissingHandshakeReply
// Lifespan ordering.
DuplicateLifespanReply
ShutdownBeforeStartup
// An event from the wrong message set for this scope (e.g. a websocket frame
// in an http response stream).
NonResponseEvent
} derive(Eq)
///|
/// A one-line human description of the violation, used in conformance failure
/// names and server logs.
pub fn EventOrderError::describe(self : EventOrderError) -> String {
match self {
BodyBeforeStart => "response body before http.response.start"
DuplicateResponseStart => "a second http.response.start"
EarlyHintAfterStart => "http.response.early_hint after the response started"
BodyAfterComplete => "a body message after the response was complete"
UnexpectedTrailers =>
"http.response.trailers when start did not promise them"
TrailersBeforeBody => "http.response.trailers before the body was complete"
MissingResponseStart => "stream ended with no http.response.start"
IncompleteBody => "stream ended before a final body (more_body was true)"
MissingTrailers => "start promised trailers but none were sent"
EventAfterComplete => "an event after the response was fully sent"
FrameBeforeAccept => "a websocket frame before websocket.accept"
DuplicateAccept => "a second websocket.accept"
EventAfterClose => "an event after the websocket was closed"
DenialBodyBeforeStart => "websocket.http.response.body before its start"
DenialAfterAccept =>
"websocket.http.response after the handshake was accepted"
AcceptAfterDenial => "websocket.accept after a denial response started"
IncompleteDenial => "a denial response with no final body"
MissingHandshakeReply =>
"the connect was never answered (accept/close/deny)"
DuplicateLifespanReply => "a second reply to the same lifespan phase"
ShutdownBeforeStartup => "a shutdown reply before the startup reply"
NonResponseEvent => "an event from the wrong message set for this scope"
}
}
///|
/// Validate the outbound event stream an application emits for an http request,
/// against ASGI's http response ordering: at most one `HttpResponseStart`, no
/// body or pathsend/zerocopysend before it, early hints only ahead of it, a body
/// stream that terminates once (`more_body: false`), trailers only when the start
/// promised them and only after the body completes, and nothing after the
/// response is fully sent. `HttpResponseDebug` carries no protocol meaning and is
/// accepted anywhere; `HttpResponsePush` is accepted before or during the
/// response. Returns the first violation, or `None` for a well-formed complete
/// response.
pub fn validate_http_response(events : Array[Event]) -> EventOrderError? {
let mut started = false
let mut complete = false
let mut trailers_promised = false
let mut trailers_done = false
for ev in events {
let finished = complete && (!trailers_promised || trailers_done)
match ev {
HttpResponseDebug(..) => ()
HttpResponseEarlyHint(..) =>
if started {
return Some(EarlyHintAfterStart)
}
HttpResponseStart(trailers~, ..) => {
if started {
return Some(DuplicateResponseStart)
}
started = true
trailers_promised = trailers
}
HttpResponseBody(more_body~, ..) => {
if !started {
return Some(BodyBeforeStart)
}
if complete {
return Some(BodyAfterComplete)
}
if !more_body {
complete = true
}
}
HttpResponseZeroCopySend(more_body~, ..) => {
if !started {
return Some(BodyBeforeStart)
}
if complete {
return Some(BodyAfterComplete)
}
if !more_body {
complete = true
}
}
HttpResponsePathSend(..) => {
if !started {
return Some(BodyBeforeStart)
}
if complete {
return Some(BodyAfterComplete)
}
complete = true
}
HttpResponsePush(..) => if finished { return Some(EventAfterComplete) }
HttpResponseTrailers(more_trailers~, ..) => {
if !trailers_promised {
return Some(UnexpectedTrailers)
}
if !complete {
return Some(TrailersBeforeBody)
}
if trailers_done {
return Some(EventAfterComplete)
}
if !more_trailers {
trailers_done = true
}
}
_ => return Some(NonResponseEvent)
}
}
if !started {
return Some(MissingResponseStart)
}
if !complete {
return Some(IncompleteBody)
}
if trailers_promised && !trailers_done {
return Some(MissingTrailers)
}
None
}
///|
/// Validate the outbound event stream an application emits for a websocket
/// connection, against ASGI's handshake ordering: the connect must be answered
/// with an `WebSocketAccept`, a `WebSocketClose`, or a denial
/// (`WebSocketHttpResponseStart` + body); frames are only legal once accepted;
/// nothing follows a close or a completed denial; a denial cannot mix with an
/// accept. Returns the first violation, or `None` for a well-formed stream.
pub fn validate_ws_response(events : Array[Event]) -> EventOrderError? {
let mut accepted = false
let mut closed = false
let mut denial_started = false
let mut denial_complete = false
for ev in events {
if closed || denial_complete {
return Some(EventAfterClose)
}
match ev {
WebSocketAccept(..) => {
if accepted {
return Some(DuplicateAccept)
}
if denial_started {
return Some(AcceptAfterDenial)
}
accepted = true
}
WebSocketSendText(_) | WebSocketSendBytes(_) =>
if !accepted {
return Some(FrameBeforeAccept)
}
WebSocketClose(..) => closed = true
WebSocketHttpResponseStart(..) => {
if accepted {
return Some(DenialAfterAccept)
}
if denial_started {
return Some(DuplicateResponseStart)
}
denial_started = true
}
WebSocketHttpResponseBody(more_body~, ..) => {
if !denial_started {
return Some(DenialBodyBeforeStart)
}
if !more_body {
denial_complete = true
}
}
_ => return Some(NonResponseEvent)
}
}
if denial_started && !denial_complete {
return Some(IncompleteDenial)
}
if not_answered(accepted, closed, denial_started) {
return Some(MissingHandshakeReply)
}
None
}
///|
/// Whether a websocket connect went unanswered — no accept, no close, no denial.
fn not_answered(accepted : Bool, closed : Bool, denial_started : Bool) -> Bool {
!accepted && !closed && !denial_started
}
///|
/// Validate the outbound replies an application emits for a lifespan run: a
/// startup reply (`LifespanStartupComplete` / `LifespanStartupFailed`) before a
/// shutdown reply (`LifespanShutdownComplete` / `LifespanShutdownFailed`), each
/// phase answered at most once. Returns the first violation, or `None`.
pub fn validate_lifespan_replies(events : Array[Event]) -> EventOrderError? {
let mut startup_replied = false
let mut shutdown_replied = false
for ev in events {
match ev {
LifespanStartupComplete | LifespanStartupFailed(..) => {
if startup_replied {
return Some(DuplicateLifespanReply)
}
startup_replied = true
}
LifespanShutdownComplete | LifespanShutdownFailed(..) => {
if !startup_replied {
return Some(ShutdownBeforeStartup)
}
if shutdown_replied {
return Some(DuplicateLifespanReply)
}
shutdown_replied = true
}
_ => return Some(NonResponseEvent)
}
}
None
}
///|
/// Validate an outbound event stream against the ordering rules for its scope,
/// dispatching to the http / websocket / lifespan validator. The public entry a
/// server calls to check an application's emissions before writing them to the
/// wire.
pub fn validate_events(
scope : Scope,
events : Array[Event],
) -> EventOrderError? {
match scope {
Http(_) => validate_http_response(events)
WebSocket(_) => validate_ws_response(events)
Lifespan(_) => validate_lifespan_replies(events)
}
}