tsukuyomi-rs / izanami

[WIP] A simple Web application interface inspired from ASGI.
https://tsukuyomi-rs.github.io/izanami
Apache License 2.0
3 stars 0 forks source link

[WIP] ASGI-like interface #43

Open ubnt-intrepid opened 4 years ago

ubnt-intrepid commented 4 years ago

The many of Rust Web application frameworks and HTTP server implementations use the RPC-like HTTP application interfaces, which forces the specific type of response body. On this thread we discuss about the new Web application interface similar to Python's ASGI.

ubnt-intrepid commented 4 years ago

In the current Web servers for Rust, the Web applications are (roughly) represented such as the following async function:

async fn app(request: Request<impl Body>) -> Result<Response<impl Body>>;

where Body is a trait that represents the streaming HTTP messages from/to the client, that emits chunks of bytes or a trailer HeaderMap. This abstraction is consistent with the many RESTful Web applications and has the advantage of being intuitively easy to understand for the Web application authors.

However, the above abstraction has the following drawbacks when the Web application take a non-RPC form like WebSocket:

The above problems could be solved by stopping the response as the function's return value, and sending then within the function by a channel. Consider the following Web application interface:

async fn app(request: Request<impl Body>, responder: impl Responder) -> Result<()>

Here, the trait Responder is defined as follows:

trait Responder {
    async fn respond(self, response: Response<impl Body>) -> Result<()>;
}

An example:

async fn app(req: Request<impl Body>, res: impl Responder) -> Result<()> {
    match recognize_path(req.uri().path()) {
        Ok(App::Index) => res.respond(index_page()).await?,
        Ok(App::Api(param)) => {
            let conn = DB_POOL.acquire().await;
            let response = handle_api(param, &conn).await;
            res.respond(response).await?;
            DB_POOL.release(conn).await;
        },
        Ok(App::Chat(handshake)) => {
            res.respond(handshake.to_response()).await?;
            let io = req.into_body().on_upgrade().await;
            ...
        }
        Err(AppError::NoRoute) => res.respond(not_found()).await?,
        Err(err) => res.respond(err.to_response()).await?,
    }
    Ok(())
}

Since both the request body and responder are provided by the Web server, they could be combined into one variable. Considering this, the above Web application interface could be simplified as follows:

async fn app(request: Request<impl Events>) -> Result<()>;

where events is an asynchronous object to exchange the all kind of events to the client.

trait Events {
    type Data: Buf;
    type Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>;

    async fn data(&mut self) -> Option<Result<Self::Data, Self::Error>>;
    async fn trailers(&mut self) -> Result<Option<HeaderMap>, Self::Error>;

    async fn start_send_response(&mut self, response: Response<()>, end_of_stream: bool) -> Result<(), Self::Error>;
    async fn send_data(&mut self, data: Self::Data, end_of_stream: bool) -> Result<(), Self::Error>;
    async fn send_trailers(&mut self, trailers: HeaderMap) -> Result<(), Self::Error>;
}
ubnt-intrepid commented 4 years ago

The implementation is started in #44.