Skip to main content

ProxyEvent

Enum ProxyEvent 

Source
#[non_exhaustive]
pub enum ProxyEvent {
Show 19 variants SessionStarted { session_id: SessionId, client_addr: SocketAddr, client_transport: String, }, SetupMessage { session_id: SessionId, side: ProxySide, message: AnyControlMessage, }, ControlMessage { session_id: SessionId, side: ProxySide, message: AnyControlMessage, }, DataStreamHeader { session_id: SessionId, side: ProxySide, header: DataStreamHeaderKind, }, ObjectHeader { session_id: SessionId, side: ProxySide, header: AnyObjectHeader, }, Object { session_id: SessionId, side: ProxySide, meta: ObjectMeta, }, Datagram { session_id: SessionId, side: ProxySide, header: AnyDatagramHeader, payload_len: usize, }, BiStreamOpened { session_id: SessionId, side: ProxySide, }, UniStreamOpened { session_id: SessionId, side: ProxySide, }, ParseError { session_id: SessionId, side: ProxySide, error: String, }, StreamClosed { session_id: SessionId, side: ProxySide, }, StreamReset { session_id: SessionId, side: ProxySide, code: u64, }, SessionEnded { session_id: SessionId, reason: String, }, ActionApplied { session_id: SessionId, side: ProxySide, stream_id: Option<u64>, site: Site, action: ActionKind, effect: Effect, }, ActionRefused { session_id: SessionId, side: ProxySide, stream_id: Option<u64>, site: Site, action: ActionKind, refusal: Refusal, }, ActionFailed { session_id: SessionId, side: ProxySide, site: Site, action: ActionKind, error: String, }, Impairment { session_id: SessionId, side: ProxySide, leg: Option<Leg>, kind: ImpairmentKind, }, Shaped { session_id: SessionId, side: ProxySide, key: StreamKey, stream_id: u64, class: String, outcome: ShapeOutcome, }, ShapedDatagram { session_id: SessionId, side: ProxySide, class: String, outcome: ShapeOutcome, },
}
Expand description

Events emitted by the proxy during stream forwarding.

Marked #[non_exhaustive]: observers must carry a catch-all arm, so that adding an event is not a breaking change.

§Asserting on events — destructure, do not compare

ProxyEvent derives Debug and Clone only. It carries decoded codec types (AnyControlMessage, AnyDatagramHeader, …) that do not implement PartialEq, so there is no assert_eq!(event, ProxyEvent::Something { .. }) to write and there will not be one: deriving PartialEq here would require it on every draft’s message enum.

Every assertion in this workspace therefore has the same two-step shape — count with matches!, then destructure and compare the payload:

use moqtap_proxy::event::{Effect, ProxyEvent};

fn exactly_one_replacement(events: &[ProxyEvent]) {
    // 1. count the variant with `matches!`
    let applied: Vec<&ProxyEvent> = events
        .iter()
        .filter(|e| matches!(e, ProxyEvent::ActionApplied { .. }))
        .collect();
    assert_eq!(applied.len(), 1);

    // 2. destructure, then compare the payload with `assert_eq!`
    let ProxyEvent::ActionApplied { effect, .. } = applied[0] else {
        unreachable!("filtered above")
    };
    assert_eq!(*effect, Effect::Replaced { bytes: 4 });
}

Step 2 works because the payload types added in 0.4.0 — Effect, ImpairmentKind, Refusal, Site and ActionKind — do derive PartialEq and Eq. The boundary is exactly at the event: the event is destructured, what comes out of it is compared.

ProxyEvent and every one of those payload enums is #[non_exhaustive], so a match over any of them from outside this crate needs a catch-all arm; matches! supplies one for free, which is the other reason it is the recommended spelling.

Variants (Non-exhaustive)§

This enum is marked as non-exhaustive
Non-exhaustive enums could have additional variants added in future. Therefore, when matching against variants of non-exhaustive enums, an extra wildcard arm must be added to account for any future variants.
§

SessionStarted

A new client connected and a session was created.

Fields

§session_id: SessionId

The session identifier.

§client_addr: SocketAddr

The client’s remote address.

§client_transport: String

The transport the client chose via ALPN — e.g. "QUIC" or "WebTransport". Observers use this to label per-client sessions; the proxy itself accepts either simultaneously.

§

SetupMessage

A setup message (CLIENT_SETUP or SERVER_SETUP) was observed.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side sent the message.

§message: AnyControlMessage

The decoded setup message.

§

ControlMessage

A control message was parsed from the forwarded byte stream.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side sent the message.

§message: AnyControlMessage

The decoded control message.

§

DataStreamHeader

A data stream header was parsed from a unidirectional stream.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side opened the stream.

§header: DataStreamHeaderKind

The parsed header.

§

ObjectHeader

👎Deprecated since 0.3.0:

superseded by ProxyEvent::Object

An object header was parsed on a data stream.

Never emitted: AnyObjectHeader has no variants past draft-13, so this could only ever report objects on the oldest drafts, and even there it reported a header without consuming the payload behind it. ProxyEvent::Object replaces it and covers drafts 07-19.

Fields

§session_id: SessionId
👎Deprecated since 0.3.0:

superseded by ProxyEvent::Object

The session identifier.

§side: ProxySide
👎Deprecated since 0.3.0:

superseded by ProxyEvent::Object

Which side sent the object.

§header: AnyObjectHeader
👎Deprecated since 0.3.0:

superseded by ProxyEvent::Object

The parsed object header.

§

Object

A complete object was framed on a data stream.

Emitted once per object, in stream order, on every draft 07-19. Objects the framer could not address individually — one larger than its buffer cap, or a stream it stopped parsing — produce no event; their bytes are still forwarded unchanged.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side sent the object.

§meta: ObjectMeta

The object’s identity and framing, without its payload.

§

Datagram

A datagram arrived and its header was parsed.

An observation of what came in, emitted where the decode happens — before any hook is consulted and before the datagram is handed to the far transport. It is deliberately not a delivery receipt, and it once said “was forwarded”, which was wrong in two directions at once: a hook that drops or replaces the datagram still produces this event, and the forward itself can be refused, which is reported separately as ImpairmentKind::DatagramNotSent or, when somebody acted on it, as ProxyEvent::ActionFailed.

It sits with ProxyEvent::Object and ProxyEvent::ControlMessage rather than with the action events: all three say a unit was seen and understood, and none of them says where it ended up. Withholding it until after the send would make an undecodable-or-dropped datagram invisible, which is exactly the case an observer is usually watching for.

Emitted only when an observer is attached — the decode is skipped entirely otherwise — and only when the header actually decoded. A datagram whose header this crate cannot read produces no event and is still forwarded.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side sent the datagram.

§header: AnyDatagramHeader

The parsed datagram header.

§payload_len: usize

Size of the datagram payload in bytes.

§

BiStreamOpened

A bidirectional stream was opened or accepted.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side opened the stream.

§

UniStreamOpened

A unidirectional stream was opened or accepted.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side opened the stream.

§

ParseError

Inline parse failed (non-fatal — bytes are still forwarded).

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side the error occurred on.

§error: String

Description of the parse error.

§

StreamClosed

A stream direction ended cleanly — the peer sent a FIN and every byte it wrote was forwarded.

An abnormal end is reported as ProxyEvent::StreamReset instead, never as this event.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

Which side closed.

§

StreamReset

A peer tore a stream down abnormally — either RESET_STREAM from the sender or STOP_SENDING from the receiver.

This reports the observation. The proxy mirrors the teardown onto the opposite stream with the same application error code; when that stream has already gone away the mirror is a no-op, and the event is still emitted so the teardown is never invisible.

Distinct from ProxyEvent::StreamClosed, which reports an orderly FIN. An observer that sees this knows the stream was abandoned and any data on it may be truncated.

Not emitted for streams the proxy itself tears down at session shutdown — those still end with a FIN, as they did before this event existed.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the teardown was observed on. For a RESET_STREAM this is the ingress side the bytes were arriving on; for a STOP_SENDING it is the egress side they were leaving on.

§code: u64

The peer’s application error code, forwarded verbatim.

§

SessionEnded

The session ended.

Fields

§session_id: SessionId

The session identifier.

§reason: String

Reason for session termination.

§

ActionApplied

An action was executed and the wire changed.

Emitted once the engine has admitted the action and produced the bytes or the plan for it: every guard has run, every refusal has already been taken, and nothing between here and the wire can decline it on this proxy’s behalf. effect is what was decided, down to the byte count.

§It is not a delivery receipt, and one case makes that visible

The engine decides; the forwarding task then hands the result to the transport. That transport can still say no — a replacement datagram above the path MTU is the case that exists — and when it does, this event has already been emitted and is followed by ProxyEvent::ActionFailed naming the same site and action. Both events are emitted for that attempt, in that order, and the pair is the whole truth: the action was admitted, and it did not reach the peer.

Folding the two into one report was rejected twice over. Withholding this event until the write returned would mean an action that was admitted, queued behind a Delay, and lost at teardown reported nothing at all — and it is precisely the admitted-then-lost case that ImpairmentKind::QueuedBytesAtTeardown is paired against. Reporting only the failure would lose which action was taken, since a refusal and a rejection name different things.

So the reading is: this event means the engine did it. An observer that needs the peer got it must also watch for ActionFailed, and for the impairments that report queued bytes.

A proxy-initiated reset appears here, not as ProxyEvent::StreamReset, which continues to mean an observed peer teardown.

§Cardinality under Delay and Hold

A deferred action emits two ActionApplied events, not one, and they are distinguishable by action:

  1. at the decision, { action: Delay | Hold, effect: Queued { release_at } } — the modifier was accepted and the unit is in the queue;
  2. at the release, { action: <the inner kind>, effect: <what the inner action did> } — e.g. { action: Replace, effect: Replaced { bytes } }.

One event would force a choice between reporting the delay and reporting the effect, and a queued unit that is later lost at teardown would have reported a Replaced that never happened. Two events keep the engine accepted this and “the wire changed” separately falsifiable, which is what ImpairmentKind::QueuedBytesAtTeardown is paired against.

So the count to assert is exactly one ActionApplied per (attempt, phase): one for a non-deferred action, two for a deferred one.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the unit arrived on.

§stream_id: Option<u64>

The source stream, when there is one.

§site: Site

Where the decision was taken.

§action: ActionKind

What was executed.

§effect: Effect

What actually happened.

§

ActionRefused

An action could not be executed. Emitted per attempt, and the unit is forwarded unchanged.

The engine declined before anything reached the transport: the wire carries exactly what it would have carried with no hook at all. This is the pre-admission half of the two failure reports — ProxyEvent::ActionFailed is the post-admission one, where the engine accepted the action and the transport rejected it.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the unit arrived on.

§stream_id: Option<u64>

The source stream, when there is one.

§site: Site

Where the decision was taken.

§action: ActionKind

What was attempted.

§refusal: Refusal

Why it was refused.

§

ActionFailed

An action was admitted but the transport rejected it. The session survives.

Emitted per failed attempt. The unit is not forwarded — the transport already declined it — and forwarding continues on everything else.

Reaching this means the capability table admitted the action and the path disagreed; a replacement datagram above the path MTU is the case that exists in 0.4.0. Neither ProxyEvent::ActionApplied nor ProxyEvent::ActionRefused can carry it on its own: the action was admitted, so refusing it after the fact would be a lie, and it did not reach the peer, so reporting only that it applied would be a bigger one.

It is paired with ActionApplied, not exclusive of it. The engine admits, emits ActionApplied, and hands the bytes on; the transport then declines them and this follows. An attempt that reaches here therefore contributes two events, in that order. It is exclusive of ProxyEvent::ActionRefused, which is the other half of the same split: a refused unit is forwarded unchanged and a transport failure on those bytes is ImpairmentKind::DatagramNotSent, because nobody’s action was in flight.

Distinct from ImpairmentKind::DatagramNotSent, which is the same transport failure on a unit nobody acted on — there is no site and no action to name there, and this variant requires both.

Connection-level errors (ConnectionLost, Connection(_)) are not reported here: they still end the session.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the unit arrived on.

§site: Site

Where the decision was taken.

§action: ActionKind

What was executed.

§error: String

The transport’s error.

§

Impairment

Something reduced what the proxy can do, with no action involved.

Every ImpairmentKind states its own emission cardinality; read it there before asserting a count.

§This is emitted after the thing it reports, never before

Whatever the report describes has already happened by the time the observer is called: the reset has been handed to the transport, the datagram has been refused, the queue has been abandoned. Nothing in this crate emits one of these on the way into an operation that could still be declined.

That ordering is what makes the event stream a record rather than an intention. Reversed, an impairment raised before a step that a guard then refuses is an observer told about a loss that did not occur — and there is no later event that retracts it, because this variant has no counterpart to ProxyEvent::ActionFailed. An observer may therefore treat every one of these as a fact about the past.

The one place the rule is visibly not the same is ProxyEvent::ActionApplied, which is emitted when the engine admits an action and before the caller hands the bytes to the transport; that pairing is covered in its own rustdoc and is why ActionFailed exists.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the reporting task was forwarding from — the direction bytes were arriving on, not necessarily the direction the impairment was felt in. Read leg for that.

§leg: Option<Leg>

Which of the proxy’s two connections the report is about, or None when it is about neither.

§Not derivable from side, which is why it is here

side names the direction the reporting task reads from, so it answers where did these bytes come from. Most of what ImpairmentKind reports is a failure to write: a queue that could not be flushed, a datagram the far transport refused, a destination stream that had to be reset. Those belong to the opposite connection from the one the bytes arrived on, and a reader who mapped side to a connection would attribute every one of them to the wrong leg — silently, and in a way that looks entirely plausible in a log.

So the two are split, exactly as ProxyStats splits what a leg read from what it wrote:

A caller that wants which connection is unhealthy reads this field and skips the Nones. A caller that wants which direction was being forwarded when this was noticed reads side.

§kind: ImpairmentKind

What happened.

§

Shaped

The egress shaper acted on a unit as configured.

This is the product working, not an impairment: a configured drop is the tool doing what it was told, where an ImpairmentKind is the tool declining to. It is also the only event that can carry a class label, because a class is a shaping concept and nothing else in this enum has one.

Cardinality: once per stream per distinct outcome. Running totals live in ShapeStats — a per-object event would drown an observer at line rate, which is the same reason ImpairmentKind::ObjectNotAddressable carries a total instead of firing per object.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the unit arrived on.

§key: StreamKey

Session-local identity. Unique even on the WebTransport arm, where every transport stream id is the constant 0.

§stream_id: u64

Transport stream id, for correlation with the other events in this enum. 0 for every WebTransport stream; key is what identifies.

§class: String

The class the unit resolved to, or an empty string for a unit that matched no rule and for an outcome that is about the stream rather than about a unit.

§outcome: ShapeOutcome

What the shaper did.

§

ShapedDatagram

The egress shaper discarded a datagram as configured.

Self::Shaped’s datagram sibling, and separate from it because a datagram belongs to no stream: Shaped carries a StreamKey and a transport stream id, and there is nothing honest to put in either. Merging the two by making those fields optional would put an Option on the far commoner event to describe the rarer one.

Cardinality: once per forwarding direction per distinct outcome, which is the same rule Self::Shaped states per stream — the direction is a datagram’s whole scope. Running totals live in ShapeStats.

Fields

§session_id: SessionId

The session identifier.

§side: ProxySide

The side the datagram arrived on.

§class: String

The class the datagram resolved to, or an empty string for one that matched no rule.

§outcome: ShapeOutcome

What the shaper did.

Trait Implementations§

Source§

impl Clone for ProxyEvent

Source§

fn clone(&self) -> ProxyEvent

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for ProxyEvent

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more