Handle HEAD request

This example shows how to:

  • handle HEAD requests in their own rout
  • handle HEAD requests in a get route

Running

cargo run -p example-handle-head-request

Sending GET request to GET handler

$ curl -i http://localhost:3000/my-get
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
x-some-header: header from GET
content-length: 13
date: Tue, 18 Mar 2025 07:10:38 GMT

body from GET

Sending HEAD request to GET handler

$ curl -I http://localhost:3000/my-get
HTTP/1.1 200 OK
x-some-header: header from HEAD in get-handler
content-length: 0
date: Tue, 18 Mar 2025 07:11:17 GMT

Sending GET request to HEAD handler

This is not handled

$ curl -i http://localhost:3000/my-head

HTTP/1.1 405 Method Not Allowed
allow: HEAD
content-length: 0
date: Tue, 18 Mar 2025 07:12:12 GMT

Sending HEAD request to HEAD handler

$ curl -I http://localhost:3000/my-head

HTTP/1.1 200 OK
x-some-header: header from HEAD in head-handler
content-length: 0
date: Tue, 18 Mar 2025 07:12:50 GMT
[package]
name = "example-handle-head-request"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
axum = { path = "../../axum" }
tokio = { version = "1.0", features = ["full"] }

[dev-dependencies]
http-body-util = "0.1.0"
hyper = { version = "1.0.0", features = ["full"] }
tower = { version = "0.5.2", features = ["util"] }
//! Run with
//!
//! ```not_rust
//! cargo run -p example-handle-head-request
//! ```

use axum::response::{IntoResponse, Response};
use axum::{http, routing::get, routing::head, Router};

fn app() -> Router {
    Router::new()
        .route("/my-get", get(get_handler))
        .route("/my-head", head(head_handler))
}

#[tokio::main]
async fn main() {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app()).await.unwrap();
}

// GET routes will also be called for HEAD requests but will have the response body removed.
// You can handle the HEAD method explicitly by extracting `http::Method` from the request.
async fn get_handler(method: http::Method) -> Response {
    // it usually only makes sense to special-case HEAD
    // if computing the body has some relevant cost
    if method == http::Method::HEAD {
        return ([("x-some-header", "header from HEAD in get-handler")]).into_response();
    }

    // then do some computing task in GET
    do_some_computing_task();

    ([("x-some-header", "header from GET")], "body from GET").into_response()
}

fn do_some_computing_task() {
    // TODO
}

// HET routes will be called only for HEAD requests.
async fn head_handler() -> Response {
    // it usually only makes sense to special-case HEAD
    // if computing the body has some relevant cost
    ([("x-some-header", "header from HEAD in head-handler")]).into_response()
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::body::Body;
    use axum::http::{Request, StatusCode};
    use http_body_util::BodyExt;
    use tower::ServiceExt;

    #[tokio::test]
    async fn test_get_from_get_handler() {
        let app = app();

        let response = app
            .oneshot(Request::get("/my-get").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(response.headers()["x-some-header"], "header from GET");

        let body = response.collect().await.unwrap().to_bytes();
        assert_eq!(&body[..], b"body from GET");
    }

    #[tokio::test]
    async fn test_implicit_head() {
        let app = app();

        let response = app
            .oneshot(Request::head("/my-get").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response.headers()["x-some-header"],
            "header from HEAD in get-handler"
        );

        let body = response.collect().await.unwrap().to_bytes();
        assert!(body.is_empty());
    }

    #[tokio::test]
    async fn test_get_from_head_handler() {
        let app = app();

        let response = app
            .oneshot(Request::get("/my-head").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
        assert!(!response.headers().contains_key("x-some-header"));
        assert_eq!(response.headers()["allow"], "HEAD");

        let body = response.collect().await.unwrap().to_bytes();
        assert!(body.is_empty());
    }

    #[tokio::test]
    async fn test_head_from_head_handler() {
        let app = app();

        let response = app
            .oneshot(Request::head("/my-head").body(Body::empty()).unwrap())
            .await
            .unwrap();

        assert_eq!(response.status(), StatusCode::OK);
        assert_eq!(
            response.headers()["x-some-header"],
            "header from HEAD in head-handler"
        );

        let body = response.collect().await.unwrap().to_bytes();
        assert!(body.is_empty());
    }
}

Copyright © 2025 • Created with ❤️ by the authors of axum an Gabor Szabo