{"id":47759407,"url":"https://github.com/willothy/tinyweb","last_synced_at":"2026-04-03T05:06:36.884Z","repository":{"id":342710194,"uuid":"1174868346","full_name":"willothy/tinyweb","owner":"willothy","description":"WIP transport and runtime-agnostic http2 server framework","archived":false,"fork":false,"pushed_at":"2026-03-07T02:19:52.000Z","size":56,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-07T04:44:54.004Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/willothy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-06T23:43:11.000Z","updated_at":"2026-03-07T02:19:55.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/willothy/tinyweb","commit_stats":null,"previous_names":["willothy/tinyweb"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/willothy/tinyweb","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willothy%2Ftinyweb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willothy%2Ftinyweb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willothy%2Ftinyweb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willothy%2Ftinyweb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/willothy","download_url":"https://codeload.github.com/willothy/tinyweb/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/willothy%2Ftinyweb/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31335249,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-03T04:42:29.251Z","status":"ssl_error","status_checked_at":"2026-04-03T04:42:12.667Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-03T05:06:36.178Z","updated_at":"2026-04-03T05:06:36.879Z","avatar_url":"https://github.com/willothy.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# tinyweb\n\nA minimal, proc-macro-free HTTP/2 server framework for Rust.\n\nBuilt directly on [h2](https://crates.io/crates/h2) with no proc macros —\nhandler extraction uses declarative macros and standard traits. Runtime and\ntransport agnostic, with support for both multi-threaded (tokio) and\nsingle-threaded (compio) runtimes.\n\n## Features\n\n- **HTTP/2 only** — built on the `h2` crate\n- **No proc macros** — extractors via `FromRequest` / `FromRequestParts` traits\n- **Runtime agnostic** — tokio or compio via feature flags\n- **Transport agnostic** — TCP, Unix sockets, or any `AsyncRead + AsyncWrite` stream\n- **Optional `Send` bounds** — `send` feature for multi-threaded runtimes, omit for single-threaded\n- **Layered middleware** — composable `Layer\u003cS: Service\u003e` middleware on handlers, routes, or whole routers\n\n## Crate Structure\n\n| Crate | Purpose |\n|---|---|\n| `tinyweb-core` | Core traits, extractors, router, body types |\n| `tinyweb-tokio` | Tokio runtime, TCP/Unix incoming, `futures_io` compat |\n| `tinyweb-compio` | Compio runtime, TCP/Unix incoming |\n| `tinyweb` | Server loop (`serve`, `serve_connection`), re-exports |\n\n## Quick Start\n\n```toml\n[dependencies]\ntinyweb = { version = \"0.1\", features = [\"tokio\"] }\n```\n\n```rust\nuse tinyweb::{Router, server};\nuse tinyweb_tokio::{TokioRuntime, TcpIncoming};\n\nasync fn hello() -\u003e \u0026'static str {\n    \"Hello, world!\"\n}\n\n#[tokio::main]\nasync fn main() {\n    let router = Router::new().get(\"/\", hello);\n\n    let listener = tokio::net::TcpListener::bind(\"127.0.0.1:8080\")\n        .await\n        .unwrap();\n\n    server::serve(TcpIncoming(listener), router, TokioRuntime)\n        .await\n        .unwrap();\n}\n```\n\n## Feature Flags\n\n| Flag | Default | Description |\n|---|---|---|\n| `send` | off | `Send` bounds on futures/traits for multi-threaded runtimes |\n| `tokio` | off | Tokio runtime adapter (implies `send`) |\n| `compio` | off | Compio runtime adapter |\n\n## Serving\n\nTwo levels of server API:\n\n**`serve`** takes an `Incoming` (stream of connections) and serves each one\nconcurrently. This is the typical entrypoint.\n\n```rust\n// TCP\nlet listener = tokio::net::TcpListener::bind(\"0.0.0.0:443\").await?;\nserver::serve(TcpIncoming(listener), router, TokioRuntime).await?;\n\n// Unix socket\nlet listener = tokio::net::UnixListener::bind(\"/tmp/app.sock\")?;\nserver::serve(UnixIncoming(listener), router, TokioRuntime).await?;\n```\n\n**`serve_connection`** serves a single IO stream directly — no `Incoming` needed.\nThe IO type just needs `futures_io::AsyncRead + AsyncWrite`.\n\n```rust\nlet (stream, _addr) = listener.accept().await?;\nserver::serve_connection(TokioIoCompat::new(stream), router, TokioRuntime).await?;\n```\n\n## Handlers\n\nAny async function is a handler. The return type must implement `IntoResponse`.\nArguments are extracted from the request via `FromRequestParts` (headers, URI,\nmethod) or `FromRequest` (consumes the body — must be the last argument). Up to\n10 extractor arguments are supported.\n\n```rust\nasync fn index() -\u003e \u0026'static str {\n    \"hello\"\n}\n\nasync fn echo(Json(body): Json\u003cserde_json::Value\u003e) -\u003e Json\u003cserde_json::Value\u003e {\n    Json(body)\n}\n```\n\n## Extractors\n\n`FromRequestParts` extractors pull data from the request without consuming the\nbody. `FromRequest` extractors consume the body and must be the last argument.\n\n| Extractor | Trait | Description |\n|---|---|---|\n| `http::Method` | `FromRequestParts` | HTTP method |\n| `http::Uri` | `FromRequestParts` | Request URI |\n| `http::HeaderMap` | `FromRequestParts` | Request headers |\n| `bytes::Bytes` | `FromRequest` | Raw body bytes |\n| `BodyStream` | `FromRequest` | Streaming body (implements `Stream\u003cItem = Result\u003cBytes, h2::Error\u003e\u003e`) |\n| `Json\u003cT\u003e` | `FromRequest` | Deserialize JSON body (`T: DeserializeOwned`) |\n\n```rust\nasync fn handler(method: http::Method, uri: http::Uri, body: bytes::Bytes) -\u003e \u0026'static str {\n    \"ok\"\n}\n```\n\n## Responses\n\nAny type implementing `IntoResponse` can be returned from a handler.\n\n| Type | Status | Content-Type |\n|---|---|---|\n| `\u0026'static str` | 200 | `text/plain` |\n| `String` | 200 | `text/plain; charset=utf-8` |\n| `bytes::Bytes` | 200 | `application/octet-stream` |\n| `Json\u003cT\u003e` | 200 | `application/json` (`T: Serialize`) |\n| `http::StatusCode` | given | — |\n| `()` | 200 | — |\n| `Result\u003cT, E\u003e` | — | `T` on `Ok`, `E` on `Err` |\n| `http::Response\u003cBody\u003e` | — | full control |\n\n## Router\n\nBuilder-style route registration with [matchit](https://crates.io/crates/matchit)\nfor path matching (supports `{param}` and `{*wildcard}` syntax).\n\n```rust\nlet router = Router::new()\n    .get(\"/\", index)\n    .get(\"/users/{id}\", get_user)\n    .post(\"/users\", create_user)\n    .delete(\"/users/{id}\", delete_user)\n    .many(\u0026[Method::GET, Method::HEAD], \"/health\", health);\n```\n\n`Router` implements `Service`, so it plugs directly into `serve` and\n`serve_connection`.\n\n## Middleware\n\nMiddleware is built on two traits: `Service` and `Layer`.\n\n`Service` is anything that takes a request and returns a response.\n`Layer\u003cS: Service\u003e` wraps a service to produce a new service.\nHandlers, routers, and middleware all compose through these same traits.\n\n### Per-handler middleware\n\nApply a layer to a single handler before registering it:\n\n```rust\nlet router = Router::new()\n    .get(\"/admin\", admin_handler.layer(auth))\n    .get(\"/public\", public_handler);\n```\n\n### Per-router middleware\n\nApply a layer to all routes registered so far. Routes added after `.layer()`\nare not affected:\n\n```rust\nlet router = Router::new()\n    .get(\"/admin/users\", list_users)\n    .get(\"/admin/settings\", settings)\n    .layer(require_admin)       // wraps both admin routes\n    .get(\"/\", index);           // no admin middleware\n```\n\n### Whole-service wrapping\n\nWrap an entire service (including a router) with `Service::layer()`:\n\n```rust\nlet app = router.layer(logging);\nserver::serve(incoming, app, runtime).await?;\n```\n\n### Writing middleware\n\nImplement `Layer` and `Service`:\n\n```rust\n#[derive(Clone)]\nstruct LogLayer;\n\nimpl\u003cS: Service\u003e Layer\u003cS\u003e for LogLayer {\n    type Service = LogService\u003cS\u003e;\n\n    fn layer(self, inner: S) -\u003e Self::Service {\n        LogService { inner }\n    }\n}\n\n#[derive(Clone)]\nstruct LogService\u003cS\u003e {\n    inner: S,\n}\n\nimpl\u003cS: Service\u003e Service for LogService\u003cS\u003e {\n    fn call(\n        \u0026self,\n        req: http::Request\u003ch2::RecvStream\u003e,\n    ) -\u003e BoxFuture\u003c'static, http::Response\u003cBody\u003e\u003e {\n        let method = req.method().clone();\n        let path = req.uri().path().to_string();\n        let inner = self.inner.clone();\n        Box::pin(async move {\n            let response = inner.call(req).await;\n            println!(\"{method} {path} -\u003e {}\", response.status());\n            response\n        })\n    }\n}\n```\n\n## Custom Transport\n\nImplement `Incoming` to use any transport:\n\n```rust\nimpl Incoming for MyTransport {\n    type Io = MyStream; // futures_io::AsyncRead + AsyncWrite\n    type Addr = MyAddr;\n    type Error = std::io::Error;\n\n    fn accept(\u0026mut self) -\u003e BoxFuture\u003c'_, Result\u003c(Self::Io, Self::Addr), Self::Error\u003e\u003e {\n        Box::pin(async move { /* accept a connection */ })\n    }\n}\n```\n\n## Custom Runtime\n\nImplement `Runtime` to use a runtime other than tokio or compio:\n\n```rust\n#[derive(Clone)]\nstruct MyRuntime;\n\nimpl Runtime for MyRuntime {\n    fn spawn(\u0026self, fut: BoxFuture\u003c'static, ()\u003e) {\n        // spawn the future\n    }\n}\n```\n\n## `send` Feature\n\nControls whether futures and trait objects require `Send + Sync`, via\n`MaybeSend` / `MaybeSync` conditional traits in `tinyweb-core`:\n\n- **With `send`** (tokio): `MaybeSend` requires `Send`, `BoxFuture` includes `+ Send`\n- **Without `send`** (compio): `MaybeSend` is a no-op, no `Send` bound on `BoxFuture`\n\n`tinyweb-tokio` and `tinyweb-compio` cannot be compiled together — they require\nmutually exclusive states of the `send` feature on `tinyweb-core`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwillothy%2Ftinyweb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwillothy%2Ftinyweb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwillothy%2Ftinyweb/lists"}