Repository: poem-web/poem (language: rust)

Issue:
tokio-runtime-worker panic at src/route/router.rs:240:66
I have been running a simple poem openAPI server. Today I checked my server log and found that there were some panics in tokio runtime due to poem.


## Sample server code
```rust
mod client;
mod datacenter;
mod oauth;
mod refresher;
use crate::client::ConfigGatewayClient;
use crate::refresher::TokenRefresher;
use std::sync::Arc;

use crate::datacenter::Datacenter;
use crate::oauth::service_account_login;
use client::{QueueInformation, QueueQuery};
use dotenv::dotenv;
use poem::{
    http::StatusCode, listener::TcpListener, middleware::Cors, web::Data, EndpointExt, Error,
    Route, Server,
};
use poem_openapi::{
    auth::Basic, param::Path, payload::Json, OpenApi, OpenApiService, SecurityScheme,
};
use regex::Regex;
use tokio::sync::RwLock;

#[derive(SecurityScheme)]
#[oai(type = "basic")]
struct QABasicAuth(Basic);

struct QueueFinderApi;

#[OpenApi]
impl QueueFinderApi {
    fn check_auth(&self, auth: QABasicAuth) -> poem::Result<()> {
        if auth.0.username != std::env::var("AUTHENTICATION_USERNAME").unwrap()
            && auth.0.password != std::env::var("AUTHENTICATION_PASSWORD").unwrap()
        {
            return Err(Error::from_status(StatusCode::UNAUTHORIZED));
        }
        Ok(())
    }

    #[oai(path = "/invalidate/:org", method = "post")]
    async fn invalidate(
        &self,
        auth: QABasicAuth,
        org: Path<String>,
        client: Data<&Arc<ConfigGatewayClient>>,
    ) -> poem::Result<()> {
        self.check_auth(auth)?;
        // Some Implementation
        Ok(())
    }

    #[oai(path = "/find/:id/one", method = "post")]
    async fn findone(
        &self,
        auth: QABasicAuth,
        id: Path<String>,
        query: Json<QueueQuery>,
        client: Data<&Arc<ConfigGatewayClient>>,
    ) -> poem::Result<Json<QueueInformation>> {
        self.check_auth(auth)?;
        // Some implementation
        Ok(Json(queue))
    }

    #[oai(path = "/find/:id/all", method = "post")]
    async fn findall(
        &self,
        auth: QABasicAuth,
        id: Path<String>,
        query: Json<QueueQuery>,
        client: Data<&Arc<ConfigGatewayClient>>,
    ) -> poem::Result<Json<Vec<QueueInformation>>> {
        self.check_auth(auth)?;
        if id.0.is_empty() {
            return Err(Error::from_status(StatusCode::BAD_REQUEST));
        }
        let Ok(regx) = Regex::new(&query.0.queue_regex) else {
            return Err(Error::from_status(StatusCode::BAD_REQUEST));
        };
        Ok(Json(client.fetch_all(&id.0, regx).await.ok_or_else(
            || Error::from_status(StatusCode::INTERNAL_SERVER_ERROR),
        )?))
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    dotenv().ok();
    let dc: Datacenter = std::env::var("DATACENTER")?.parse()?;
    let name = std::env::var("MACHINE_USERNAME")?;
    let password = std::env::var("MACHINE_PASSWORD")?;
    let auth = Arc::new(RwLock::new(
        service_account_login(&name, &password, &dc).await?,
    ));
    let client = Arc::new(ConfigGatewayClient::new(auth.clone(), &dc));
    let mut refresher = TokenRefresher::new(auth.clone(), &dc);

    refresher.start().await;

    let api_service =
        OpenApiService::new(QueueFinderApi, "Queue Finder", env!("CARGO_PKG_VERSION"))
            .server("https://foo.example.com");
    let redoc = api_service.redoc();
    let rapidoc = api_service.rapidoc();
    let app = Route::new()
        .nest("/", api_service)
        .nest("/redoc", redoc)
        .nest("/docs", rapidoc)
        .with(Cors::new())
        .data(client);

    Server::new(TcpListener::bind("0.0.0.0:8080"))
        .run(app)
        .await?;
    Ok(())
}
```

## Specifications

```
Dec 28 22:59:07 ip-172-31-33-157 queue-finder[2578]: [2022-12-28T22:59:07Z INFO  queue_finder::refresher] Token refresh to be performed after 64749 seconds
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 29 00:51:47 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 29 16:58:16 ip-172-31-33-157 queue-finder[2578]: [2022-12-29T16:58:16Z INFO  queue_finder::refresher] Performing token refresh using refresh token
Dec 29 16:58:17 ip-172-31-33-157 queue-finder[2578]: [2022-12-29T16:58:17Z INFO  queue_finder::refresher] Token refresh completed, releasing token lock for read access
Dec 29 16:58:17 ip-172-31-33-157 queue-finder[2578]: [2022-12-29T16:58:17Z INFO  queue_finder::refresher] Token refresh to be performed after 64749 seconds
Dec 30 01:00:32 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 30 01:00:33 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 30 01:00:33 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 30 01:00:33 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 30 01:00:33 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 30 01:00:33 ip-172-31-33-157 queue-finder[2578]: thread 'tokio-runtime-worker' panicked at 'removal index (is 18446744073709551615) should be < len (is 0)', /home/ubuntu/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.50/src/route/router.rs:240:66
Dec 30 10:36:21 ip-172-31-33-157 queue-finder[2578]: [2022-12-30T10:36:21Z INFO  queue_finder::client] Cache miss for org 89b474a3-17d9-41ff-8fb0-534bebdfb160
```
My service has been running fine the API is reachable but I am opening this issue anyways since it looks some error at poem router.

- Version: poem-1.3.50
- Platform: Ubuntu 22.04LTS
- Subsystem: Linux EC2


Discussion / hints:
It seems `req.state().match_params.len()` is 0.
https://github.com/poem-web/poem/blob/7ada5b69c94c0a98ddff64172703ed61c3386fdf/poem/src/route/router.rs#L238-L242

Do you know what the requested url is?
We recently received the same panic, although I haven't found yet what request URL caused this. 

(btw, thanks for Poem, it works amazing!)
We recently received the panic. The request url:  /%c0.%c0./%c0.%c0./%c0.%c0./%c0.%c0./winnt/win.ini
The panic is triggered by /%c0

```
    let app = Route::new().nest(
        "/",
        StaticFilesEndpoint::new("./").index_file("index.html"),
    );

    server.run(app).await
}
I tried fuzzing it, found a few similar cases: "/%d0", "/%d1", "/%e0", "//%e0", "/0%e0", "/e%e0", "/%A0"

Fix patch:
diff --git a/poem/src/route/router.rs b/poem/src/route/router.rs
index 24622013af..4b4817f563 100644
--- a/poem/src/route/router.rs
+++ b/poem/src/route/router.rs
@@ -4,7 +4,7 @@ use regex::Regex;
 
 use crate::{
     endpoint::BoxEndpoint,
-    error::{NotFoundError, RouteError},
+    error::{NotFoundError, ParsePathError, RouteError},
     http::{uri::PathAndQuery, Uri},
     route::{check_result, internal::radix_tree::RadixTree},
     Endpoint, EndpointExt, IntoEndpoint, IntoResponse, Request, Response, Result,
@@ -250,9 +250,12 @@ impl Route {
 
             async fn call(&self, mut req: Request) -> Result<Self::Output> {
                 if !self.root {
-                    let idx = req.state().match_params.len() - 1;
-                    let (name, _) = req.state_mut().match_params.remove(idx);
-                    assert_eq!(name, "--poem-rest");
+                    let params = &mut req.state_mut().match_params;
+                    if params.last().map(|(name, _)| name.as_str()) != Some("--poem-rest") {
+                        return Err(ParsePathError.into());
+                    }
+
+                    params.pop().expect("can't be empty due to a check above");
                 }
 
                 let new_uri = {
@@ -535,6 +538,17 @@ mod tests {
         );
     }
 
+    #[tokio::test]
+    async fn issue_471() {
+        let app = Route::new().nest("/", make_sync(|_| "hello"));
+        assert_eq!(
+            app.get_response(Request::builder().uri(Uri::from_static("/%d0")).finish())
+                .await
+                .status(),
+            StatusCode::BAD_REQUEST
+        );
+    }
+
     #[tokio::test]
     async fn path_pattern() {
         let app = Route::new()


Test patch: