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;