├── .gitignore
├── examples
├── postgres
│ ├── .gitignore
│ ├── create_db.sql
│ ├── Cargo.toml
│ └── src
│ │ ├── pool.rs
│ │ └── main.rs
├── tokio-tophat
│ ├── .gitignore
│ ├── Cargo.toml
│ └── src
│ │ └── main.rs
├── bench.rs
├── bench-multi.rs
├── basic.rs
├── observability_bytes_written.rs
├── errors.rs
├── routing.rs
├── server_sent_events.rs
├── cors.rs
├── server_sent_events_2.rs
├── errors_verbose.rs
├── middleware.rs
└── identity.rs
├── src
├── chunked
│ ├── mod.rs
│ └── encoder.rs
├── request.rs
├── response.rs
├── client
│ ├── mod.rs
│ ├── error.rs
│ ├── decode.rs
│ └── encode.rs
├── lib.rs
├── server
│ ├── error.rs
│ ├── mod.rs
│ ├── router.rs
│ ├── encode.rs
│ ├── response_writer.rs
│ ├── identity.rs
│ ├── glitch.rs
│ ├── decode.rs
│ └── cors.rs
├── timeout.rs
├── trailers.rs
├── util.rs
└── body.rs
├── CHANGELOG.md
├── LICENSE-MIT
├── justfile
├── Cargo.toml
├── tests
├── client_basic.rs
├── server_error_handling.rs
├── mock.rs
└── server_basic.rs
├── .github
└── workflows
│ └── ci.yaml
├── NOTES.md
├── README.md
└── LICENSE-APACHE
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 | .cookie
4 | .env
5 |
--------------------------------------------------------------------------------
/examples/postgres/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 | .env
4 |
--------------------------------------------------------------------------------
/examples/tokio-tophat/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | Cargo.lock
3 | .env
4 |
--------------------------------------------------------------------------------
/src/chunked/mod.rs:
--------------------------------------------------------------------------------
1 | mod decoder;
2 | mod encoder;
3 |
4 | pub(crate) use decoder::ChunkedDecoder;
5 | pub(crate) use encoder::ChunkedEncoder;
6 |
--------------------------------------------------------------------------------
/src/request.rs:
--------------------------------------------------------------------------------
1 | use http::Request as HttpRequest;
2 |
3 | use crate::body::Body;
4 |
5 | /// Currently, Request is not generic over Body type
6 | pub type Request = HttpRequest
;
7 |
--------------------------------------------------------------------------------
/src/response.rs:
--------------------------------------------------------------------------------
1 | use http::Response as HttpResponse;
2 |
3 | use crate::body::Body;
4 |
5 | /// Currently, Response is not generic over Body type
6 | pub type Response = HttpResponse;
7 |
--------------------------------------------------------------------------------
/examples/postgres/create_db.sql:
--------------------------------------------------------------------------------
1 | create table if not exists test_users (id int, planet text, organization text);
2 |
3 | insert into test_users (id, planet, organization) values
4 | (1,'tatooine','jedi'),
5 | (2,'tatooine','new republic'),
6 | (3,'nevarro','guild'),
7 | (4,'mandalore','guild');
8 |
--------------------------------------------------------------------------------
/examples/tokio-tophat/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tokio-tophat"
3 | version = "0.1.0"
4 | authors = ["Walther Chen "]
5 | edition = "2018"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | async-dup = "1.2.2"
11 | futures-lite = "1.11.3"
12 | tokio = { version = "1", features = ["full"] }
13 | tophat = { path = "../../" }
14 | http = "0.2.2"
15 | tokio-util = { version = "0.6.0", features = ["compat"] }
16 | tracing-subscriber = "0.2.15"
17 |
--------------------------------------------------------------------------------
/src/client/mod.rs:
--------------------------------------------------------------------------------
1 | //! Simple client for HTTP/1.1
2 |
3 | mod decode;
4 | mod encode;
5 | mod error;
6 |
7 | use futures_lite::{io, AsyncRead, AsyncWrite};
8 |
9 | use crate::{Request, Response};
10 | use decode::decode;
11 | use encode::Encoder;
12 | use error::ClientError;
13 |
14 | /// Opens an HTTP/1.1 connection to a remote host.
15 | pub async fn connect(mut stream: RW, req: Request) -> Result
16 | where
17 | RW: AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
18 | {
19 | let mut req = Encoder::encode(req).await?;
20 |
21 | io::copy(&mut req, &mut stream).await.map_err(error::io)?;
22 |
23 | let res = decode(stream).await?;
24 |
25 | Ok(res)
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![deny(unsafe_code)]
2 | #![warn(missing_docs)]
3 |
4 | //! # tophat
5 | //!
6 | //! A small, pragmatic, and flexible async HTTP server library.
7 | //!
8 | //! More docs coming soon! For now, please see the examples directory for features.
9 | //!
10 | //! Also, please note that you'll need to set up your own async runtime to work with tophat. All
11 | //! the examples use `smol` as the runtime.
12 |
13 | mod body;
14 | mod chunked;
15 | pub mod client;
16 | mod request;
17 | mod response;
18 | pub mod server;
19 | mod timeout;
20 | pub mod trailers;
21 | mod util;
22 |
23 | /// Re-export http crate for convenience
24 | pub use http;
25 |
26 | pub use body::Body;
27 | pub use request::Request;
28 | pub use response::Response;
29 |
--------------------------------------------------------------------------------
/examples/postgres/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "postgres-example"
3 | version = "0.1.0"
4 | authors = ["Walther Chen "]
5 | edition = "2018"
6 |
7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
8 |
9 | [dependencies]
10 | anyhow = "1.0.37"
11 | async-dup = "1.2.2"
12 | async-trait = "0.1.42"
13 | deadpool = "0.7.0"
14 | dotenv = "0.15.0"
15 | futures-lite = "1.11.3"
16 | tophat = { path = "../../", features = ["router"] }
17 | http = "0.2.2"
18 | smol = "1.2.5"
19 | thiserror = "1.0.23"
20 | tokio-postgres = { version = "0.7.0", default_features = false }
21 | tokio-util = { version = "0.6.0", features = ["compat"] }
22 | tracing = "0.1.22"
23 | tracing-subscriber = "0.2.15"
24 | url = "2.2.0"
25 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 2020-05-19, v0.2.0
2 | ## Features
3 | - `ResponseWriter` now holds a `Response`, so it's not need to create one separately.
4 | - Convenience methods on `ResponseWriter`.
5 | - `Glitch` and `GlitchExt` for error management to error response.
6 | - `ResponseWritten` no longer creatable by user.
7 | - Remove unwrap macros (were they ever a good idea?).
8 | - Router now behind feature gate.
9 | - Cors, feature gated.
10 | - Identity, feature gated.
11 | - "Middleware" philosophy confirmed. (no specific framework for it)
12 | - Beginning of docs.
13 |
14 |
15 | ## Internal
16 | - remove `mime` crate.
17 | - pub use `http` crate.
18 | - remove more unwraps.
19 | - ci on all features
20 | - remove clippy on stable (nightly has different lints)
21 | - anyhow was added then removed.
22 |
--------------------------------------------------------------------------------
/examples/bench.rs:
--------------------------------------------------------------------------------
1 | use async_dup::Arc;
2 | use smol::Async;
3 | use std::net::TcpListener;
4 | use tophat::server::accept;
5 |
6 | fn main() -> Result<(), Box> {
7 | let listener = Async::::bind(([127,0,0,1],9999))?;
8 |
9 | smol::block_on(async {
10 | loop {
11 | let (stream, _) = listener.accept().await?;
12 | let stream = Arc::new(stream);
13 |
14 | let task = smol::spawn(async move {
15 | let serve = accept(stream, |_req, resp_wtr| async { resp_wtr.send().await }).await;
16 |
17 | if let Err(err) = serve {
18 | eprintln!("Error: {}", err);
19 | }
20 | });
21 |
22 | task.detach();
23 | }
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 tophat Developers
2 |
3 | Permission is hereby granted, free of charge, to any
4 | person obtaining a copy of this software and associated
5 | documentation files (the "Software"), to deal in the
6 | Software without restriction, including without
7 | limitation the rights to use, copy, modify, merge,
8 | publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software
10 | is furnished to do so, subject to the following
11 | conditions:
12 |
13 | The above copyright notice and this permission notice
14 | shall be included in all copies or substantial portions
15 | of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 | DEALINGS IN THE SOFTWARE.
26 |
--------------------------------------------------------------------------------
/justfile:
--------------------------------------------------------------------------------
1 | watch:
2 | cargo watch -x 'check --all-features --examples --tests'
3 |
4 | test filter='':
5 | cargo watch -x 'test {{filter}}'
6 |
7 | test-anyhow:
8 | cargo watch -x 'test --features=anyhow -- --nocapture'
9 |
10 | bench:
11 | RUST_LOG=info cargo watch -x 'run --release --example bench'
12 |
13 | basic:
14 | RUST_LOG=info cargo watch -x 'run --release --example basic'
15 |
16 | routing:
17 | RUST_LOG=info cargo watch -x 'run --release --example routing --features="router"'
18 |
19 | routing_2:
20 | RUST_LOG=info cargo watch -x 'run --release --example routing_2 --features="router"'
21 |
22 | identity:
23 | RUST_LOG=info cargo watch --ignore .cookie -x 'run --release --example identity --features="router identity"'
24 |
25 | identity-login:
26 | curl -v --cookie .cookie --cookie-jar .cookie --location localhost:9999/login/test
27 |
28 | identity-hello:
29 | curl -v --cookie .cookie localhost:9999/
30 |
31 | identity-logout:
32 | curl -v --cookie .cookie --cookie-jar .cookie --location localhost:9999/logout
33 |
34 | clippy:
35 | cargo watch -x '+nightly clippy --all-features -- -D warnings -Z unstable-options'
36 |
--------------------------------------------------------------------------------
/examples/bench-multi.rs:
--------------------------------------------------------------------------------
1 | use async_channel::unbounded;
2 | use async_dup::Arc;
3 | use easy_parallel::Parallel;
4 | use smol::{future, Async, Executor};
5 | use std::net::TcpListener;
6 | use tophat::server::accept;
7 |
8 | fn main() -> Result<(), Box> {
9 | let ex = Executor::new();
10 | let (signal, shutdown) = unbounded::<()>();
11 |
12 | Parallel::new()
13 | .each(0..num_cpus::get().max(1), |_| future::block_on(ex.run(shutdown.recv())))
14 | .finish(|| future::block_on(async {
15 | drop(signal);
16 | }));
17 |
18 | let listener = Async::::bind(([127,0,0,1],9999))?;
19 |
20 | smol::block_on(async {
21 | loop {
22 | let (stream, _) = listener.accept().await?;
23 | let stream = Arc::new(stream);
24 |
25 | let task = smol::spawn(async move {
26 | let serve = accept(stream, |_req, resp_wtr| async { resp_wtr.send().await }).await;
27 |
28 | if let Err(err) = serve {
29 | eprintln!("Error: {}", err);
30 | }
31 | });
32 |
33 | task.detach();
34 | }
35 | })
36 | }
37 |
--------------------------------------------------------------------------------
/src/server/error.rs:
--------------------------------------------------------------------------------
1 | //! Errors that indicate system failure, user error in using tophat, or closed connection.
2 | //!
3 | //! "App" errors, which are handled within an endpoint and result only in loggin and an Http
4 | //! Response, are handled by `Glitch`.
5 |
6 | use std::fmt;
7 |
8 | /// Public Errors (does not include internal fails)
9 | #[derive(Debug)]
10 | pub enum ServerError {
11 | /// Error because tophat does not support the transfer encoding.
12 | ConnectionClosedUnsupportedTransferEncoding,
13 |
14 | /// Connection lost
15 | ConnectionLost(std::io::Error),
16 | }
17 |
18 | impl std::error::Error for ServerError {
19 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
20 | use ServerError::*;
21 | match self {
22 | ConnectionClosedUnsupportedTransferEncoding => None,
23 | ConnectionLost(err) => Some(err),
24 | }
25 | }
26 | }
27 |
28 | impl fmt::Display for ServerError {
29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 | use ServerError::*;
31 | match self {
32 | ConnectionClosedUnsupportedTransferEncoding => {
33 | write!(f, "Connection closed: Unsupported Transfer Encoding")
34 | }
35 | ConnectionLost(err) => write!(f, "Connection lost: {}", err),
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/examples/basic.rs:
--------------------------------------------------------------------------------
1 | use async_dup::Arc;
2 | use http::header;
3 | use smol::Async;
4 | use std::net::TcpListener;
5 | use tophat::server::accept;
6 |
7 | fn main() -> Result<(), Box> {
8 | tracing_subscriber::fmt::init();
9 |
10 | let listener = Async::::bind(([127,0,0,1],9999))?;
11 |
12 | smol::block_on(async {
13 | loop {
14 | let (stream, _) = listener.accept().await?;
15 | let stream = Arc::new(stream);
16 |
17 | let task = smol::spawn(async move {
18 | let serve = accept(stream, |req, mut resp_wtr| async {
19 | println!("{:?}", *req.uri());
20 | println!("{:?}", req.version());
21 | println!("{:?}", req.method());
22 | println!("{:?}", req.headers().get(header::CONTENT_LENGTH));
23 | println!("{:?}", req.headers().get(header::HOST));
24 |
25 | let req_body = req.into_body().into_string().await?;
26 | let resp_body = format!("Hello, {}!", req_body);
27 | resp_wtr.set_body(resp_body.into());
28 |
29 | resp_wtr.send().await
30 | })
31 | .await;
32 |
33 | if let Err(err) = serve {
34 | eprintln!("Error: {}", err);
35 | }
36 | });
37 |
38 | task.detach();
39 | }
40 | })
41 | }
42 |
--------------------------------------------------------------------------------
/examples/observability_bytes_written.rs:
--------------------------------------------------------------------------------
1 | use async_dup::Arc;
2 | use http::header;
3 | use smol::Async;
4 | use std::net::TcpListener;
5 | use tophat::server::accept;
6 |
7 | fn main() -> Result<(), Box> {
8 | tracing_subscriber::fmt::init();
9 |
10 | let listener = Async::::bind(([127,0,0,1],9999))?;
11 |
12 | smol::block_on(async {
13 | loop {
14 | let (stream, _) = listener.accept().await?;
15 | let stream = Arc::new(stream);
16 |
17 | let task = smol::spawn(async move {
18 | let serve = accept(stream, |req, mut resp_wtr| async {
19 | println!("{:?}", *req.uri());
20 | println!("{:?}", req.version());
21 | println!("{:?}", req.method());
22 | println!("{:?}", req.headers().get(header::CONTENT_LENGTH));
23 | println!("{:?}", req.headers().get(header::HOST));
24 |
25 | let req_body = req.into_body().into_string().await?;
26 | let resp_body = format!("Hello, {}!", req_body);
27 | resp_wtr.set_body(resp_body.into());
28 |
29 | let done = resp_wtr.send().await?;
30 |
31 | println!("Bytes written: {}", done.bytes_written());
32 |
33 | Ok(done)
34 |
35 | })
36 | .await;
37 |
38 | if let Err(err) = serve {
39 | eprintln!("Error: {}", err);
40 | }
41 | });
42 |
43 | task.detach();
44 | }
45 | })
46 | }
47 |
--------------------------------------------------------------------------------
/src/timeout.rs:
--------------------------------------------------------------------------------
1 | // From async-std future::timeout, except that futures_timer is swapped in.
2 |
3 | use std::error::Error;
4 | use std::fmt;
5 | use std::future::Future;
6 | use std::pin::Pin;
7 | use std::task::{Context, Poll};
8 | use std::time::Duration;
9 |
10 | use futures_timer::Delay;
11 | use pin_project_lite::pin_project;
12 |
13 |
14 | pub(crate) async fn timeout(dur: Duration, f: F) -> Result
15 | where
16 | F: Future