Skip to main content

structured_proxy/
received.rs

1//! The HTTP request a transcoded call was made from.
2//!
3//! A transcoded call reaches the upstream as a gRPC call to the RPC its
4//! binding selected; the client's HTTP method and request target are no
5//! longer part of it, and one RPC may be bound to several paths (additional
6//! bindings, aliases), so the RPC path cannot be turned back into them. An
7//! upstream served in process gets them as a [`ReceivedRequest`] in the
8//! request extensions. The proxy records them and checks nothing about them.
9//! A remote upstream never sees request extensions.
10
11use axum::http::uri::PathAndQuery;
12use axum::http::Method;
13
14/// The HTTP request a transcoded gRPC call was made from, as the proxy
15/// received it: its method, its path and query, and the gRPC path of the RPC
16/// the matched binding selected.
17///
18/// The path is the one on the request line, before a router the proxy is
19/// nested in strips its prefix, so an alias or a public prefix stays visible.
20/// An absolute-form target (any HTTP/2 request, an HTTP/1.1 request through a
21/// forward proxy) records its path and query alone, so one request records the
22/// same value over either protocol. No header the client sends
23/// (`x-original-uri`, forwarding headers) changes it.
24///
25/// # Examples
26///
27/// ```
28/// use structured_proxy::ReceivedRequest;
29///
30/// // What a tonic handler behind the proxy does with it.
31/// fn target(request: &tonic::Request<()>) -> Option<String> {
32///     let received = request.extensions().get::<ReceivedRequest>()?;
33///     Some(format!("{} {}", received.method(), received.path_and_query()))
34/// }
35///
36/// let received = ReceivedRequest::new(
37///     http::Method::POST,
38///     http::uri::PathAndQuery::from_static("/v1/orgs/42:suspend?dry=1"),
39///     http::uri::PathAndQuery::from_static("/acme.v1.Orgs/Suspend"),
40/// );
41/// let mut request = tonic::Request::new(());
42/// request.extensions_mut().insert(received);
43/// assert_eq!(target(&request).as_deref(), Some("POST /v1/orgs/42:suspend?dry=1"));
44/// ```
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct ReceivedRequest {
48    method: Method,
49    path_and_query: PathAndQuery,
50    rpc: PathAndQuery,
51}
52
53impl ReceivedRequest {
54    /// A request received as `method` `path_and_query`, transcoded to the RPC
55    /// at the gRPC path `rpc`. The view an upstream's own tests hand it.
56    pub fn new(method: Method, path_and_query: PathAndQuery, rpc: PathAndQuery) -> Self {
57        Self {
58            method,
59            path_and_query,
60            rpc,
61        }
62    }
63
64    /// The HTTP method as received: `HEAD` on a route bound to `GET` stays
65    /// `HEAD`, and a `custom` binding's method is itself.
66    pub fn method(&self) -> &Method {
67        &self.method
68    }
69
70    /// The path and query as received, percent-encoding untouched.
71    pub fn path_and_query(&self) -> &PathAndQuery {
72        &self.path_and_query
73    }
74
75    /// The gRPC path of the RPC the matched binding selected
76    /// (`/package.Service/Method`).
77    pub fn rpc(&self) -> &str {
78        self.rpc.path()
79    }
80}
81
82#[cfg(test)]
83mod tests;