tower-rs/tower-http

"Example server" example in the docs doesn't actually run a server.

Opened this issue · 2 comments

Bug Report

The docs have a section "Example server" section which claims to "run that service using hyper."

//! # Example server
//!
//! This example shows how to apply middleware from tower-http to a [`Service`] and then run
//! that service using [hyper].
//!
//! ```rust,no_run
//! use tower_http::{
//! add_extension::AddExtensionLayer,
//! compression::CompressionLayer,
//! propagate_header::PropagateHeaderLayer,
//! sensitive_headers::SetSensitiveRequestHeadersLayer,
//! set_header::SetResponseHeaderLayer,
//! trace::TraceLayer,
//! validate_request::ValidateRequestHeaderLayer,
//! };
//! use tower::{ServiceBuilder, service_fn, BoxError};
//! use http::{Request, Response, header::{HeaderName, CONTENT_TYPE, AUTHORIZATION}};
//! use std::{sync::Arc, net::SocketAddr, convert::Infallible, iter::once};
//! use bytes::Bytes;
//! use http_body_util::Full;
//! # struct DatabaseConnectionPool;
//! # impl DatabaseConnectionPool {
//! # fn new() -> DatabaseConnectionPool { DatabaseConnectionPool }
//! # }
//! # fn content_length_from_response<B>(_: &http::Response<B>) -> Option<http::HeaderValue> { None }
//! # async fn update_in_flight_requests_metric(count: usize) {}
//!
//! // Our request handler. This is where we would implement the application logic
//! // for responding to HTTP requests...
//! async fn handler(request: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, BoxError> {
//! // ...
//! # todo!()
//! }
//!
//! // Shared state across all request handlers --- in this case, a pool of database connections.
//! struct State {
//! pool: DatabaseConnectionPool,
//! }
//!
//! #[tokio::main]
//! async fn main() {
//! // Construct the shared state.
//! let state = State {
//! pool: DatabaseConnectionPool::new(),
//! };
//!
//! // Use tower's `ServiceBuilder` API to build a stack of tower middleware
//! // wrapping our request handler.
//! let service = ServiceBuilder::new()
//! // Mark the `Authorization` request header as sensitive so it doesn't show in logs
//! .layer(SetSensitiveRequestHeadersLayer::new(once(AUTHORIZATION)))
//! // High level logging of requests and responses
//! .layer(TraceLayer::new_for_http())
//! // Share an `Arc<State>` with all requests
//! .layer(AddExtensionLayer::new(Arc::new(state)))
//! // Compress responses
//! .layer(CompressionLayer::new())
//! // Propagate `X-Request-Id`s from requests to responses
//! .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-request-id")))
//! // If the response has a known size set the `Content-Length` header
//! .layer(SetResponseHeaderLayer::overriding(CONTENT_TYPE, content_length_from_response))
//! // Authorize requests using a token
//! .layer(ValidateRequestHeaderLayer::bearer("passwordlol"))
//! // Accept only application/json, application/* and */* in a request's ACCEPT header
//! .layer(ValidateRequestHeaderLayer::accept("application/json"))
//! // Wrap a `Service` in our middleware stack
//! .service_fn(handler);
//! # let mut service = service;
//! # tower::Service::call(&mut service, Request::new(Full::default()));
//! }
//! ```
//!
//! Keep in mind that while this example uses [hyper], tower-http supports any HTTP
//! client/server implementation that uses the [http] and [http-body] crates.

However this doesn't seem to use Hyper at all and doesn't actually run any server. There is just hidden code to send a request to the Service from Rust.

Description

It seems that either the wording around the example should be updated or the example should be fixed to actually run a server.

It would be nice to have a minimal server example, as it seems like right now the way to do this is to use a "full featured" framework like axum or warp just to run a tower Service.

I think this is as minimal as it gets when not using something like axum / warp.

Want to incorporate that into the docs in a PR?

similar minimal version but without serving a file:

main.rs:

use std::convert::Infallible;
use std::net::SocketAddr;

use http_body_util::Full;
use hyper::body::Bytes;
use hyper::{Request, Response};
use tokio::net::TcpListener;
use tower::ServiceBuilder;

async fn handle(_req: Request<hyper::body::Incoming>) -> Result<Response<Full<Bytes>>, Infallible> {
    Ok(Response::new(Full::new(Bytes::from("hello, world"))))
}

#[tokio::main]
async fn main() {
    let tower_service = ServiceBuilder::new().service_fn(handle);
    let hyper_service = hyper_util::service::TowerToHyperService::new(tower_service);

    let addr = SocketAddr::from(([0, 0, 0, 0], 5000));
    let listener = TcpListener::bind(addr).await.unwrap();

    loop {
        let (stream, _) = listener.accept().await.unwrap();

        let io = hyper_util::rt::TokioIo::new(stream);
        let service_clone = hyper_service.clone();

        tokio::task::spawn(async move {
            if let Err(err) =
                hyper_util::server::conn::auto::Builder::new(hyper_util::rt::TokioExecutor::new())
                    .serve_connection(io, service_clone)
                    .await
            {
                eprintln!("server error: {}", err);
            }
        });
    }
}

Cargo.toml:

[package]
name = "hyper-hello-world"
version = "0.1.0"
edition = "2021"

[dependencies]
bytes = { version = "1", default-features = false }
hyper = { version = "1", default-features = false }
http-body-util = { version = "0.1", default-features = false }
hyper-util = { version = "0.1", features = ["http1", "service", "server", "tokio"], default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros"], default-features = false }
tower = { version = "0.5", default-features = false, features = ["util"] }
tower-http = { version = "0.6", default-features = false }