Search code examples
rustrust-tokiorust-axum

Implemented a Controller merging routing and handling; Getting 404 -- suspect a Rust tenant violation


I am working with Rust and Axum on a simple REST API project. What I am trying to accomplish is to merge a route, http verb, and handler together to mimic what controllers in other languages/frameworks do. Everything is looking really good except that when I launch the server and hit the /projects:9090 endpoint, I receive a 404 error rather than a list or empty list. I suspect that my issue is how I am mutating the router instance.

use crate::state::AppState;
use axum::response::IntoResponse;
use axum::Router;
use serde::{Deserialize, Serialize};
mod create;
mod delete;
mod find_one;
mod health_check;
mod list;
mod update;

const HEALTH_CHECK_PATH: &str = "/health_check";
const PROJECTS_PATH: &str = "/projects";
const PROJECTS_NAME_PATH: &str = "/projects/:name";

#[derive(Clone, Debug)]
pub struct Controller {
    pub(crate) router: Router<AppState>,
}

impl Controller {
    pub fn new() -> Controller {
        let controller = Controller {
            router: Router::new(),
        };

        controller
            .create()
            .delete()
            .find_one()
            .health_check()
            .list()
            .update();

        controller
    }
}

and here is the list() method...

use crate::state::AppState;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::Json;
use axum_macros::debug_handler;
use serde::Deserialize;
use crate::controller::{Controller, PROJECTS_PATH};

impl Controller {
    pub fn list(&self) -> &Controller {
        // #[derive(Debug, Deserialize, Default)]
        // pub struct Pagination {
        //     pub offset: Option<usize>,
        //     pub limit: Option<usize>,
        // }

        #[debug_handler]
        pub async fn handler(
            //pagination: Option<Query<Pagination>>,
            State(state): State<AppState>,
        ) -> impl IntoResponse {
            //let Query(_pagination) = pagination.unwrap_or_default();
            match state.query_port.read().unwrap().list() {
                Ok(res) => (StatusCode::OK, Json(res)).into_response(),
                Err(err) => {
                    (StatusCode::INTERNAL_SERVER_ERROR, Json(err.to_string())).into_response()
                }
            }
        }

        let _ = self.router.clone().route(PROJECTS_PATH, get(handler));
        self
    }
}

Solution

  • When you clone() the router then modify the cloned router then discard it, you did basically nothing. You should mutate the original router.

    Now I guess that the reason you cloned the router is because you tried without it and the compiler complained "cannot move out of...". But blindly adding a clone does not always help. Like in this case.

    It would help if there would be a route() method that takes &mut self and modifies the router in place. But unfortunately there isn't such method. So for now you can use the techniques mentioned in Temporarily move out of borrowed content, the easiest of which (with no dependencies) is to take() the value (since Router implements Default), modify it, then put it back:

    self.router = std::mem::take(&mut self.router).route(PROJECTS_PATH, get(handler));