use rainier_http::{Method, Request, Response};
use crate::pipeline::{Middleware, Next};
const FIELD: &str = "_method";
const HEADER: &str = "x-http-method-override";
#[derive(Debug, Clone)]
pub struct MethodOverride {
trust_header: bool,
}
impl Default for MethodOverride {
fn default() -> Self {
Self::new()
}
}
impl MethodOverride {
pub fn new() -> Self {
Self { trust_header: false }
}
#[must_use = "this returns a configured middleware rather than configuring in place"]
pub fn trusting_the_header(mut self) -> Self {
self.trust_header = true;
self
}
}
#[async_trait::async_trait]
impl Middleware for MethodOverride {
async fn handle(&self, mut request: Request, next: Next) -> Response {
if request.method() == Method::POST {
if let Some(method) = self.requested(&request) {
request.set_method(method);
}
}
next.run(request).await
}
fn name(&self) -> &'static str {
"MethodOverride"
}
}
impl MethodOverride {
fn requested(&self, request: &Request) -> Option<Method> {
let asked = request
.input(FIELD)
.or_else(|| {
self.trust_header.then(|| request.header(HEADER).map(str::to_owned)).flatten()
})?
.trim()
.to_ascii_uppercase();
match asked.as_str() {
"PUT" => Some(Method::PUT),
"PATCH" => Some(Method::PATCH),
"DELETE" => Some(Method::DELETE),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::Pipeline;
use serde_json::json;
async fn method_seen(request: Request, middleware: MethodOverride) -> String {
Pipeline::new()
.through(middleware)
.then(|request: Request| async move { Response::ok(request.method().to_string()) })
.run(request)
.await
.into_string()
.await
.unwrap()
}
fn form(method: Method, body: serde_json::Value) -> Request {
Request::builder().method(method).uri("/posts/7").json(&body).build()
}
#[tokio::test]
async fn a_post_with_the_field_becomes_that_method() {
for (asked, expected) in [("PUT", "PUT"), ("PATCH", "PATCH"), ("DELETE", "DELETE")] {
let request = form(Method::POST, json!({ FIELD: asked }));
assert_eq!(method_seen(request, MethodOverride::new()).await, expected);
}
}
#[tokio::test]
async fn the_spelling_does_not_have_to_be_shouted() {
let request = form(Method::POST, json!({ FIELD: "delete" }));
assert_eq!(method_seen(request, MethodOverride::new()).await, "DELETE");
}
#[tokio::test]
async fn a_get_is_never_rewritten() {
let request = Request::builder().method(Method::GET).uri("/posts/7?_method=DELETE").build();
assert_eq!(method_seen(request, MethodOverride::new()).await, "GET");
}
#[tokio::test]
async fn a_post_cannot_become_a_get() {
let request = form(Method::POST, json!({ FIELD: "GET" }));
assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
}
#[tokio::test]
async fn nonsense_is_ignored_rather_than_refused() {
let request = form(Method::POST, json!({ FIELD: "TEAPOT" }));
assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
}
#[tokio::test]
async fn the_header_is_ignored_until_it_is_trusted() {
let request = || {
Request::builder().method(Method::POST).uri("/posts/7").header(HEADER, "DELETE").build()
};
assert_eq!(method_seen(request(), MethodOverride::new()).await, "POST");
assert_eq!(
method_seen(request(), MethodOverride::new().trusting_the_header()).await,
"DELETE"
);
}
#[tokio::test]
async fn a_request_with_nothing_to_say_is_left_alone() {
let request = form(Method::POST, json!({ "title": "Hello" }));
assert_eq!(method_seen(request, MethodOverride::new()).await, "POST");
}
}