Skip to main content

rainier_middleware/
method_override.rs

1//! Letting an HTML form spell `DELETE` — [`MethodOverride`].
2//!
3//! ```ignore
4//! // Globally, and only if you serve HTML forms.
5//! registry.global(MethodOverride::new());
6//! ```
7//!
8//! ```html
9//! <form method="post" action="/posts/7">
10//!   <input type="hidden" name="_method" value="DELETE">
11//! </form>
12//! ```
13//!
14//! A browser form can send `GET` and `POST` and nothing else. Laravel's
15//! `@method('DELETE')` writes the hidden field above and its `TrustHosts`
16//! stack converts it; this is the same trick, and it is what lets a
17//! server-rendered application have a REST-shaped route table without a line
18//! of JavaScript.
19//!
20//! # Off by default, on purpose
21//!
22//! A JSON API has no use for it — its clients can send any method — and a
23//! rewrite nobody needs is a rewrite that can surprise somebody:
24//!
25//! - Anything upstream that made a decision **by method** made it on the
26//!   original. A WAF rule, an audit log, a proxy that only forwards `POST`
27//!   to this path, a rate limit keyed on the method — all of them saw a
28//!   `POST` and this makes it a `DELETE` afterwards.
29//! - It is a body field, so a caller who can shape the body chooses the
30//!   method. That is fine when the route is the same route either way, and it
31//!   is exactly the point when it is not.
32//!
33//! So it is a deliberate switch, and it only ever upgrades a `POST`.
34
35use rainier_http::{Method, Request, Response};
36
37use crate::pipeline::{Middleware, Next};
38
39/// The field a form puts the real method in. Laravel's spelling.
40const FIELD: &str = "_method";
41
42/// The header form of the same thing, which some clients and proxies send.
43const HEADER: &str = "x-http-method-override";
44
45/// Rewrites a `POST` carrying `_method` into that method.
46#[derive(Debug, Clone)]
47pub struct MethodOverride {
48    trust_header: bool,
49}
50
51impl Default for MethodOverride {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl MethodOverride {
58    /// Read the override from the form field only.
59    pub fn new() -> Self {
60        Self { trust_header: false }
61    }
62
63    /// Also honour `X-HTTP-Method-Override`.
64    ///
65    /// For a client behind something that will not forward a `PATCH` — some
66    /// corporate proxies still will not. Separate from the default because a
67    /// header is easier to attach by accident than a form field: an
68    /// intermediary that adds it to every request would rewrite every `POST`
69    /// in the application.
70    #[must_use = "this returns a configured middleware rather than configuring in place"]
71    pub fn trusting_the_header(mut self) -> Self {
72        self.trust_header = true;
73        self
74    }
75}
76
77#[async_trait::async_trait]
78impl Middleware for MethodOverride {
79    async fn handle(&self, mut request: Request, next: Next) -> Response {
80        // Only a POST. A GET carrying `_method=DELETE` is a link somebody
81        // crafted, and following it would make every crawler a hazard.
82        if request.method() == Method::POST {
83            if let Some(method) = self.requested(&request) {
84                request.set_method(method);
85            }
86        }
87
88        next.run(request).await
89    }
90
91    fn name(&self) -> &'static str {
92        "MethodOverride"
93    }
94}
95
96impl MethodOverride {
97    /// The method this request is asking to be, if it is asking for one we
98    /// will grant.
99    fn requested(&self, request: &Request) -> Option<Method> {
100        let asked = request
101            .input(FIELD)
102            .or_else(|| {
103                self.trust_header.then(|| request.header(HEADER).map(str::to_owned)).flatten()
104            })?
105            .trim()
106            .to_ascii_uppercase();
107
108        match asked.as_str() {
109            // The three a form actually needs. Not GET or HEAD: turning a POST
110            // into a GET would move the body into nowhere and change which
111            // route runs in a way no form ever wants. Not POST: that is what
112            // it already is.
113            "PUT" => Some(Method::PUT),
114            "PATCH" => Some(Method::PATCH),
115            "DELETE" => Some(Method::DELETE),
116            _ => None,
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::pipeline::Pipeline;
125    use serde_json::json;
126
127    /// Echoes back the method the handler was reached with.
128    async fn method_seen(request: Request, middleware: MethodOverride) -> String {
129        Pipeline::new()
130            .through(middleware)
131            .then(|request: Request| async move { Response::ok(request.method().to_string()) })
132            .run(request)
133            .await
134            .into_string()
135            .await
136            .unwrap()
137    }
138
139    fn form(method: Method, body: serde_json::Value) -> Request {
140        Request::builder().method(method).uri("/posts/7").json(&body).build()
141    }
142
143    #[tokio::test]
144    async fn a_post_with_the_field_becomes_that_method() {
145        for (asked, expected) in [("PUT", "PUT"), ("PATCH", "PATCH"), ("DELETE", "DELETE")] {
146            let request = form(Method::POST, json!({ FIELD: asked }));
147            assert_eq!(method_seen(request, MethodOverride::new()).await, expected);
148        }
149    }
150
151    #[tokio::test]
152    async fn the_spelling_does_not_have_to_be_shouted() {
153        let request = form(Method::POST, json!({ FIELD: "delete" }));
154        assert_eq!(method_seen(request, MethodOverride::new()).await, "DELETE");
155    }
156
157    #[tokio::test]
158    async fn a_get_is_never_rewritten() {
159        // A link with `?_method=DELETE` would otherwise make every crawler a
160        // hazard.
161        let request = Request::builder().method(Method::GET).uri("/posts/7?_method=DELETE").build();
162
163        assert_eq!(method_seen(request, MethodOverride::new()).await, "GET");
164    }
165
166    #[tokio::test]
167    async fn a_post_cannot_become_a_get() {
168        let request = form(Method::POST, json!({ FIELD: "GET" }));
169        assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
170    }
171
172    #[tokio::test]
173    async fn nonsense_is_ignored_rather_than_refused() {
174        // A 400 here would be a stricter contract than the form ever agreed
175        // to, and the route for a plain POST is still the right answer.
176        let request = form(Method::POST, json!({ FIELD: "TEAPOT" }));
177        assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
178    }
179
180    #[tokio::test]
181    async fn the_header_is_ignored_until_it_is_trusted() {
182        let request = || {
183            Request::builder().method(Method::POST).uri("/posts/7").header(HEADER, "DELETE").build()
184        };
185
186        assert_eq!(method_seen(request(), MethodOverride::new()).await, "POST");
187        assert_eq!(
188            method_seen(request(), MethodOverride::new().trusting_the_header()).await,
189            "DELETE"
190        );
191    }
192
193    #[tokio::test]
194    async fn a_request_with_nothing_to_say_is_left_alone() {
195        let request = form(Method::POST, json!({ "title": "Hello" }));
196        assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
197    }
198}