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. Server-side
15//! frameworks have long worked around that with a hidden `_method` field the
16//! server converts on arrival; 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 — the spelling PHP frameworks
40/// made conventional.
41const FIELD: &str = "_method";
42
43/// The header form of the same thing, which some clients and proxies send.
44const HEADER: &str = "x-http-method-override";
45
46/// Rewrites a `POST` carrying `_method` into that method.
47#[derive(Debug, Clone)]
48pub struct MethodOverride {
49    trust_header: bool,
50}
51
52impl Default for MethodOverride {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl MethodOverride {
59    /// Read the override from the form field only.
60    pub fn new() -> Self {
61        Self { trust_header: false }
62    }
63
64    /// Also honour `X-HTTP-Method-Override`.
65    ///
66    /// For a client behind something that will not forward a `PATCH` — some
67    /// corporate proxies still will not. Separate from the default because a
68    /// header is easier to attach by accident than a form field: an
69    /// intermediary that adds it to every request would rewrite every `POST`
70    /// in the application.
71    #[must_use = "this returns a configured middleware rather than configuring in place"]
72    pub fn trusting_the_header(mut self) -> Self {
73        self.trust_header = true;
74        self
75    }
76}
77
78#[async_trait::async_trait]
79impl Middleware for MethodOverride {
80    async fn handle(&self, mut request: Request, next: Next) -> Response {
81        // Only a POST. A GET carrying `_method=DELETE` is a link somebody
82        // crafted, and following it would make every crawler a hazard.
83        if request.method() == Method::POST {
84            if let Some(method) = self.requested(&request) {
85                request.set_method(method);
86            }
87        }
88
89        next.run(request).await
90    }
91
92    fn name(&self) -> &'static str {
93        "MethodOverride"
94    }
95}
96
97impl MethodOverride {
98    /// The method this request is asking to be, if it is asking for one we
99    /// will grant.
100    fn requested(&self, request: &Request) -> Option<Method> {
101        let asked = request
102            .input(FIELD)
103            .or_else(|| {
104                self.trust_header.then(|| request.header(HEADER).map(str::to_owned)).flatten()
105            })?
106            .trim()
107            .to_ascii_uppercase();
108
109        match asked.as_str() {
110            // The three a form actually needs. Not GET or HEAD: turning a POST
111            // into a GET would move the body into nowhere and change which
112            // route runs in a way no form ever wants. Not POST: that is what
113            // it already is.
114            "PUT" => Some(Method::PUT),
115            "PATCH" => Some(Method::PATCH),
116            "DELETE" => Some(Method::DELETE),
117            _ => None,
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::pipeline::Pipeline;
126    use serde_json::json;
127
128    /// Echoes back the method the handler was reached with.
129    async fn method_seen(request: Request, middleware: MethodOverride) -> String {
130        Pipeline::new()
131            .through(middleware)
132            .then(|request: Request| async move { Response::ok(request.method().to_string()) })
133            .run(request)
134            .await
135            .into_string()
136            .await
137            .unwrap()
138    }
139
140    fn form(method: Method, body: serde_json::Value) -> Request {
141        Request::builder().method(method).uri("/posts/7").json(&body).build()
142    }
143
144    #[tokio::test]
145    async fn a_post_with_the_field_becomes_that_method() {
146        for (asked, expected) in [("PUT", "PUT"), ("PATCH", "PATCH"), ("DELETE", "DELETE")] {
147            let request = form(Method::POST, json!({ FIELD: asked }));
148            assert_eq!(method_seen(request, MethodOverride::new()).await, expected);
149        }
150    }
151
152    #[tokio::test]
153    async fn the_spelling_does_not_have_to_be_shouted() {
154        let request = form(Method::POST, json!({ FIELD: "delete" }));
155        assert_eq!(method_seen(request, MethodOverride::new()).await, "DELETE");
156    }
157
158    #[tokio::test]
159    async fn a_get_is_never_rewritten() {
160        // A link with `?_method=DELETE` would otherwise make every crawler a
161        // hazard.
162        let request = Request::builder().method(Method::GET).uri("/posts/7?_method=DELETE").build();
163
164        assert_eq!(method_seen(request, MethodOverride::new()).await, "GET");
165    }
166
167    #[tokio::test]
168    async fn a_post_cannot_become_a_get() {
169        let request = form(Method::POST, json!({ FIELD: "GET" }));
170        assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
171    }
172
173    #[tokio::test]
174    async fn nonsense_is_ignored_rather_than_refused() {
175        // A 400 here would be a stricter contract than the form ever agreed
176        // to, and the route for a plain POST is still the right answer.
177        let request = form(Method::POST, json!({ FIELD: "TEAPOT" }));
178        assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
179    }
180
181    #[tokio::test]
182    async fn the_header_is_ignored_until_it_is_trusted() {
183        let request = || {
184            Request::builder().method(Method::POST).uri("/posts/7").header(HEADER, "DELETE").build()
185        };
186
187        assert_eq!(method_seen(request(), MethodOverride::new()).await, "POST");
188        assert_eq!(
189            method_seen(request(), MethodOverride::new().trusting_the_header()).await,
190            "DELETE"
191        );
192    }
193
194    #[tokio::test]
195    async fn a_request_with_nothing_to_say_is_left_alone() {
196        let request = form(Method::POST, json!({ "title": "Hello" }));
197        assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
198    }
199}