├── .dockerignore ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── ROADMAP.md ├── benches.Dockerfile ├── benches ├── Cargo.lock ├── Cargo.toml ├── README.md ├── axum │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── bench.bat ├── bench.sh └── feather │ ├── Cargo.toml │ └── src │ └── main.rs ├── crates ├── feather-runtime │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── http │ │ ├── mod.rs │ │ ├── request.rs │ │ └── response.rs │ │ ├── lib.rs │ │ ├── runtime │ │ ├── engine.rs │ │ └── mod.rs │ │ └── utils │ │ ├── error.rs │ │ ├── message.rs │ │ ├── mod.rs │ │ ├── queue.rs │ │ └── worker.rs └── feather │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── internals │ ├── app.rs │ ├── context.rs │ ├── error_stack.rs │ └── mod.rs │ ├── jwt.rs │ ├── lib.rs │ └── middleware │ ├── builtins.rs │ ├── common.rs │ └── mod.rs └── examples ├── api ├── Cargo.toml └── src │ └── main.rs ├── basic-app ├── Cargo.toml └── src │ └── main.rs ├── context ├── Cargo.toml └── src │ └── main.rs ├── counter ├── Cargo.toml └── src │ └── main.rs ├── error-pipeline ├── Cargo.toml └── src │ └── main.rs ├── jwt ├── Cargo.toml └── src │ └── main.rs ├── middleware ├── Cargo.toml └── src │ ├── main.rs │ └── middleware.rs └── serve ├── Cargo.toml ├── public └── index.html └── src └── main.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/Cargo.lock 3 | !benches/**/Cargo.lock -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: BersisSe 14 | thanks_dev: BersisSe 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | - A clear and concise description of what the bug is. 12 | - Is it related to `Feather-Runtime` or `Feather` 13 | 14 | **To Reproduce** 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Desktop (please complete the following information):** 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | - Device: [e.g. iPhone6] 34 | - OS: [e.g. iOS8.1] 35 | - Browser [e.g. stock browser, safari] 36 | - Version [e.g. 22] 37 | 38 | **Additional context** 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | **/Cargo.lock 3 | !benches/**/Cargo.lock 4 | .vscode 5 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | --- 6 | 7 | ## [0.4.4] - 2025-05-24 8 | 9 | ### Notes 10 | This is a Major Update not to Feather but rather Feather Runtime. 11 | This update brings more modularity to Feather lets take a look at the changes! 12 | 13 | ### Added 14 | **Feather Framework** 15 | - N/A 16 | **Feather Runtime** 17 | - Request has a new Method named `take_stream` it takes the underlying TcpStream Out of the Request use this Method wisely. 18 | 19 | ## Fixed 20 | **General** 21 | - The `send_bytes` method on the Response mangling the input spesificly this [issue by timwedde ](https://github.com/BersisSe/feather/issues/12) 22 | 23 | ## Changed 24 | **Feather Framework** 25 | - N/A 26 | **Feather Runtime** 27 | - Some of the internals has been renamed for clarity 28 | 29 | --- 30 | 31 | 32 | ## [0.4.3] - 2025-05-24 33 | 34 | ### Notes 35 | This update is a minor update to Feather. It includes some bug fixes & some quality of life improvements. 36 | 37 | ### Added 38 | **Feather Framework** 39 | - N/A 40 | **Feather Runtime** 41 | - New `send_file` method on the `Response` object to send files as a response. 42 | - New `path` method on the `Request` object to get the request path as percent encoded. 43 | ## Fixed 44 | **Feather Framework** 45 | - Fixed a bug where the routes were not percent encoded. 46 | **Feather Runtime** 47 | - N/A 48 | 49 | --- 50 | 51 | ## [0.4.1] - 2025-05-11 52 | 53 | ### Notes 54 | No Notable changes to the framework. Only the Readme file has been symlinked. 55 | 56 | --- 57 | 58 | ## [0.4.0] - 2025-05-08 59 | 60 | ### Notes 61 | This update is a major update. it solves the Error Handling Issue in Feather. With the new Error-Pipeline System. 62 | 63 | Now Every middleware Returns a `Outcome` 64 | This allows you to handle errors using the `?` operator. That will just pass the error to the next middleware. 65 | If there is no Error handler in the pipeline it will be passed to the default error handler. 66 | Default error handler will log the message and return a 500 Internal Server Error with the error message. 67 | 68 | ### Added 69 | **Feather Framework** 70 | - New Error-Pipeline System to handle errors in middlewares. 71 | - New `set_handler` method to set a custom error handler for the app. 72 | - New `next()!` macro for better readability and less boilerplate code. 73 | - New `Error-pipeline` Example to show how to use the new error handling system. 74 | **Feather Runtime** 75 | - N/A 76 | ## Fixed 77 | **Feather Framework** 78 | - `ServeStatic` middleware's Security problems and use excessive of `unwrap`&`expect` has been fixed. 79 | - The non-existing route now returns a 404 not found error instead of just freazing the client. 80 | ## Changed 81 | **Feather Framework** 82 | - Now every middleware returns a `Result`(We Call it `Outcome` for simplicty) instead of `MiddlewareResult`. 83 | - File Structure has been changed for better scalability. 84 | - Middleware example has been rewritten to match the latest changes. 85 | **Feather Runtime** 86 | - Response's `status` method's name is changed to `set_status` for better clarity. 87 | --- 88 | 89 | ## [0.3.2] - 2025-05-04 90 | 91 | ### Notes 92 | No Changes to the framework Only the Readme file has been rewritten. 93 | 94 | --- 95 | 96 | ## [0.3.1] - 2025-05-04 97 | 98 | ### Notes 99 | This Update includes some bugs fixes in the Runtime and some Quality of life additions 100 | ### Added 101 | **Feather Framework** 102 | - Context now has `get_mut_state` method to access mutable state without Mutexes 103 | - New Counter Example! 104 | **Feather Runtime** 105 | - Request Now has `query` method. 106 | ### Fixed 107 | **Feather Framework** 108 | - N/A 109 | **Feather Runtime** 110 | - Now Puts The Correct `connection` HTTP headers 111 | ## Changed 112 | **Feather Framework** 113 | - N/A 114 | **Feather Runtime** 115 | - Response's `to_string` method is renamed to `to_raw` for better clarity 116 | --- 117 | 118 | ## [0.3.0] - 2025-05-01 119 | ### Notes 120 | 121 | This update is a major update. it adds a Solid State management system on top of Feather called Context API. 122 | Every App now has a Context from that Context you can add State or Retrieve State this is especially usefull when using databases or file accesses. 123 | App Context is also reserved for future use for things like event system ,html rendering and more! 124 | 125 | ### Added 126 | 127 | **Feather Framework** 128 | 129 | - New Context Api to manage app state without extractors or macros 130 | - New context.rs example to show how context works with a database 131 | **Feather Runtime** 132 | 133 | - N/A 134 | 135 | ### Removed 136 | 137 | **Feather Framework** 138 | 139 | - _BREAKING CHANGE_: The old routes now require a `context` parameter. 140 | 141 | **Feather Runtime** 142 | 143 | - N/A 144 | 145 | ### Fixed 146 | 147 | **Feather Framework** 148 | 149 | - N/A 150 | 151 | **Feather Runtime** 152 | 153 | - Response's status method now returns a referance to the response so you can chain other methods like send_text etc 154 | 155 | ### Changed 156 | 157 | **Feather Framework** 158 | 159 | - Changed the file structure for better readablity. 160 | - Middlewares are no longer needs to implement `Clone`. 161 | **Feather Runtime** 162 | - N/A 163 | 164 | --- 165 | 166 | ## [0.2.1] - 2025-04-24 167 | 168 | ### Notes 169 | 170 | This update is a minor update to the Feather Framework and Feather Runtime. It includes new features and bug fixes. The JWT module allows you to create and verify JWT tokens when the `jwt` feature is enabled. The new `chain` macro allows chaining multiple middlewares for better organization and readability. 171 | 172 | ### Added 173 | 174 | **Feather Framework** 175 | 176 | - New JWT module to create and verify JWT tokens. 177 | - New JWT auth helper to protect routes with JWT. 178 | - New `generate_jwt` function. 179 | - New `chain` macro to chain middlewares together. 180 | 181 | **Feather Runtime** 182 | 183 | - N/A 184 | 185 | ### Removed 186 | 187 | **Feather Framework** 188 | 189 | - N/A 190 | 191 | **Feather Runtime** 192 | 193 | - N/A 194 | 195 | ### Fixed 196 | 197 | **Feather Framework** 198 | 199 | - N/A 200 | 201 | **Feather Runtime** 202 | 203 | - Fixed a bug where the `Response` object's status could not be changed. 204 | 205 | ### Changed 206 | 207 | **Feather Framework** 208 | 209 | - Middleware module has been split into multiple files for better organization. This might break some code that uses the old module path. 210 | 211 | **Feather Runtime** 212 | 213 | - N/A 214 | 215 | --- 216 | 217 | ## [0.2.0] - 2025-04-20 218 | 219 | ### Added 220 | 221 | **Feather Framework** 222 | 223 | - New JSON methods for the `Request` object to simplify retrieving the JSON body. 224 | - Doc comments for most methods and structs. 225 | 226 | **Feather Runtime** 227 | 228 | - Internals rewritten for better readability and maintainability. 229 | - Added `TaskPool` to manage concurrent tasks (essentially concurrent requests). 230 | - Added `MessageQueue` to manage requests efficiently. 231 | - Added a new connection management system. 232 | - Added proper error handling and logging. 233 | 234 | ### Removed 235 | 236 | **Feather Framework** 237 | 238 | - Removed the `AppConfig` struct. 239 | - Removed the `App` struct's `with_config` method. 240 | 241 | **Feather Runtime** 242 | 243 | - Removed the `rusty-pool` dependency. 244 | 245 | ### Fixed 246 | 247 | **Feather Framework** 248 | 249 | - Improved general performance. 250 | 251 | **Feather Runtime** 252 | 253 | - Fixed a bug where the server would not send a response if the client shut down the connection. 254 | - Fixed a bug where the server would not shut down properly. 255 | - Improved runtime performance by optimizing internals. See [details](feather-runtime/Performance.md). 256 | 257 | --- 258 | 259 | ## [0.1.2] - 2025-04-07 260 | 261 | ### Added 262 | 263 | - New `ServeStatic` middleware to serve static files from a directory. 264 | - New `Response` methods: `send`, `send_json`, and `send_html` for easier response handling. 265 | 266 | ### Changed 267 | 268 | - The `MiddlewareResult` enum is now included in the prelude file for easier access. 269 | 270 | --- 271 | 272 | ## [0.1.1] - 2025-04-04 273 | 274 | ### Added 275 | 276 | - Simplified API for `Response` and `Request` objects. 277 | - `App` struct now has a `with_config` method to create an app with a configuration. 278 | 279 | ### Changed 280 | 281 | - Internal code refactored for better readability and maintainability. 282 | - The `App` struct's `new` method no longer takes a configuration. 283 | 284 | --- 285 | 286 | ## [0.1.0] - 2025-03-21 287 | 288 | ### Added 289 | 290 | - Initial release of the framework. 291 | - Simple Express-style routing and middlewares. 292 | - Configurable thread pool for handling concurrent requests. 293 | 294 | --- 295 | 296 | ## [0.0.1] - 2025-03-15 297 | 298 | ### Changed 299 | 300 | - Migrated to `Feather-Runtime` from `Tiny-HTTP`. See [Feather Runtime README](feather-runtime/README.md) for details. 301 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" # 2024 3 | members = ["crates/*", "examples/*"] 4 | exclude = ["benches/axum", "benches/feather"] 5 | 6 | [workspace.dependencies] 7 | feather = { version = "~0.4",path = "./crates/feather", default-features = false } 8 | feather-runtime = { version = "~0.3", path = "./crates/feather-runtime", default-features = false } 9 | http = { version = "1", default-features = false, features = ["std"]} 10 | httparse = { version = "1", default-features = false } 11 | serde = { version = "1", default-features = false } 12 | serde_json = { version = "1", default-features = false, features = ["std"]} 13 | chrono = { version = "0.4.41", default-features = false, features = ["now"]} 14 | bytes = { version = "1", default-features = false } 15 | log = { version = "~0.4", default-features = false } 16 | serde_urlencoded = { version = "~0.7", default-features = false } 17 | thiserror = { version = "2", default-features = false } 18 | crossbeam = { version = "~0.8", default-features = false ,features = ["std"]} 19 | anymap = { version = "~0.12", default-features = false } 20 | jsonwebtoken = { version = "9", default-features = false } 21 | rusqlite = { version = "~0.35", default-features = false } 22 | urlencoding = "2.1.3" 23 | parking_lot = { version = "~0.12"} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Bersis Sevimli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🪶 Feather 2 | 3 | ## **Feather** is a lightweight, DX-first web framework for Rust — inspired by the simplicity of Express.js, but designed for Rust’s performance and safety. 4 | 5 | ## Why Feather? 6 | 7 | - **Middleware-First Architecture** 8 | Everything is a middleware even if they are not a middleware they produce a middleware in the end. 9 | 10 | - **Easy State Management Using Context** 11 | Recently implemented the Context API that makes it very easy to manage state without the use of Extractors/Macros. 12 | 13 | - **Feel of Async Without Async** 14 | Feather is Multithreaded by default running on **Feather-Runtime**. 15 | 16 | - **Great Tooling Out Of the Box** 17 | With the use of the [Feather-CLI](https://github.com/BersisSe/feather-cli/tree/main) creating API's and Web Servers becomes a _Breeze_. 18 | 19 | ## How it works behind the scenes: 20 | Every Request given a thread from the Server's threadpool and that thread is responsible for returning the a response to that request. 21 | So you can Run long running task's on another thread in the middlewares but the response can only be returned from the middleware the request is accepted on. 22 | If you want to go deeper take look at [Feather-Runtime](./crates/feather-runtime) 23 | 24 | --- 25 | 26 | ## Getting Started 27 | 28 | Add Feather to your `Cargo.toml`: 29 | 30 | ```toml 31 | [dependencies] 32 | feather = "~0.5" 33 | ``` 34 | 35 | --- 36 | 37 | ## Quick Example 38 | 39 | ```rust 40 | use feather::middleware::builtins; 41 | use feather::{App, AppContext, next}; 42 | use feather::{Request, Response}; 43 | fn main() { 44 | let mut app = App::new(); 45 | app.get("/", |_request: &mut Request, response: &mut Response, _ctx: &mut AppContext| { 46 | response.send_text("Hello, world!"); 47 | next!() 48 | }); 49 | 50 | app.use_middleware(builtins::Logger); 51 | app.listen("127.0.0.1:5050"); 52 | } 53 | ``` 54 | 55 | That’s all — no async. 56 | 57 | --- 58 | 59 | ## Middleware in Feather 60 | 61 | Middleware is intented to be the heart of Feather. You may write it as a closure, a struct, or chain them together: 62 | 63 | ```rust,no_run 64 | use feather::{App, AppContext, Request, Response,next,Outcome}; 65 | use feather::middleware::builtins; 66 | use feather::middleware::{Middleware, MiddlewareResult}; 67 | 68 | // Implementors of the Middleware trait are middleware that can be used in a Feather app. 69 | struct Custom; 70 | 71 | impl Middleware for Custom { 72 | fn handle(&self, request: &mut Request, _response: &mut Response, _ctx: &mut AppContext) -> Outcome { 73 | println!("Now running some custom middleware (struct Custom)!"); 74 | println!("And there's a request with path: {:?}", request.uri); 75 | next!() 76 | } 77 | } 78 | 79 | fn main() { 80 | let mut app = App::new(); 81 | app.use_middleware(builtins::Logger); 82 | app.use_middleware(Custom); 83 | app.use_middleware(|_req: &mut Request, _res: &mut Response, _ctx: &mut AppContext| { 84 | println!("Now running some custom middleware (closure)!"); 85 | next!() 86 | }); 87 | 88 | app.get("/",|_req: &mut Request, res: &mut Response, _ctx: &mut AppContext| { 89 | res.send_text("Hello, world!"); 90 | next!() 91 | }); 92 | 93 | app.listen("127.0.0.1:5050"); 94 | } 95 | ``` 96 | --- 97 | 98 | ## State Management using the Context API 99 | 100 | Feather's new Context API allows you to manage application-wide state without extractors or macros. 101 | 102 | As an example: 103 | 104 | ```rust,no_run 105 | use feather::{next, App, AppContext, Request, Response}; 106 | // Create a couter struct to hold the state 107 | #[derive(Debug)] 108 | struct Counter { 109 | pub count: i32, 110 | } 111 | fn main() { 112 | let mut app = App::new(); 113 | let counter = Counter { count: 0 }; 114 | app.context().set_state(counter); 115 | 116 | app.get("/",move |_req: &mut Request, res: &mut Response, ctx: &mut AppContext| { 117 | let counter: &mut Counter = ctx.get_mut_state::().unwrap(); 118 | counter.count += 1; 119 | res.send_text(format!("Counted! {}", counter.count)); 120 | next!() 121 | }); 122 | // Lastly add a route to get the current count 123 | app.get("/count",move |_req: &mut Request, res: &mut Response, ctx: &mut AppContext| { 124 | let counter = ctx.get_state::().unwrap(); 125 | res.send_text(counter.count.to_string()); 126 | next!() 127 | }); 128 | app.listen("127.0.0.1:5050"); 129 | } 130 | 131 | ``` 132 | 133 | Context is especially useful when needing to access databases and files. 134 | 135 | ## Built-in JWT Authentication 136 | 137 | Feather has a native JWT module activated using a cargo feature `jwt`: 138 | 139 | ```toml 140 | [dependencies] 141 | feather = { version = "0.3.1", features = ["jwt"] } 142 | ``` 143 | 144 | ```rust,no_run 145 | use feather::jwt::{generate_jwt, with_jwt_auth}; 146 | use feather::{App, AppContext,next}; 147 | 148 | fn main() { 149 | let mut app = App::new(); 150 | app.get("/auth",with_jwt_auth("secretcode", |_req, res,_ctx, claim| { 151 | println!("Claim: {:?}", claim); 152 | res.send_text("Hello, JWT!"); 153 | next!() 154 | }), 155 | ); 156 | // Check the JWT Example for a more complete version! 157 | app.listen("127.0.0.1:8080") 158 | } 159 | ``` 160 | 161 | --- 162 | 163 | ## Goals 164 | 165 | - Being the simplest Rust web framework to get started with 166 | - Being modular and easy to extend 167 | - Focusing on DX without sacrificing Rust's safety and performance 168 | 169 | --- 170 | 171 | ## Contributing 172 | 173 | PRs are welcome! 174 | If you have ideas or bugs, please [open an issue]([https://github.com/BersisSe/feather/issues) or submit a pull request. 175 | 176 | ```bash 177 | # Getting started with dev 178 | git clone https://github.com/BersisSe/feather.git 179 | cd feather 180 | cargo run --example app 181 | ``` 182 | 183 | --- 184 | 185 | ## License 186 | 187 | Feather is MIT licensed. See [LICENSE](./LICENSE). 188 | 189 | --- 190 | 191 | ## Acknowledgments 192 | 193 | Feather is inspired by [Express.js](https://expressjs.com) and exists to bring that same productivity to Rust. 194 | 195 | --- 196 | 197 | ## Spread the Word 198 | 199 | If you like Feather: 200 | 201 | - ⭐ Star it on [GitHub](https://github.com/BersisSe/feather), 202 | - If you can Maybe buy me a Coffee [Here](https://buymeacoffee.com/bersisse)! 203 | - Share it on Reddit, HN, or Discord 204 | - Build something and show up! 205 | 206 | --- 207 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # Feather Framework Roadmap 2 | 3 | This document outlines the planned features and improvements for the Feather framework. 4 | 5 | ### Planned Features 6 | - ✅ Implement JWT authentication 7 | - ✅ Improve error handling and logging 8 | - ✅ Improve Performance: Optimize the framework for better performance and lower latency 9 | - ✅ Implement advanced routing features (e.g., route parameters, query parameters) 10 | - Add support for WebSockets 11 | - Implement Cookie Support 12 | - ✅ Add App wide State Management System 13 | - ✅ Add Error Handling Middleware 14 | - Add Templating for Server-Side Rendering 15 | - Add a Event/Hook System that users can plug into(Kinda Like Svelte and Vue.js) 16 | - Add Extansiblity Hooks to allow users to extend the framework 17 | -------------------------------------------------------------------------------- /benches.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest AS build 2 | ARG framework 3 | WORKDIR /app 4 | COPY . . 5 | RUN cd benches && \ 6 | rustup target add x86_64-unknown-linux-musl && \ 7 | cargo build --target x86_64-unknown-linux-musl --locked --release --package ${framework}-bench 8 | 9 | FROM scratch 10 | ARG framework 11 | COPY --from=build /app/benches/target/x86_64-unknown-linux-musl/release/${framework}-bench /app/server 12 | EXPOSE 3000 13 | CMD ["/app/server"] -------------------------------------------------------------------------------- /benches/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "anymap" 22 | version = "0.12.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "33954243bd79057c2de7338850b85983a44588021f8a5fee574a8888c6de4344" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.4.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 31 | 32 | [[package]] 33 | name = "axum" 34 | version = "0.8.4" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" 37 | dependencies = [ 38 | "axum-core", 39 | "bytes", 40 | "futures-util", 41 | "http", 42 | "http-body", 43 | "http-body-util", 44 | "hyper", 45 | "hyper-util", 46 | "itoa", 47 | "matchit", 48 | "memchr", 49 | "mime", 50 | "percent-encoding", 51 | "pin-project-lite", 52 | "rustversion", 53 | "serde", 54 | "sync_wrapper", 55 | "tokio", 56 | "tower", 57 | "tower-layer", 58 | "tower-service", 59 | ] 60 | 61 | [[package]] 62 | name = "axum-bench" 63 | version = "0.0.0" 64 | dependencies = [ 65 | "axum", 66 | "tokio", 67 | ] 68 | 69 | [[package]] 70 | name = "axum-core" 71 | version = "0.5.2" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" 74 | dependencies = [ 75 | "bytes", 76 | "futures-core", 77 | "http", 78 | "http-body", 79 | "http-body-util", 80 | "mime", 81 | "pin-project-lite", 82 | "rustversion", 83 | "sync_wrapper", 84 | "tower-layer", 85 | "tower-service", 86 | ] 87 | 88 | [[package]] 89 | name = "backtrace" 90 | version = "0.3.74" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 93 | dependencies = [ 94 | "addr2line", 95 | "cfg-if", 96 | "libc", 97 | "miniz_oxide", 98 | "object", 99 | "rustc-demangle", 100 | "windows-targets", 101 | ] 102 | 103 | [[package]] 104 | name = "bytes" 105 | version = "1.10.1" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 108 | 109 | [[package]] 110 | name = "cfg-if" 111 | version = "1.0.0" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 114 | 115 | [[package]] 116 | name = "chrono" 117 | version = "0.4.41" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 120 | dependencies = [ 121 | "num-traits", 122 | ] 123 | 124 | [[package]] 125 | name = "crossbeam" 126 | version = "0.8.4" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 129 | dependencies = [ 130 | "crossbeam-channel", 131 | "crossbeam-deque", 132 | "crossbeam-epoch", 133 | "crossbeam-queue", 134 | "crossbeam-utils", 135 | ] 136 | 137 | [[package]] 138 | name = "crossbeam-channel" 139 | version = "0.5.15" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 142 | dependencies = [ 143 | "crossbeam-utils", 144 | ] 145 | 146 | [[package]] 147 | name = "crossbeam-deque" 148 | version = "0.8.6" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" 151 | dependencies = [ 152 | "crossbeam-epoch", 153 | "crossbeam-utils", 154 | ] 155 | 156 | [[package]] 157 | name = "crossbeam-epoch" 158 | version = "0.9.18" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" 161 | dependencies = [ 162 | "crossbeam-utils", 163 | ] 164 | 165 | [[package]] 166 | name = "crossbeam-queue" 167 | version = "0.3.12" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" 170 | dependencies = [ 171 | "crossbeam-utils", 172 | ] 173 | 174 | [[package]] 175 | name = "crossbeam-utils" 176 | version = "0.8.21" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 179 | 180 | [[package]] 181 | name = "feather" 182 | version = "0.4.1" 183 | dependencies = [ 184 | "anymap", 185 | "chrono", 186 | "feather-runtime", 187 | ] 188 | 189 | [[package]] 190 | name = "feather-bench" 191 | version = "0.0.0" 192 | dependencies = [ 193 | "feather", 194 | ] 195 | 196 | [[package]] 197 | name = "feather-runtime" 198 | version = "0.2.1" 199 | dependencies = [ 200 | "bytes", 201 | "chrono", 202 | "crossbeam", 203 | "http", 204 | "httparse", 205 | "log", 206 | "serde", 207 | "serde_json", 208 | "serde_urlencoded", 209 | "thiserror", 210 | ] 211 | 212 | [[package]] 213 | name = "fnv" 214 | version = "1.0.7" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 217 | 218 | [[package]] 219 | name = "form_urlencoded" 220 | version = "1.2.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 223 | dependencies = [ 224 | "percent-encoding", 225 | ] 226 | 227 | [[package]] 228 | name = "futures-channel" 229 | version = "0.3.31" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 232 | dependencies = [ 233 | "futures-core", 234 | ] 235 | 236 | [[package]] 237 | name = "futures-core" 238 | version = "0.3.31" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 241 | 242 | [[package]] 243 | name = "futures-task" 244 | version = "0.3.31" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 247 | 248 | [[package]] 249 | name = "futures-util" 250 | version = "0.3.31" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 253 | dependencies = [ 254 | "futures-core", 255 | "futures-task", 256 | "pin-project-lite", 257 | "pin-utils", 258 | ] 259 | 260 | [[package]] 261 | name = "gimli" 262 | version = "0.31.1" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 265 | 266 | [[package]] 267 | name = "http" 268 | version = "1.3.1" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" 271 | dependencies = [ 272 | "bytes", 273 | "fnv", 274 | "itoa", 275 | ] 276 | 277 | [[package]] 278 | name = "http-body" 279 | version = "1.0.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 282 | dependencies = [ 283 | "bytes", 284 | "http", 285 | ] 286 | 287 | [[package]] 288 | name = "http-body-util" 289 | version = "0.1.3" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" 292 | dependencies = [ 293 | "bytes", 294 | "futures-core", 295 | "http", 296 | "http-body", 297 | "pin-project-lite", 298 | ] 299 | 300 | [[package]] 301 | name = "httparse" 302 | version = "1.10.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" 305 | 306 | [[package]] 307 | name = "httpdate" 308 | version = "1.0.3" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" 311 | 312 | [[package]] 313 | name = "hyper" 314 | version = "1.6.0" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 317 | dependencies = [ 318 | "bytes", 319 | "futures-channel", 320 | "futures-util", 321 | "http", 322 | "http-body", 323 | "httparse", 324 | "httpdate", 325 | "itoa", 326 | "pin-project-lite", 327 | "smallvec", 328 | "tokio", 329 | ] 330 | 331 | [[package]] 332 | name = "hyper-util" 333 | version = "0.1.11" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" 336 | dependencies = [ 337 | "bytes", 338 | "futures-util", 339 | "http", 340 | "http-body", 341 | "hyper", 342 | "pin-project-lite", 343 | "tokio", 344 | "tower-service", 345 | ] 346 | 347 | [[package]] 348 | name = "itoa" 349 | version = "1.0.15" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 352 | 353 | [[package]] 354 | name = "libc" 355 | version = "0.2.172" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 358 | 359 | [[package]] 360 | name = "log" 361 | version = "0.4.27" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 364 | 365 | [[package]] 366 | name = "matchit" 367 | version = "0.8.4" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" 370 | 371 | [[package]] 372 | name = "memchr" 373 | version = "2.7.4" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 376 | 377 | [[package]] 378 | name = "mime" 379 | version = "0.3.17" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 382 | 383 | [[package]] 384 | name = "miniz_oxide" 385 | version = "0.8.8" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 388 | dependencies = [ 389 | "adler2", 390 | ] 391 | 392 | [[package]] 393 | name = "mio" 394 | version = "1.0.3" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 397 | dependencies = [ 398 | "libc", 399 | "wasi", 400 | "windows-sys", 401 | ] 402 | 403 | [[package]] 404 | name = "num-traits" 405 | version = "0.2.19" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 408 | dependencies = [ 409 | "autocfg", 410 | ] 411 | 412 | [[package]] 413 | name = "object" 414 | version = "0.36.7" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 417 | dependencies = [ 418 | "memchr", 419 | ] 420 | 421 | [[package]] 422 | name = "percent-encoding" 423 | version = "2.3.1" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 426 | 427 | [[package]] 428 | name = "pin-project-lite" 429 | version = "0.2.16" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 432 | 433 | [[package]] 434 | name = "pin-utils" 435 | version = "0.1.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 438 | 439 | [[package]] 440 | name = "proc-macro2" 441 | version = "1.0.95" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 444 | dependencies = [ 445 | "unicode-ident", 446 | ] 447 | 448 | [[package]] 449 | name = "quote" 450 | version = "1.0.40" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 453 | dependencies = [ 454 | "proc-macro2", 455 | ] 456 | 457 | [[package]] 458 | name = "rustc-demangle" 459 | version = "0.1.24" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 462 | 463 | [[package]] 464 | name = "rustversion" 465 | version = "1.0.20" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" 468 | 469 | [[package]] 470 | name = "ryu" 471 | version = "1.0.20" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 474 | 475 | [[package]] 476 | name = "serde" 477 | version = "1.0.219" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 480 | dependencies = [ 481 | "serde_derive", 482 | ] 483 | 484 | [[package]] 485 | name = "serde_derive" 486 | version = "1.0.219" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 489 | dependencies = [ 490 | "proc-macro2", 491 | "quote", 492 | "syn", 493 | ] 494 | 495 | [[package]] 496 | name = "serde_json" 497 | version = "1.0.140" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 500 | dependencies = [ 501 | "itoa", 502 | "memchr", 503 | "ryu", 504 | "serde", 505 | ] 506 | 507 | [[package]] 508 | name = "serde_urlencoded" 509 | version = "0.7.1" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 512 | dependencies = [ 513 | "form_urlencoded", 514 | "itoa", 515 | "ryu", 516 | "serde", 517 | ] 518 | 519 | [[package]] 520 | name = "smallvec" 521 | version = "1.15.0" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 524 | 525 | [[package]] 526 | name = "socket2" 527 | version = "0.5.9" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" 530 | dependencies = [ 531 | "libc", 532 | "windows-sys", 533 | ] 534 | 535 | [[package]] 536 | name = "syn" 537 | version = "2.0.101" 538 | source = "registry+https://github.com/rust-lang/crates.io-index" 539 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 540 | dependencies = [ 541 | "proc-macro2", 542 | "quote", 543 | "unicode-ident", 544 | ] 545 | 546 | [[package]] 547 | name = "sync_wrapper" 548 | version = "1.0.2" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 551 | 552 | [[package]] 553 | name = "thiserror" 554 | version = "2.0.12" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 557 | dependencies = [ 558 | "thiserror-impl", 559 | ] 560 | 561 | [[package]] 562 | name = "thiserror-impl" 563 | version = "2.0.12" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 566 | dependencies = [ 567 | "proc-macro2", 568 | "quote", 569 | "syn", 570 | ] 571 | 572 | [[package]] 573 | name = "tokio" 574 | version = "1.44.2" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" 577 | dependencies = [ 578 | "backtrace", 579 | "libc", 580 | "mio", 581 | "pin-project-lite", 582 | "socket2", 583 | "tokio-macros", 584 | "windows-sys", 585 | ] 586 | 587 | [[package]] 588 | name = "tokio-macros" 589 | version = "2.5.0" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 592 | dependencies = [ 593 | "proc-macro2", 594 | "quote", 595 | "syn", 596 | ] 597 | 598 | [[package]] 599 | name = "tower" 600 | version = "0.5.2" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 603 | dependencies = [ 604 | "futures-core", 605 | "futures-util", 606 | "pin-project-lite", 607 | "sync_wrapper", 608 | "tokio", 609 | "tower-layer", 610 | "tower-service", 611 | ] 612 | 613 | [[package]] 614 | name = "tower-layer" 615 | version = "0.3.3" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 618 | 619 | [[package]] 620 | name = "tower-service" 621 | version = "0.3.3" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 624 | 625 | [[package]] 626 | name = "unicode-ident" 627 | version = "1.0.18" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 630 | 631 | [[package]] 632 | name = "wasi" 633 | version = "0.11.0+wasi-snapshot-preview1" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 636 | 637 | [[package]] 638 | name = "windows-sys" 639 | version = "0.52.0" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 642 | dependencies = [ 643 | "windows-targets", 644 | ] 645 | 646 | [[package]] 647 | name = "windows-targets" 648 | version = "0.52.6" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 651 | dependencies = [ 652 | "windows_aarch64_gnullvm", 653 | "windows_aarch64_msvc", 654 | "windows_i686_gnu", 655 | "windows_i686_gnullvm", 656 | "windows_i686_msvc", 657 | "windows_x86_64_gnu", 658 | "windows_x86_64_gnullvm", 659 | "windows_x86_64_msvc", 660 | ] 661 | 662 | [[package]] 663 | name = "windows_aarch64_gnullvm" 664 | version = "0.52.6" 665 | source = "registry+https://github.com/rust-lang/crates.io-index" 666 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 667 | 668 | [[package]] 669 | name = "windows_aarch64_msvc" 670 | version = "0.52.6" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 673 | 674 | [[package]] 675 | name = "windows_i686_gnu" 676 | version = "0.52.6" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 679 | 680 | [[package]] 681 | name = "windows_i686_gnullvm" 682 | version = "0.52.6" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 685 | 686 | [[package]] 687 | name = "windows_i686_msvc" 688 | version = "0.52.6" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 691 | 692 | [[package]] 693 | name = "windows_x86_64_gnu" 694 | version = "0.52.6" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 697 | 698 | [[package]] 699 | name = "windows_x86_64_gnullvm" 700 | version = "0.52.6" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 703 | 704 | [[package]] 705 | name = "windows_x86_64_msvc" 706 | version = "0.52.6" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 709 | -------------------------------------------------------------------------------- /benches/Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "3" 3 | members = ["axum", "feather"] 4 | 5 | [profile.release] 6 | opt-level = 3 7 | debug = false 8 | debug-assertions = false 9 | lto = true 10 | panic = "abort" 11 | incremental = false 12 | codegen-units = 1 13 | rpath = false 14 | strip = false 15 | 16 | [workspace.dependencies] 17 | axum = { version = "0.8.4", default-features = false, features = ["tokio", "http1"] } 18 | tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] } 19 | feather = { path = "../crates/feather", default-features = false } 20 | -------------------------------------------------------------------------------- /benches/README.md: -------------------------------------------------------------------------------- 1 | There's script files available to easily benchmark, but they have set parameters. Those are: 2 | - 10 threads 3 | - 400 connections 4 | - 10 seconds 5 | 6 | If you want to customize them, copy the commands and modify the last one. -------------------------------------------------------------------------------- /benches/axum/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-bench" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | axum = { workspace = true } 9 | tokio = { workspace = true } 10 | -------------------------------------------------------------------------------- /benches/axum/src/main.rs: -------------------------------------------------------------------------------- 1 | use axum::extract::Path; 2 | use axum::routing::{get, post}; 3 | use axum::Router; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let router: Router = Router::new() 8 | .route("/", get(|| async {})) 9 | .route("/user", post(|| async {})) 10 | .route( 11 | "/user/{id}", 12 | get(|Path(id): Path| async move { id }), 13 | ); 14 | 15 | let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); 16 | axum::serve(listener, router.into_make_service()) 17 | .await 18 | .unwrap(); 19 | } -------------------------------------------------------------------------------- /benches/bench.bat: -------------------------------------------------------------------------------- 1 | cd .. 2 | docker build -f benches.Dockerfile -t %1-bench --build-arg framework=%1 . 3 | docker rm --force %1-bench 4 | docker run --rm -d -p 3000:3000 --name %1-bench %1-bench 5 | docker run --rm ghcr.io/william-yeh/wrk -t10 -c400 -d10s http://host.docker.internal:3000/ -------------------------------------------------------------------------------- /benches/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd .. 4 | docker build -f benches.Dockerfile -t "$1"-bench --build-arg framework="$1" . 5 | docker rm --force "$1"-bench 6 | docker run --rm -d -p 3000:3000 --name "$1"-bench "$1"-bench 7 | docker run --rm ghcr.io/william-yeh/wrk -t10 -c400 -d10s http://host.docker.internal:3000/ -------------------------------------------------------------------------------- /benches/feather/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "feather-bench" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true } 9 | -------------------------------------------------------------------------------- /benches/feather/src/main.rs: -------------------------------------------------------------------------------- 1 | use feather::{App, AppContext, MiddlewareResult, Request, Response,}; 2 | 3 | fn main() { 4 | let mut app = App::new(); 5 | 6 | app.get("/", |_req: &mut Request, res: &mut Response, _ctx: &mut AppContext| { 7 | res.send_bytes([]); 8 | MiddlewareResult::NextRoute 9 | }); 10 | 11 | app.post("/user", |_req: &mut Request, _res: &mut Response, _ctx: &mut AppContext| { 12 | MiddlewareResult::NextRoute 13 | }); 14 | 15 | app.get("/user", |req: &mut Request, res: &mut Response, _ctx: &mut AppContext| { 16 | if let Some(query) = req.uri.query() { 17 | res.send_bytes(query); 18 | } else { 19 | res.send_bytes([]); 20 | } 21 | 22 | MiddlewareResult::NextRoute 23 | }); 24 | 25 | app.listen("0.0.0.0:3000"); 26 | } 27 | -------------------------------------------------------------------------------- /crates/feather-runtime/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "feather-runtime" 3 | version = "0.3.0" 4 | edition = "2024" 5 | authors = ["Bersis Sevimli"] 6 | description = "Web Server Runtime for Feather" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | bytes = { workspace = true } 11 | chrono = { workspace = true } 12 | crossbeam = { workspace = true } 13 | http = { workspace = true } 14 | httparse = { workspace = true } 15 | log = { workspace = true } 16 | serde = { workspace = true } 17 | serde_json = { workspace = true } 18 | serde_urlencoded = { workspace = true } 19 | thiserror = { workspace = true } 20 | urlencoding = {workspace = true} 21 | parking_lot ={ workspace = true} 22 | [dev-dependencies] 23 | simple_logger = "5.0.0" 24 | -------------------------------------------------------------------------------- /crates/feather-runtime/README.md: -------------------------------------------------------------------------------- 1 | # Feather Runtime 2 | 3 | **Feather Runtime** is a lightweight, multithreaded HTTP server engine for [Feather](https://github.com/BersisSe/feather#). It provides enhanced control over low-level server operations and replaces `tiny-http`, which is no longer maintained. 4 | 5 | ## Features 6 | 7 | - 🚀 **Multithreaded Request Handling** – Efficiently handles multiple connections using a thread pool. 8 | - 🔄 **Graceful Shutdown** – Cleanly shuts down on signal 9 | - 🌍 **Dynamic HTTP Responses** – Easily generate responses based on incoming requests. 10 | - ⚡ **Buffered I/O** – Optimized performance with `BufReader` and `BufWriter`. 11 | 12 | 13 | ## Why Feather Runtime? 14 | 15 | I built **Feather Runtime** because I wanted greater control over the low-level aspects of the framework. `tiny-http`, the library Feather was originally based on, is no longer maintained, making it necessary to develop a more flexible and future-proof solution. 16 | 17 | Feather Runtime ensures a modern, efficient, and reliable HTTP server experience tailored for Feather. 18 | 19 | ### For Contributors 20 | 21 | If you're contributing to Feather but don't want to mess with low-level server internals, you can mostly ignore this subcrate. Feather Runtime is designed to handle the core HTTP processing while Feather itself provides higher-level abstractions. If you have a feature request or a problem open a [Issue](https://github.com/BersisSe/feather/issues) for it 22 | 23 | --- 24 | 25 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/http/mod.rs: -------------------------------------------------------------------------------- 1 | mod request; 2 | mod response; 3 | use std::ops::Deref; 4 | 5 | pub use request::Request; 6 | pub use response::Response; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum ConnectionState { 10 | KeepAlive, 11 | Close, 12 | } 13 | 14 | impl ConnectionState { 15 | pub fn parse(string: &str) -> Option { 16 | let string = string.to_lowercase(); 17 | match string.as_str() { 18 | "close" => Some(ConnectionState::Close), 19 | "keep-alive" => Some(ConnectionState::KeepAlive), 20 | _ => None, 21 | } 22 | } 23 | } 24 | 25 | impl Deref for ConnectionState { 26 | type Target = str; 27 | 28 | fn deref(&self) -> &Self::Target { 29 | match self { 30 | Self::KeepAlive => return "keep-alive", 31 | Self::Close => return "close", 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/http/request.rs: -------------------------------------------------------------------------------- 1 | use super::ConnectionState; 2 | use crate::utils::error::Error; 3 | use bytes::Bytes; 4 | use http::{Extensions, HeaderMap, Method, Uri, Version}; 5 | use std::str::FromStr; 6 | use std::{borrow::Cow, collections::HashMap, fmt, net::TcpStream}; 7 | use urlencoding::decode; 8 | 9 | /// Contains a incoming Http Request 10 | #[derive(Debug)] 11 | pub struct Request { 12 | stream: Option, 13 | /// The HTTP method of the request.
14 | /// For example, GET, POST, PUT, DELETE, etc. 15 | pub method: Method, 16 | /// The URI of the request. 17 | pub uri: Uri, 18 | /// The HTTP version of the request. 19 | pub version: Version, 20 | /// The headers of the request. 21 | pub headers: HeaderMap, 22 | /// The body of the request. 23 | pub body: Bytes, 24 | /// The extensions of the request. 25 | pub extensions: Extensions, 26 | // Connection State(Keep-Alive OR Close) of the Request 27 | pub(crate) connection: Option, 28 | } 29 | impl Clone for Request { 30 | fn clone(&self) -> Self { 31 | if let Some(tcp) = &self.stream{ 32 | Self { 33 | stream: Some(tcp.try_clone().unwrap()), 34 | method: self.method.clone(), 35 | uri: self.uri.clone(), 36 | version: self.version.clone(), 37 | headers: self.headers.clone(), 38 | body: self.body.clone(), 39 | extensions: self.extensions.clone(), 40 | connection: self.connection.clone(), 41 | } 42 | 43 | } 44 | else { 45 | Self { 46 | stream: None, 47 | method: self.method.clone(), 48 | uri: self.uri.clone(), 49 | version: self.version.clone(), 50 | headers: self.headers.clone(), 51 | body: self.body.clone(), 52 | extensions: self.extensions.clone(), 53 | connection: self.connection.clone(), 54 | } 55 | } 56 | } 57 | } 58 | 59 | impl Request { 60 | /// Parses a Request from raw bytes if parsing fails returns a error 61 | /// #### Puts a None to as stream of the request! 62 | pub fn parse(raw: &[u8]) -> Result { 63 | let mut headers = [httparse::EMPTY_HEADER; 16]; 64 | let mut request = httparse::Request::new(&mut headers); 65 | let mut connection = None; 66 | request 67 | .parse(raw) 68 | .map_err(|e| Error::ParseError(format!("Failed to parse request: {}", e)))?; 69 | let method = match Method::from_str(request.method.unwrap_or("nil")) { 70 | Ok(m) => m, 71 | Err(e) => return Err(Error::ParseError(format!("Failed to parse method: {}", e))), 72 | }; 73 | let uri: Uri = request 74 | .path 75 | .ok_or_else(|| Error::ParseError("Failed to parse URI".to_string()))? 76 | .parse() 77 | .map_err(|e| Error::ParseError(format!("Failed to parse URI: {}", e)))?; 78 | 79 | let version = match request.version { 80 | Some(0) => Version::HTTP_10, 81 | Some(1) => Version::HTTP_11, 82 | _ => Version::HTTP_11, 83 | }; 84 | let mut header_map = HeaderMap::new(); 85 | for header in request.headers.iter() { 86 | let name = http::header::HeaderName::from_bytes(header.name.as_bytes()) 87 | .map_err(|e| Error::ParseError(format!("Failed to parse header name: {}", e)))?; 88 | let value = http::header::HeaderValue::from_bytes(header.value) 89 | .map_err(|e| Error::ParseError(format!("Failed to parse header value: {}", e)))?; 90 | 91 | if name.as_str().eq_ignore_ascii_case("connection") { 92 | connection = ConnectionState::parse(value.to_str().unwrap_or("")); 93 | } 94 | 95 | header_map.insert(name, value); 96 | } 97 | let body_start = raw 98 | .windows(4) 99 | .position(|w| w == b"\r\n\r\n") 100 | .map(|pos| pos + 4) 101 | .unwrap_or(raw.len()); 102 | let body = Bytes::copy_from_slice(&raw[body_start..]); 103 | let extensions = Extensions::new(); 104 | Ok(Request { 105 | stream: None, 106 | method, 107 | uri, 108 | version, 109 | headers: header_map, 110 | body, 111 | extensions, 112 | connection, 113 | }) 114 | } 115 | 116 | pub(crate) fn set_stream(&mut self, stream: TcpStream) { 117 | self.stream = Some(stream); 118 | } 119 | /// Takes the stream of the request 120 | /// This medhod takes ownership of the Request! 121 | /// And also gives you to responsiblity give answer to the socket connection 122 | /// This method is only intented to used by Power Users use **it at your own risk.** 123 | /// ## Usage: 124 | /// Takes the stream out but you have to clone the request to do so. 125 | /// ```rust,ignore 126 | /// if let Some(stream) = request.clone().take_stream(){ 127 | /// 128 | /// } 129 | /// ``` 130 | /// The Reason of this is in the middlewares don't allow you to have ownership of the request and response objects so to get ownership you have to clone it 131 | pub fn take_stream(mut self) -> Option { 132 | self.stream.take() 133 | } 134 | /// Returns true if the Request has a Stream false if stream is taken 135 | pub fn has_stream(&self) -> bool { 136 | self.stream.is_some() 137 | } 138 | 139 | /// Parses the body of the request as Serde JSON Value. Returns an error if the body is not valid JSON. 140 | /// This method is useful for parsing JSON payloads in requests. 141 | pub fn json(&self) -> Result { 142 | serde_json::from_slice(&self.body) 143 | .map_err(|e| Error::ParseError(format!("Failed to parse JSON body: {}", e))) 144 | } 145 | /// Returns a Hashmap of the query parameters of the Request. 146 | /// Returns a Error if parsing fails 147 | pub fn query(&self) -> Result, Error> { 148 | if let Some(query) = self.uri.query() { 149 | serde_urlencoded::from_str(query) 150 | .map_err(|e| Error::ParseError(format!("Failed to Parse Query parameters {}", e))) 151 | } else { 152 | Ok(HashMap::new()) 153 | } 154 | } 155 | /// Returns the path of the Request 156 | pub fn path(&self) -> Cow<'_, str> { 157 | decode(self.uri.path()).unwrap() 158 | } 159 | } 160 | 161 | 162 | 163 | impl fmt::Display for Request { 164 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 165 | write!( 166 | f, 167 | "{} for {}: Body Data: {} ", 168 | self.method, 169 | self.uri.path(), 170 | String::from_utf8_lossy(&self.body) 171 | ) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/http/response.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; 3 | use serde::Serialize; 4 | use std::{fmt::Display, fs::File, io::Read, str::FromStr}; 5 | 6 | #[derive(Debug, Clone, Default)] 7 | pub struct Response { 8 | /// The HTTP status code of the response. 9 | /// This is a 3-digit integer that indicates the result of the request. 10 | pub status: StatusCode, 11 | /// The headers of the HTTP response. 12 | /// Headers are key-value pairs that provide additional information about the response. 13 | pub headers: HeaderMap, 14 | /// The body of the HTTP response. 15 | /// This is the content that is sent back to the client. 16 | /// The body is represented as a `Bytes` object for efficient handling of binary data. 17 | pub body: Option, 18 | /// The HTTP version of the response. 19 | pub version: http::Version, 20 | } 21 | 22 | impl Response { 23 | /// Sets the StatusCode of the response and Returns a Muteable Reference to the Response so you can things like 24 | /// ```rust,ignore 25 | /// res.status(200).send_text("eyo"); 26 | /// ``` 27 | /// The StatusCode is a 3-digit integer that indicates the result of the request. 28 | pub fn set_status(&mut self, status: u16) -> &mut Response { 29 | self.status = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); 30 | self 31 | } 32 | /// Adds a header to the response. 33 | /// The header is a key-value pair that provides additional information about the response. 34 | pub fn add_header(&mut self, key: &str, value: &str) -> Option<()> { 35 | if let Ok(val) = HeaderValue::from_str(value) { 36 | if let Ok(key) = HeaderName::from_str(key) { 37 | self.headers.insert(key, val); 38 | } 39 | return None; 40 | } 41 | None 42 | } 43 | /// Converts the `HttpResponse` into a raw HTTP response as Bytes. 44 | pub fn to_raw(&self) -> Bytes { 45 | let mut response = format!( 46 | "HTTP/1.1 {} {}\r\n", 47 | self.status.as_u16(), 48 | self.status.canonical_reason().unwrap_or("Unknown") 49 | ) 50 | .into_bytes(); 51 | 52 | for (key, value) in &self.headers { 53 | response.extend_from_slice(format!("{}: {}\r\n", key, value.to_str().unwrap()).as_bytes()); 54 | } 55 | 56 | response.extend_from_slice(b"\r\n"); 57 | 58 | if let Some(ref body) = self.body { 59 | response.extend_from_slice(body); 60 | } 61 | 62 | Bytes::from(response) 63 | } 64 | 65 | /// Converts the `HttpResponse` into a raw HTTP response as bytes. 66 | pub fn to_bytes(&self) -> Bytes { 67 | let mut response = self.to_string().into_bytes(); 68 | if let Some(ref body) = self.body { 69 | response.extend_from_slice(body); 70 | } 71 | 72 | Bytes::from(response) 73 | } 74 | /// Sends given String as given text 75 | pub fn send_text(&mut self, data: impl Into) { 76 | let body = data.into(); 77 | self.body = Some(Bytes::from(body)); 78 | self.headers 79 | .insert("Content-Type", "text/plain;charset=utf-8".parse().unwrap()); 80 | self.headers.insert( 81 | "Content-Length", 82 | self.body 83 | .as_ref() 84 | .unwrap() 85 | .len() 86 | .to_string() 87 | .parse() 88 | .unwrap(), 89 | ); 90 | self.headers 91 | .insert("Date", chrono::Utc::now().to_string().parse().unwrap()); 92 | } 93 | /// Sends Given Bytes as plain text 94 | pub fn send_bytes(&mut self, data: impl Into>) { 95 | let body = data.into(); 96 | self.headers 97 | .insert("Date", chrono::Utc::now().to_string().parse().unwrap()); 98 | self.body = Some(Bytes::from(body)); 99 | self.headers.insert( 100 | "Content-Length", 101 | self.body 102 | .as_ref() 103 | .unwrap() 104 | .len() 105 | .to_string() 106 | .parse() 107 | .unwrap(), 108 | ); 109 | } 110 | ///Takes a String(Should be valid HTML) and sends it's as Html 111 | pub fn send_html(&mut self, data: impl Into) { 112 | let body = data.into(); 113 | self.body = Some(Bytes::from(body)); 114 | self.headers 115 | .insert("Date", chrono::Utc::now().to_string().parse().unwrap()); 116 | self.headers 117 | .insert("Content-Type", "text/html".parse().unwrap()); 118 | self.headers.insert( 119 | "Content-Length", 120 | self.body 121 | .as_ref() 122 | .unwrap() 123 | .len() 124 | .to_string() 125 | .parse() 126 | .unwrap(), 127 | ); 128 | } 129 | /// Takes a Serializeable object and sends it as json. 130 | pub fn send_json(&mut self, data: T) { 131 | match serde_json::to_string(&data) { 132 | Ok(json) => { 133 | self.body = Some(Bytes::from(json)); 134 | self.headers 135 | .insert("Date", chrono::Utc::now().to_string().parse().unwrap()); 136 | self.headers 137 | .insert("Content-Type", HeaderValue::from_static("application/json")); 138 | self.headers.insert( 139 | "Content-Length", 140 | self.body 141 | .as_ref() 142 | .unwrap() 143 | .len() 144 | .to_string() 145 | .parse() 146 | .unwrap(), 147 | ); 148 | } 149 | Err(_) => { 150 | self.headers 151 | .insert("Date", chrono::Utc::now().to_string().parse().unwrap()); 152 | self.status = StatusCode::INTERNAL_SERVER_ERROR; 153 | self.body = Some(Bytes::from("Internal Server Error")); 154 | self.headers 155 | .insert("Content-Type", HeaderValue::from_static("text/plain")); 156 | self.headers.insert( 157 | "Content-Length", 158 | self.body 159 | .as_ref() 160 | .unwrap() 161 | .len() 162 | .to_string() 163 | .parse() 164 | .unwrap(), 165 | ); 166 | } 167 | } 168 | } 169 | /// Take a [File] Struct and sends it as a file 170 | pub fn send_file(&mut self, mut file: File) { 171 | let mut buffer = Vec::new(); 172 | match file.read_to_end(&mut buffer) { 173 | Ok(_) => { 174 | self.body = Some(Bytes::from(buffer)); 175 | self.headers 176 | .insert("Date", chrono::Utc::now().to_string().parse().unwrap()); 177 | self.headers.insert( 178 | "Content-Length", 179 | self.body 180 | .as_ref() 181 | .unwrap() 182 | .len() 183 | .to_string() 184 | .parse() 185 | .unwrap(), 186 | ); 187 | } 188 | Err(_) => { 189 | self.status = StatusCode::INTERNAL_SERVER_ERROR; 190 | self.body = Some(Bytes::from("Internal Server Error")); 191 | } 192 | } 193 | } 194 | } 195 | 196 | impl Display for Response { 197 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 198 | write!(f, "{}", self.to_string()) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod http; 2 | pub mod runtime; 3 | mod utils; 4 | 5 | pub use ::http::{HeaderMap, HeaderName, HeaderValue, Method, Uri}; 6 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/runtime/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::http::{Request, Response}; 2 | use crate::utils::worker::{Job, PoolConfig, TaskPool}; 3 | use crate::utils::{Message, Queue}; 4 | use std::io::{self, BufReader, BufWriter, Read, Result as IoResult, Write}; 5 | use std::net::{Ipv4Addr, TcpListener, ToSocketAddrs}; 6 | use std::sync::Arc; 7 | use std::sync::atomic::{AtomicBool, Ordering}; 8 | use std::thread; 9 | 10 | pub struct EngineConfig { 11 | /// The Address of the Engine(IP,Port) 12 | pub address: (Ipv4Addr, u16), 13 | pub worker_config: PoolConfig, 14 | } 15 | 16 | /// The Engine is the main struct of the Runtime. 17 | /// It drives the runtime Accepting Requests Answering them etc. 18 | pub struct Engine { 19 | listener: TcpListener, 20 | messages: Arc>, 21 | shutdown_flag: Arc, 22 | tasks: Arc, 23 | } 24 | 25 | impl Engine { 26 | /// Creates a new `Engine` instance without a config 27 | pub fn new(addr: impl ToSocketAddrs) -> Self { 28 | let listener = TcpListener::bind(addr).expect("Failed to bind to address"); 29 | let messages = Queue::with_capacity(16); 30 | let shutdown_flag = Arc::new(AtomicBool::new(false)); 31 | let tasks = TaskPool::new(); 32 | let server = Self { 33 | listener, 34 | messages: Arc::new(messages), 35 | shutdown_flag, 36 | tasks: Arc::new(tasks), 37 | }; 38 | server 39 | } 40 | /// Creates a new `Engine` instance with a config 41 | pub fn with_config(config: EngineConfig) -> Self { 42 | let listener = TcpListener::bind(config.address).expect("Failed to bind to address"); 43 | let messages = Queue::with_capacity(16); 44 | let shutdown_flag = Arc::new(AtomicBool::new(false)); 45 | let tasks = TaskPool::with_config(config.worker_config); 46 | let server = Self { 47 | listener, 48 | messages: Arc::new(messages), 49 | shutdown_flag, 50 | tasks: Arc::new(tasks), 51 | }; 52 | server 53 | } 54 | /// Add a new task to the internal TaskPool works like `thread::spawn` but its managed the Engine 55 | pub fn spawn(&self, task: impl Into) { 56 | self.tasks.add_task(task.into()); 57 | } 58 | /// Trigger the shutdown flag to stop the Engine. 59 | /// This method will unblock the thread that is waiting for a message. 60 | /// It will also stop the acceptor thread. 61 | /// This method should be called when the Engine is shutting down. 62 | /// Its Called when the Engine is dropped. 63 | pub fn shutdown(&self) { 64 | self.messages.unblock(); 65 | self.shutdown_flag.store(true, Ordering::SeqCst); 66 | } 67 | 68 | /// Starts Acceptor thread. 69 | /// This thread will accept incoming connections and push them to the queue. 70 | /// The thread will run until the Engine is shutdown. 71 | pub fn start(&self) { 72 | let inside_closer = self.shutdown_flag.clone(); 73 | let inside_queue = self.messages.clone(); 74 | let server = self.listener.try_clone().unwrap(); 75 | let tasks = self.tasks.clone(); 76 | // Start the Acceptor thread 77 | thread::spawn(move || { 78 | let tasks = tasks; 79 | log::debug!("Running Acceptor"); 80 | 81 | while !inside_closer.load(Ordering::SeqCst) { 82 | match server.accept() { 83 | Ok((stream, _)) => { 84 | let inside_queue = inside_queue.clone(); 85 | tasks.add_task(Job::Task(Box::new(move || { 86 | let mut buf_reader = BufReader::with_capacity(4096, &stream); 87 | let mut buffer = [0u8; 4096]; 88 | 89 | loop { 90 | buffer.fill(0); 91 | match buf_reader.read(&mut buffer) { 92 | Ok(0) => { 93 | stream.shutdown(std::net::Shutdown::Both).unwrap_or(()); 94 | break; 95 | } 96 | Ok(n) => { 97 | if let Ok(mut request) = Request::parse(&buffer[..n]) { 98 | request.set_stream(stream.try_clone().unwrap()); 99 | inside_queue.push(Message::Request(request)) 100 | } 101 | } 102 | Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => { 103 | log::debug!("Connection timed out"); 104 | stream.shutdown(std::net::Shutdown::Both).unwrap_or(()); 105 | break; 106 | } 107 | Err(ref e) if e.kind() == io::ErrorKind::ConnectionReset => { 108 | log::debug!("Connection reset by peer"); 109 | stream.shutdown(std::net::Shutdown::Both).unwrap_or(()); 110 | break; 111 | } 112 | Err(e) => { 113 | log::debug!("Error reading stream: {}", e); 114 | break; 115 | } 116 | } 117 | } 118 | }))); 119 | } 120 | Err(e) => { 121 | log::debug!("Error accepting connection: {}", e); 122 | continue; 123 | } 124 | } 125 | } 126 | log::debug!("Acceptor thread shutting down"); 127 | }); 128 | } 129 | 130 | /// Blocks until a message is available to receive. 131 | /// If the queue is empty, it will wait until a message is available. 132 | /// If the queue is unblocked, it will return an error. 133 | pub fn recv(&self) -> IoResult { 134 | match self.messages.pop() { 135 | Some(Message::Error(e)) => Err(e), 136 | Some(Message::Request(r)) => Ok(r), 137 | None => Err(io::Error::new(io::ErrorKind::Other, "No message available")), 138 | } 139 | } 140 | /// Returns the address the Engine is Bound to. 141 | pub fn address(&self) -> String { 142 | self.listener.local_addr().unwrap().to_string() 143 | } 144 | /// Unblocks the thread that is waiting for a message. 145 | /// this medhod allows graceful shutdown of the Engine's Runtime. 146 | pub fn unblock(&self) { 147 | self.messages.unblock(); 148 | } 149 | 150 | /// Iterates over incoming requests and handles them using the provided closure. 151 | /// The closure should take a `HttpRequest` and return a `HttpResponse`. 152 | /// This method will block until a request is available. 153 | /// It will also handle the response and write it to the stream. 154 | pub fn for_each(&self, mut handle: F) -> io::Result<()> 155 | where 156 | F: FnMut(Request) -> (Request, Response), 157 | { 158 | let engine = self; 159 | loop { 160 | match engine.recv() { 161 | Ok(request) => { 162 | let connect = &request 163 | .connection 164 | .as_deref() 165 | .unwrap_or("keep-alive") 166 | .to_lowercase(); 167 | let (request, mut response) = handle(request); 168 | response.add_header("connection", &connect); 169 | if let Some(mut stream) = request.take_stream() { 170 | let mut writer = BufWriter::new(&mut stream); 171 | writer.write_all(&response.to_raw())?; 172 | match writer.flush() { 173 | Ok(_) => {} 174 | Err(e) if e.kind() == io::ErrorKind::BrokenPipe => { 175 | log::debug!("Client disconnected"); 176 | continue; 177 | } 178 | Err(e) => { 179 | log::debug!("Error writing response: {}", e); 180 | break; 181 | } 182 | }; 183 | if connect == "close" { 184 | break; 185 | } 186 | } 187 | } 188 | Err(e) => { 189 | log::debug!("Error receiving message: {}", e); 190 | break; 191 | } 192 | } 193 | } 194 | Ok(()) 195 | } 196 | } 197 | 198 | impl Drop for Engine { 199 | fn drop(&mut self) { 200 | self.shutdown(); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/runtime/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod engine; 2 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/utils/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Debug, Error)] 4 | pub enum Error { 5 | #[error("An IO error occurred: {0}")] 6 | IoError(#[from] std::io::Error), 7 | 8 | #[error("A parsing error occurred: {0}")] 9 | ParseError(String), 10 | 11 | #[error("An unknown error occurred")] 12 | Unknown, 13 | } 14 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/utils/message.rs: -------------------------------------------------------------------------------- 1 | use crate::http::Request; 2 | use std::io::Error as IoError; 3 | 4 | /// A lightweight message type for the server. 5 | pub enum Message { 6 | Error(IoError), 7 | Request(Request), 8 | } 9 | 10 | impl From for Message { 11 | fn from(value: IoError) -> Self { 12 | Message::Error(IoError::from(value)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | mod message; 3 | mod queue; 4 | pub mod worker; 5 | 6 | pub use message::Message; 7 | pub use queue::Queue; 8 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/utils/queue.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::{Condvar, Mutex}; 2 | use std::collections::VecDeque; 3 | /// A simple enum to represent the Flow-State of the queue 4 | /// This enum is used to differentiate between a value and an unblock signal. 5 | /// The `Value` variant contains the actual value, while the `Unblock` variant 6 | pub enum QueueFlow { 7 | Value(T), 8 | Unblock, 9 | } 10 | 11 | /// A thread-safe queue that allows blocking and unblocking operations. 12 | /// This queue is designed to be used in a multi-threaded environment. 13 | pub struct Queue { 14 | /// A mutex to protect access to the queue 15 | /// This ensures that only one thread can access the queue at a time. 16 | queue: Mutex>>, 17 | /// A condition variable to notify waiting threads 18 | condvar: Condvar, 19 | } 20 | 21 | impl Queue { 22 | /// Creates a new `Queue` instance with the specified capacity. 23 | /// The capacity is the maximum number of items that can be stored in the queue. 24 | /// The queue will grow dynamically if more items are added. 25 | /// The `size` parameter specifies the initial capacity of the queue. 26 | pub fn with_capacity(size: usize) -> Self { 27 | Queue { 28 | queue: Mutex::new(VecDeque::with_capacity(size)), 29 | condvar: Condvar::new(), 30 | } 31 | } 32 | /// Pushes new item into to back of the queue. 33 | /// Threads Get notified when a new item is pushed via Condvar. 34 | pub fn push(&self, item: T) { 35 | let mut queue = self.queue.lock(); 36 | queue.push_back(QueueFlow::Value(item)); 37 | self.condvar.notify_one(); 38 | } 39 | ///Blocks Until a value is available to pop 40 | /// if Unblock is called it will return None 41 | pub fn pop(&self) -> Option { 42 | let mut queue = self.queue.lock(); 43 | loop { 44 | match queue.pop_front() { 45 | Some(QueueFlow::Value(v)) => return Some(v), 46 | Some(QueueFlow::Unblock) => return None, 47 | None => (), 48 | } 49 | self.condvar.wait(&mut queue); 50 | } 51 | } 52 | /// Unblocks the queue, all the threads that are struck in the pop method will return None 53 | pub fn unblock(&self) { 54 | let mut queue = self.queue.lock(); 55 | queue.push_back(QueueFlow::Unblock); 56 | self.condvar.notify_one(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /crates/feather-runtime/src/utils/worker.rs: -------------------------------------------------------------------------------- 1 | // src/utils/worker.rs 2 | use crossbeam::channel::{Receiver, Sender, unbounded}; 3 | use std::sync::Arc; 4 | use std::sync::atomic::{AtomicUsize, Ordering}; 5 | use std::thread; 6 | use std::time::Duration; 7 | 8 | #[derive(Debug)] 9 | pub struct PoolConfig { 10 | pub max_workers: usize, 11 | pub min_workers: usize, 12 | pub timeout: Duration, 13 | } 14 | impl Default for PoolConfig { 15 | fn default() -> Self { 16 | Self { 17 | max_workers: 60, 18 | min_workers: 6, 19 | timeout: Duration::from_secs(30), 20 | } 21 | } 22 | } 23 | 24 | /// A Dynamic TaskPool 25 | /// If there is not enough threads for the task it creates new ones 26 | pub struct TaskPool { 27 | sender: Sender, 28 | receiver: Receiver, 29 | active_workers: AtomicUsize, 30 | idle_workers: Arc, 31 | config: PoolConfig, 32 | } 33 | 34 | impl TaskPool { 35 | /// Create a new TaskPool with the default configration 36 | pub fn new() -> Self { 37 | let config = PoolConfig::default(); 38 | let min_workers = config.min_workers; 39 | let (sender, receiver) = unbounded(); 40 | let pool = TaskPool { 41 | sender, 42 | receiver, 43 | active_workers: AtomicUsize::new(0), 44 | idle_workers: Arc::new(AtomicUsize::new(0)), 45 | config, 46 | }; 47 | 48 | for _ in 0..min_workers { 49 | pool.add_worker(); 50 | } 51 | 52 | pool 53 | } 54 | /// Create a new TaskPool with the given configration 55 | pub fn with_config(config: PoolConfig) -> Self { 56 | let min_workers = config.min_workers; 57 | let (sender, receiver) = unbounded(); 58 | let pool = TaskPool { 59 | sender, 60 | receiver, 61 | active_workers: AtomicUsize::new(0), 62 | idle_workers: Arc::new(AtomicUsize::new(0)), 63 | config, 64 | }; 65 | 66 | for _ in 0..min_workers { 67 | pool.add_worker(); 68 | } 69 | 70 | pool 71 | } 72 | /// Add's new task to the sender channel of the TaskPool 73 | pub fn add_task(&self, task: Job) { 74 | self.sender.send(task).unwrap(); 75 | 76 | if self.idle_workers.load(Ordering::Acquire) == 0 77 | && self.active_workers.load(Ordering::Acquire) < self.config.max_workers 78 | { 79 | self.add_worker(); 80 | } 81 | } 82 | /// Add's a new worker if the load is higher than task pool can handle 83 | fn add_worker(&self) { 84 | self.active_workers.fetch_add(1, Ordering::Release); 85 | let idle_workers = self.idle_workers.clone(); 86 | let receiver = self.receiver.clone(); 87 | let timeout = self.config.timeout; 88 | thread::spawn(move || { 89 | loop { 90 | match receiver.recv_timeout(timeout) { 91 | Ok(job) => match job { 92 | Job::Task(task) => { 93 | idle_workers.fetch_sub(1, Ordering::Release); 94 | let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(task)); 95 | idle_workers.fetch_add(1, Ordering::Release); 96 | } 97 | Job::Stop => break, 98 | }, 99 | Err(_) => { 100 | // Timeout: terminate the worker if idle 101 | if idle_workers.fetch_sub(1, Ordering::Release) == 1 { 102 | break; 103 | } 104 | } 105 | } 106 | } 107 | }); 108 | } 109 | } 110 | /// Dropin here babyyyyy 111 | impl Drop for TaskPool { 112 | fn drop(&mut self) { 113 | for _ in 0..self.active_workers.load(Ordering::Acquire) { 114 | self.sender.send(Job::Stop).unwrap(); 115 | } 116 | } 117 | } 118 | 119 | /// Represents a job to be executed by the worker pool. 120 | /// It can be either a task (a closure) or a signal to stop the worker. 121 | pub enum Job { 122 | Task(Box), 123 | Stop, 124 | } 125 | 126 | impl Into for Box { 127 | fn into(self) -> Job { 128 | Job::Task(self) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /crates/feather/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "feather" 3 | version = "0.4.4" 4 | edition = "2024" 5 | repository = "https://github.com/BersisSe/feather" 6 | description = "Feather: A minimal HTTP framework for Rust" 7 | authors = ["Bersis Sevimli"] 8 | license = "MIT" 9 | readme = "README.md" 10 | keywords = ["http", "web", "framework", "minimal", "rust"] 11 | categories = ["web-programming", "network-programming"] 12 | 13 | [dependencies] 14 | anymap = { workspace = true } 15 | chrono = { workspace = true } 16 | feather-runtime = { workspace = true } 17 | jsonwebtoken = { workspace = true, optional = true } 18 | serde = { workspace = true, features = ["derive"], optional = true } 19 | serde_json = { workspace = true, optional = true } 20 | [features] 21 | default = ["json"] 22 | json = ["dep:serde", "dep:serde_json"] 23 | jwt = ["dep:jsonwebtoken"] 24 | -------------------------------------------------------------------------------- /crates/feather/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /crates/feather/src/internals/app.rs: -------------------------------------------------------------------------------- 1 | use super::error_stack::ErrorHandler; 2 | use super::AppContext; 3 | use crate::middleware::Middleware; 4 | pub use feather_runtime::Method; 5 | use feather_runtime::http::{Request, Response}; 6 | use feather_runtime::runtime::engine::Engine; 7 | use std::borrow::Cow; 8 | use std::{fmt::Display, net::ToSocketAddrs}; 9 | 10 | /// A route in the application. 11 | pub struct Route { 12 | method: Method, 13 | path: Cow<'static, str>, 14 | middleware: Box, 15 | } 16 | 17 | /// A Feather application. 18 | 19 | pub struct App { 20 | routes: Vec, 21 | middleware: Vec>, 22 | context: AppContext, 23 | error_handler: Option 24 | } 25 | 26 | macro_rules! route_methods { 27 | ($($method:ident $name:ident)+) => { 28 | $( 29 | /// Adds a route to the application for the HTTP method. 30 | pub fn $name(&mut self, path: impl Into, middleware: M) { 31 | self.route(Method::$method, path.into(), middleware); 32 | } 33 | )+ 34 | } 35 | } 36 | 37 | impl App { 38 | /// Create a new instance of the application 39 | #[must_use = "Does nothing if you don't use the `listen` method"] 40 | pub fn new() -> Self { 41 | Self { 42 | routes: Vec::new(), 43 | middleware: Vec::new(), 44 | context: AppContext::new(), 45 | error_handler: None 46 | } 47 | } 48 | /// Returns a Handle to the [AppContext] inside the App 49 | /// [AppContext] is Used for App wide state managment 50 | pub fn context(&mut self) -> &mut AppContext { 51 | &mut self.context 52 | } 53 | /// Set up the Error Handling Solution for that [App]. 54 | /// If there are no Error Handler present by default, 55 | /// framework will Catch the error and print it to the `stderr` and return a `500` Status code response back to the client 56 | pub fn set_error_handler(&mut self,handler: ErrorHandler){ 57 | self.error_handler = Some(handler) 58 | } 59 | 60 | /// Add a route to the application. 61 | /// Every Route Returns A MiddlewareResult to control the flow of your application. 62 | pub fn route( 63 | &mut self, 64 | method: Method, 65 | path: impl Into>, 66 | middleware: M, 67 | ) { 68 | self.routes.push(Route { 69 | method, 70 | path: path.into(), 71 | middleware: Box::new(middleware), 72 | }); 73 | } 74 | 75 | /// Add a global middleware to the application that will be applied to all routes. 76 | pub fn use_middleware(&mut self, middleware: impl Middleware + 'static) { 77 | self.middleware.push(Box::new(middleware)); 78 | } 79 | 80 | route_methods!( 81 | GET get 82 | POST post 83 | PUT put 84 | DELETE delete 85 | PATCH patch 86 | HEAD head 87 | OPTIONS options 88 | ); 89 | 90 | fn run_middleware( 91 | mut request: &mut Request, 92 | routes: &[Route], 93 | global_middleware: &[Box], 94 | mut context: &mut AppContext, 95 | error_handler: &Option 96 | ) -> Response { 97 | let mut response = Response::default(); 98 | // Run global middleware 99 | 100 | for middleware in global_middleware { 101 | match middleware.handle(&mut request, &mut response, &mut context) { 102 | Ok(_) => {} 103 | Err(e) => { 104 | if let Some(handler) = &error_handler { 105 | handler(e,&request,&mut response) 106 | }else{ 107 | eprintln!("Unhandled Error caught in middlewares: {}",e); 108 | response.set_status(500).send_text("Internal Server Error!"); 109 | return response; 110 | } 111 | } 112 | } 113 | } 114 | // Run route-specific middleware 115 | if let Some(route) = routes 116 | .iter() 117 | .find(|r| r.method == request.method && r.path == request.path()) 118 | { 119 | match route.middleware.handle(request, &mut response, &mut context){ 120 | Ok(_) => {} 121 | Err(e) => { 122 | if let Some(handler) = &error_handler{ 123 | handler(e,&request,&mut response) 124 | }else{ 125 | eprintln!("Unhandled Error caught in Route Middlewares : {}", e); 126 | response.set_status(500).send_text("Internal Server Error"); 127 | } 128 | } 129 | } 130 | }else{ 131 | response.set_status(404).send_text("404 Not Found"); 132 | } 133 | 134 | response 135 | } 136 | 137 | 138 | /// Start the application and listen for incoming requests on the given address. 139 | /// Blocks the current thread until the server is stopped. 140 | /// 141 | /// # Panics 142 | /// 143 | /// Panics if the server fails to start 144 | pub fn listen(&mut self, address: impl ToSocketAddrs + Display) { 145 | println!( 146 | "Feather listening on : http://{address}", 147 | ); 148 | let rt = Engine::new(address); 149 | let routes = &self.routes; 150 | let middleware = &self.middleware; 151 | let mut ctx = &mut self.context; 152 | let error_handle = &self.error_handler; 153 | rt.start(); 154 | rt.for_each(move |mut req| { 155 | let req_clone = req.clone(); 156 | let response = Self::run_middleware(&mut req, &routes, &middleware, &mut ctx,error_handle); 157 | return (req_clone,response); 158 | }).unwrap(); 159 | } 160 | } 161 | 162 | -------------------------------------------------------------------------------- /crates/feather/src/internals/context.rs: -------------------------------------------------------------------------------- 1 | use anymap::AnyMap; 2 | 3 | /// AppContext Is Used for Managing state in your Feather Application 4 | /// **Context Should only used with objects like Connections and Custom Structs!** 5 | #[derive(Debug)] 6 | pub struct AppContext { 7 | inner: AnyMap, 8 | } 9 | impl AppContext { 10 | /// Crate Only Method 11 | pub(crate) fn new() -> Self { 12 | Self { 13 | inner: AnyMap::new(), 14 | } 15 | } 16 | /// Used the Read the State from the Context you can use the turbofish syntax to access objects 17 | /// Like this: 18 | /// ```rust,ignore 19 | /// let db = ctx.read_state::(); 20 | /// ``` 21 | pub fn get_state(&self) -> Option<&T> { 22 | self.inner.get::() 23 | } 24 | /// Used the Read the State from the Context you can use the turbofish syntax to access objects 25 | /// Like this: 26 | /// ```rust,ignore 27 | /// let db = ctx.read_state::(); 28 | /// ``` 29 | /// Returns a Mutable referance to the object 30 | pub fn get_mut_state(&mut self) -> Option<&mut T> { 31 | self.inner.get_mut::() 32 | } 33 | /// Used to Capture a Object as a State Inside the Context 34 | /// That object Ownership now gets transfered to the context 35 | /// This method is very useful when using database connections and file accesses 36 | pub fn set_state(&mut self, val: T) { 37 | self.inner.insert::(val); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /crates/feather/src/internals/error_stack.rs: -------------------------------------------------------------------------------- 1 | // I get it its kinda pointess to open a new module for just a 2 types but maybe I'll add more features to the errors ;) 2 | 3 | use std::error::Error; 4 | use feather_runtime::http::{Request, Response}; 5 | 6 | type BoxError = Box; 7 | 8 | /// Type Alias for the Error Handling Function: `Box` 9 | pub type ErrorHandler = Box; 10 | -------------------------------------------------------------------------------- /crates/feather/src/internals/mod.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod context; 3 | pub use app::App; 4 | pub use context::AppContext; 5 | mod error_stack; 6 | 7 | pub use feather_runtime::{Method,Uri,HeaderMap,HeaderName,HeaderValue}; -------------------------------------------------------------------------------- /crates/feather/src/jwt.rs: -------------------------------------------------------------------------------- 1 | use crate::{middleware::Middleware, next, AppContext, Outcome, Request, Response}; 2 | use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub struct Claims { 7 | pub sub: String, 8 | pub exp: usize, 9 | } 10 | 11 | 12 | /// Used to protect the route with JWT authentication 13 | /// The middleware will check if the token is valid and not expired 14 | /// If the token is valid, it will call the handler function 15 | /// If the token is invalid or expired, it will send back a 401 error 16 | /// The handler function will receive the request, response and the claims 17 | pub fn with_jwt_auth(secret: &'static str, handler: F) -> impl Middleware 18 | where 19 | F: Fn(&mut Request, &mut Response, &mut AppContext, Claims) -> Outcome, 20 | { 21 | move |req: &mut Request, res: &mut Response, ctx: &mut AppContext| -> Outcome { 22 | let Some(auth_header) = req.headers.get("Authorization") else { 23 | res.set_status(401); 24 | res.send_text("Missing Authorization header"); 25 | return next!(); 26 | }; 27 | 28 | let Ok(auth_str) = auth_header.to_str() else { 29 | res.set_status(400); 30 | res.send_text("Invalid header format"); 31 | return next!(); 32 | }; 33 | 34 | if !auth_str.starts_with("Bearer ") { 35 | res.set_status(400); 36 | res.send_text("Expected Bearer token"); 37 | return next!(); 38 | } 39 | 40 | let token = &auth_str[7..]; 41 | 42 | match decode::( 43 | token, 44 | &DecodingKey::from_secret(secret.as_bytes()), 45 | &Validation::default(), 46 | ) { 47 | Ok(data) => Ok(handler(req, res, ctx, data.claims)?), 48 | Err(_) => { 49 | res.set_status(401); 50 | res.send_text("Invalid or expired token"); 51 | next!() 52 | } 53 | } 54 | } 55 | } 56 | 57 | /// Function to generate a JWT token 58 | /// The token will be valid for 1 hour 59 | /// and will be signed with the secret 60 | pub fn generate_jwt( 61 | subject: Option<&str>, 62 | secret: &str, 63 | ) -> Result { 64 | let claims = Claims { 65 | sub: subject.unwrap_or_default().to_string(), 66 | exp: chrono::Utc::now() 67 | .checked_add_signed(chrono::Duration::hours(1)) 68 | .expect("valid timestamp") 69 | .timestamp() as usize, 70 | }; 71 | 72 | encode( 73 | &Header::default(), 74 | &claims, 75 | &EncodingKey::from_secret(secret.as_bytes()), 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /crates/feather/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Feather is a lightweight and extensible web framework for Rust, inspired by the simplicity of Express.js. 2 | //! 3 | //! # Features 4 | //! - **Express.js-like Routing**: Use `app.get` style routing for simplicity and familiarity. 5 | //! - **State Management**: Manage application state efficiently using the Context API. 6 | //! - **Error Handling**: Runtime error handling for easier debugging and recovery. 7 | //! - **Middleware Support**: Create and chain middlewares for modular and reusable code. 8 | //! - **Out of the Box Support**: Things like JWT are supported via Cargo Features 9 | //! 10 | //! 11 | //! ## Example 12 | //! ```rust,no_run 13 | //! use feather::{App, Request, Response, AppContext, next}; 14 | //! 15 | //! fn main() { 16 | //! let mut app = App::new(); 17 | //! 18 | //! app.get("/", |_req: &mut Request, res: &mut Response, _ctx: &mut AppContext| { 19 | //! res.send_text("Hello, Feather!"); 20 | //! next!() 21 | //! }); 22 | //! 23 | //! app.listen("127.0.0.1:5050"); 24 | //! } 25 | //! ``` 26 | //! 27 | //! # Modules 28 | //! - `middleware`: Define and use custom middlewares. 29 | //! - `internals`: Core components like `App` and `AppContext`. 30 | //! - `jwt` (optional): JWT utilities for authentication (requires the `jwt` feature). 31 | //! 32 | //! # Type Aliases 33 | //! - `Outcome`: A type alias for `Result>`, used as the return type for middlewares. 34 | //! 35 | //! # Macros 36 | //! - `next!`: A syntactic sugar for `Ok(MiddlewareResult::Next)`, reducing boilerplate in middleware implementations. 37 | 38 | pub mod middleware; 39 | pub mod internals; 40 | #[cfg(feature = "jwt")] 41 | pub mod jwt; 42 | 43 | use std::error::Error; 44 | 45 | pub use feather_runtime::http::{Request,Response}; 46 | pub use internals::{App, AppContext}; 47 | pub use crate::middleware::MiddlewareResult; 48 | 49 | /// This is just a type alias for `Result>;` 50 | /// Outcome is used in All middlewares as a return type. 51 | pub type Outcome = Result>; 52 | 53 | /// This macro is just a syntactic sugar over the `Ok(MiddlewareResult::Next)` 54 | /// syntax just to clear some Boilerplate 55 | #[macro_export] 56 | macro_rules! next { 57 | () => { 58 | Ok($crate::middleware::MiddlewareResult::Next) 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /crates/feather/src/middleware/builtins.rs: -------------------------------------------------------------------------------- 1 | use crate::{internals::AppContext, next, Outcome}; 2 | use super::common::Middleware; 3 | 4 | 5 | use feather_runtime::http::{Request, Response}; 6 | use std::{ 7 | fs::{self, File}, io::{self, Read}, path::Path, 8 | }; 9 | 10 | /// Log incoming requests and transparently pass them to the next middleware. 11 | pub struct Logger; 12 | 13 | impl Middleware for Logger { 14 | fn handle(&self, request: &mut Request, _: &mut Response, _: &mut AppContext) -> Outcome { 15 | println!("Request: {request}"); 16 | next!() 17 | } 18 | } 19 | 20 | #[derive(Default)] 21 | /// Add [CORS] headers to the response. 22 | /// 23 | /// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS 24 | pub struct Cors(Option); 25 | 26 | impl Cors { 27 | #[must_use] 28 | pub const fn new(origin: String) -> Self { 29 | Self(Some(origin)) 30 | } 31 | } 32 | 33 | impl Middleware for Cors { 34 | fn handle(&self, _: &mut Request, response: &mut Response, _: &mut AppContext) -> Outcome { 35 | response.add_header( 36 | "Access-Control-Allow-Origin", 37 | self.0.as_deref().unwrap_or("*"), 38 | ); 39 | next!() 40 | } 41 | } 42 | 43 | /// Serve static files from the given path. 44 | pub struct ServeStatic(String); 45 | 46 | impl ServeStatic { 47 | #[must_use = "Put this in a `App.use_middleware()` Method"] 48 | pub const fn new(directory: String) -> Self { 49 | Self(directory) 50 | } 51 | 52 | fn handle_io_error(&self, e: io::Error, path: &Path, response: &mut Response) { 53 | let status_code = match e.kind() { 54 | io::ErrorKind::PermissionDenied => 403, 55 | io::ErrorKind::NotFound => 404, 56 | _ => 500, // Internal Server Error for other IO issues 57 | }; 58 | 59 | eprintln!( 60 | "ServeStatic: Error accessing path {:?} (Base: {}): {} - Responding with {}", 61 | path, &self.0, e, status_code 62 | ); 63 | 64 | response.set_status(status_code); 65 | match status_code { 66 | 404 => response.send_text("404 Not Found"), 67 | 403 => response.send_text("403 Forbidden"), 68 | _ => response.send_text("500 Internal Server Error"), 69 | }; 70 | } 71 | 72 | fn guess_content_type(path: &Path) -> &'static str { 73 | match path.extension().and_then(|ext| ext.to_str()) { 74 | Some("html") | Some("htm") => "text/html; charset=utf-8", 75 | Some("css") => "text/css; charset=utf-8", 76 | Some("js") => "application/javascript; charset=utf-8", 77 | Some("json") => "application/json", 78 | Some("png") => "image/png", 79 | Some("jpg") | Some("jpeg") => "image/jpeg", 80 | Some("gif") => "image/gif", 81 | Some("svg") => "image/svg+xml", 82 | Some("ico") => "image/x-icon", 83 | Some("txt") => "text/plain; charset=utf-8", 84 | _ => "application/octet-stream", // Default binary type 85 | } 86 | } 87 | } 88 | 89 | impl Middleware for ServeStatic { 90 | fn handle( 91 | &self, 92 | request: &mut Request, 93 | response: &mut Response, 94 | _: &mut AppContext, 95 | ) -> Outcome { 96 | let requested_path = request.uri.path().trim_start_matches('/'); 97 | let base_dir = Path::new(&self.0); 98 | let mut target_path = base_dir.join(requested_path); 99 | 100 | match target_path.canonicalize() { 101 | Ok(canonical_path) => { 102 | // Need to canonicalize base_dir too for reliable comparison 103 | match base_dir.canonicalize() { 104 | Ok(canonical_base) => { 105 | if !canonical_path.starts_with(&canonical_base) { 106 | // Path tried to escape the base directory! 107 | eprintln!( 108 | "ServeStatic: Forbidden path traversal attempt: Requested '{}', Resolved '{}' outside base '{}'", 109 | requested_path, canonical_path.display(), canonical_base.display() 110 | ); 111 | response.set_status(403); 112 | response.send_text("403 Forbidden"); 113 | return next!() 114 | } 115 | target_path = canonical_path; 116 | } 117 | Err(e) => { 118 | // Failed to canonicalize base directory - major configuration issue 119 | self.handle_io_error(e, base_dir, response); 120 | return next!() 121 | } 122 | } 123 | } 124 | Err(e) => { 125 | self.handle_io_error(e, &target_path, response); 126 | return next!() 127 | } 128 | } 129 | 130 | match fs::metadata(&target_path) { 131 | Ok(metadata) => { 132 | if metadata.is_file() { 133 | match File::open(&target_path) { 134 | Ok(mut file) => { 135 | let mut buffer = Vec::new(); 136 | match file.read_to_end(&mut buffer) { 137 | Ok(_) => { 138 | let content_type = Self::guess_content_type(&target_path); 139 | response.add_header("Content-Type", content_type); 140 | response.add_header("Content-Length", &buffer.len().to_string()); 141 | response.send_bytes(buffer); 142 | } 143 | Err(e) => { 144 | self.handle_io_error(e, &target_path, response); 145 | } 146 | } 147 | } 148 | Err(e) => { 149 | self.handle_io_error(e, &target_path, response); 150 | } 151 | } 152 | } else if metadata.is_dir() { 153 | eprintln!("ServeStatic: Access denied for directory: {:?}", target_path); 154 | response.set_status(403); 155 | response.send_text("403 Forbidden"); 156 | } else { 157 | eprintln!("ServeStatic: Path is not a file or directory: {:?}", target_path); 158 | response.set_status(404); 159 | response.send_text("404 Not Found"); 160 | } 161 | } 162 | Err(e) => { 163 | // Error getting metadata (likely Not Found or Permission Denied) 164 | self.handle_io_error(e, &target_path, response); 165 | } 166 | } 167 | 168 | next!() 169 | } 170 | } 171 | 172 | -------------------------------------------------------------------------------- /crates/feather/src/middleware/common.rs: -------------------------------------------------------------------------------- 1 | use crate::{internals::AppContext, Outcome}; 2 | use feather_runtime::http::{Request, Response}; 3 | 4 | pub trait Middleware { 5 | /// Handle an incoming request synchronously. 6 | fn handle( 7 | &self, 8 | request: &mut Request, 9 | response: &mut Response, 10 | ctx: &mut AppContext, 11 | ) -> Outcome; 12 | } 13 | 14 | #[derive(Debug)] 15 | pub enum MiddlewareResult { 16 | /// Continue to the next middleware. 17 | Next, 18 | /// Skip all subsequent middleware and continue to the next route. 19 | NextRoute, 20 | } 21 | 22 | /// Implement the `Middleware` trait for a slice of middleware. 23 | impl Middleware for [&Box] { 24 | fn handle( 25 | &self, 26 | request: &mut Request, 27 | response: &mut Response, 28 | ctx: &mut AppContext, 29 | ) -> Outcome { 30 | for middleware in self { 31 | if matches!( 32 | middleware.handle(request, response, ctx), 33 | Ok(MiddlewareResult::NextRoute) 34 | ) { 35 | return Ok(MiddlewareResult::NextRoute); 36 | } 37 | } 38 | Ok(MiddlewareResult::Next) 39 | } 40 | } 41 | 42 | ///Implement the `Middleware` trait for Closures with Request and Response Parameters. 43 | impl Outcome> Middleware for F { 44 | fn handle( 45 | &self, 46 | request: &mut Request, 47 | response: &mut Response, 48 | ctx: &mut AppContext, 49 | ) -> Outcome { 50 | self(request, response, ctx) 51 | } 52 | } 53 | 54 | /// Can be used to chain two middlewares together. 55 | /// The first middleware will be executed first. 56 | /// If it returns `MiddlewareResult::Next`, the second middleware will be executed. 57 | pub fn _chainer(a: A, b: B) -> impl Middleware 58 | where 59 | A: Middleware, 60 | B: Middleware, 61 | { 62 | move |request: &mut Request, 63 | response: &mut Response, 64 | ctx: &mut AppContext| 65 | -> Outcome { 66 | match a.handle(request, response, ctx) { 67 | Ok(MiddlewareResult::Next) => b.handle(request, response, ctx), 68 | Ok(MiddlewareResult::NextRoute) => Ok(MiddlewareResult::NextRoute), 69 | Err(e) => Err(e), 70 | } 71 | } 72 | } 73 | 74 | #[macro_export] 75 | /// A macro to chain multiple middlewares together.
76 | /// This macro takes a list of middlewares and chains them together. 77 | macro_rules! chain { 78 | ($first:expr, $($rest:expr),+ $(,)?) => {{ 79 | let chained = $first; 80 | $(let chained = $crate::middleware::common::_chainer(chained, $rest);)+ 81 | chained 82 | }}; 83 | } 84 | pub use chain; 85 | -------------------------------------------------------------------------------- /crates/feather/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | // This is the module for the Middleware system. 2 | 3 | pub mod builtins; 4 | pub mod common; 5 | 6 | pub use common::{Middleware, MiddlewareResult, chain }; 7 | -------------------------------------------------------------------------------- /examples/api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "api" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true, features = ["json"] } 9 | -------------------------------------------------------------------------------- /examples/api/src/main.rs: -------------------------------------------------------------------------------- 1 | use feather::{App, AppContext, Request, Response,next}; 2 | 3 | fn main() { 4 | // Lets Create a App instance named api 5 | let mut api = App::new(); 6 | 7 | // Register the get_handler function for the "/" path 8 | api.get("/", get_handler); 9 | // Lets use a post handler to simulate a login 10 | // This will be called when a POST request is made to the "/auth" path 11 | // The data will be parsed as JSON and echoed back to the client 12 | api.post( 13 | "/auth", 14 | |req: &mut Request, res: &mut Response, _ctx: &mut AppContext| { 15 | let data = req.json().unwrap(); 16 | println!("Received data: {:?}", data); 17 | res.send_json(data); 18 | next!() 19 | }, 20 | ); 21 | // We have to listen to the api instance to start the server 22 | // This will start the server on port 5050 23 | api.listen("127.0.0.1:5050"); 24 | } 25 | 26 | // Handler Can Also Be Functions Like this 27 | // This function will be called when a GET request is made to the "/" 28 | fn get_handler( 29 | _req: &mut Request, 30 | res: &mut Response, 31 | _ctx: &mut AppContext, 32 | ) -> feather::Outcome { 33 | res.send_html("

Hello I am an Feather Api

"); 34 | next!() 35 | } 36 | -------------------------------------------------------------------------------- /examples/basic-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true } 9 | -------------------------------------------------------------------------------- /examples/basic-app/src/main.rs: -------------------------------------------------------------------------------- 1 | // Import dependencies from Feather 2 | use feather::middleware::builtins; 3 | use feather::{App, AppContext, next}; 4 | use feather::{Request, Response}; 5 | 6 | // Main function - no async here! 7 | fn main() { 8 | // Create a new instance of App 9 | let mut app = App::new(); 10 | 11 | // Define a route for the root path 12 | app.get( 13 | "/", 14 | |_request: &mut Request, response: &mut Response, _ctx: &mut AppContext| { 15 | response.send_text("Hello, world!"); 16 | next!() 17 | }, 18 | ); 19 | // Use the Logger middleware for all routes 20 | app.use_middleware(builtins::Logger); 21 | // Listen on port 5050 22 | app.listen("127.0.0.1:5050"); 23 | } 24 | -------------------------------------------------------------------------------- /examples/context/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "context" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true, features = ["json"] } 9 | rusqlite = { workspace = true } 10 | serde_json = { workspace = true } -------------------------------------------------------------------------------- /examples/context/src/main.rs: -------------------------------------------------------------------------------- 1 | /// Use of the AppContext State Managment with Sqlite 2 | // Import Our Dependencies 3 | use feather::{next, App, AppContext, Outcome, Request, Response}; 4 | use rusqlite::{Connection, Result}; 5 | use serde_json::json; 6 | 7 | fn main() -> Result<()> { 8 | // Create a new App 9 | let mut app = App::new(); 10 | // Open a Connection to Sqlite 11 | let conn = Connection::open_in_memory()?; 12 | // Create a person table 13 | conn.execute( 14 | " 15 | CREATE TABLE person ( 16 | id INTEGER PRIMARY KEY, 17 | name TEXT NOT NULL 18 | )", 19 | [], 20 | )?; 21 | app.context().set_state(conn); // Store the connection inside of our context 22 | // from now on context is only accesible inside the context 23 | 24 | app.post("/login", login); 25 | 26 | app.get("/user", get_user); 27 | 28 | app.listen("127.0.0.1:5050"); 29 | Ok(()) 30 | } 31 | // Post Route for loging in users 32 | fn login(req: &mut Request, res: &mut Response, ctx: &mut AppContext) -> Outcome { 33 | let data = match req.json() { 34 | Ok(json) => json, 35 | Err(_) => { 36 | res.set_status(400).send_json(json!({"error": "Invalid JSON"})); 37 | return next!() 38 | } 39 | }; 40 | println!("Received Json: {data}"); // Log it to see what we got 41 | let db = ctx.get_state::().unwrap(); // Get the Connection from the context. Unwrap is safe here because we know we set it before 42 | let username = match data.get("username") { 43 | Some(v) => v.to_string(), 44 | None => { 45 | res.set_status(400).send_text("No username found in the data!"); 46 | return next!() 47 | } 48 | }; 49 | 50 | // Now Lets put it inside of our DB 51 | match db.execute("INSERT INTO person (name) VALUES (?1)", [username]) { 52 | //If it succeeds we send a successs message with 200 Code 53 | Ok(rows_changed) => res.set_status(200).send_json(json! 54 | ( 55 | { 56 | "success":true, 57 | "rows_changed":rows_changed 58 | } 59 | )), 60 | //If it fails we send a successs message with 500 Code 61 | Err(e) => { 62 | res.set_status(500).send_json(json! 63 | ( 64 | { 65 | "success":false, 66 | } 67 | )); 68 | println!("{e}") 69 | } 70 | }; 71 | next!() 72 | } 73 | // Get Route for listing users 74 | fn get_user(_req: &mut Request, res: &mut Response, ctx: &mut AppContext) -> Outcome { 75 | let db = ctx.get_state::().unwrap(); // Again Take our Connection from the context. that is still a single connection 76 | 77 | // We can use the ? operator here because we are inside of a function that returns a Result 78 | // We prepare a statement to select all names from the person table 79 | let mut stmt = db.prepare("SELECT name FROM person")?; 80 | let users = stmt 81 | .query_map([], |row| row.get::<_, String>(0))? 82 | .filter_map(Result::ok) 83 | .collect::>(); 84 | 85 | res.set_status(200).send_json(json!({ "users": users })); 86 | next!() 87 | } 88 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true } 9 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use feather::{next, App, AppContext, Request, Response}; 2 | // Create a couter struct to hold the state 3 | #[derive(Debug)] 4 | struct Counter { 5 | pub count: i32, 6 | } 7 | 8 | fn main() { 9 | let mut app = App::new(); 10 | let counter = Counter { count: 0 }; 11 | // Put the counter in the app context 12 | app.context().set_state(counter); 13 | 14 | app.get( 15 | "/", 16 | move |_req: &mut Request, res: &mut Response, ctx: &mut AppContext| { 17 | let counter: &mut Counter = ctx.get_mut_state::().unwrap(); 18 | counter.count += 1;// Increment the counter for every request 19 | // Send the current count as a response 20 | res.send_text(format!("Counted! {}", counter.count)); 21 | next!() 22 | }, 23 | ); 24 | // Lastly add a route to get the current count 25 | app.get( 26 | "/count", 27 | move |_req: &mut Request, res: &mut Response, ctx: &mut AppContext| { 28 | let counter = ctx.get_state::().unwrap(); 29 | res.send_text(counter.count.to_string()); 30 | next!() 31 | }, 32 | ); 33 | 34 | app.listen("127.0.0.1:5050"); 35 | } 36 | -------------------------------------------------------------------------------- /examples/error-pipeline/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "error-pipeline" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true } 9 | -------------------------------------------------------------------------------- /examples/error-pipeline/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io}; 2 | use feather::{next, App, AppContext, Request, Response}; 3 | /// Lets see how can we use the new error pipeline feature! 4 | fn main() { 5 | let mut app = App::new(); 6 | app.get("/", |_req: &mut Request,_res: &mut Response,_ctx: &mut AppContext|{ 7 | // Lets say we have a operation that can fail for this example a File Access 8 | let _file: fs::File = fs::File::open("file.txt")?; // With the ? Operator we can easily toss the error in the pipeline to be handled 9 | next!() 10 | }); 11 | 12 | // if there is no Custom Error handler set Framework will catch the error log it and send a 500 back to the client 13 | // We can attach a custom error handler with this function 14 | app.set_error_handler(Box::new(|err,_req,res|{ 15 | println!("A Error Accured"); 16 | if err.is::(){ 17 | println!("Error is a IO error{err}"); 18 | res.set_status(500).send_text("Missing data on the server? Internal Error"); 19 | } 20 | })); 21 | // This way we can handle Errors Gracefully and safely. 22 | app.listen("127.0.0.1:5050"); 23 | } -------------------------------------------------------------------------------- /examples/jwt/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "jwt" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true, features = ["jwt"] } 9 | -------------------------------------------------------------------------------- /examples/jwt/src/main.rs: -------------------------------------------------------------------------------- 1 | // This example demonstrates how to use JWT authentication in a Feather application. 2 | use feather::jwt::{generate_jwt, with_jwt_auth}; 3 | use feather::{next, App, AppContext}; 4 | 5 | fn main() { 6 | // Create a new instance of App 7 | let mut app = App::new(); 8 | // This route will used to generate a JWT token 9 | // and send it back to the client 10 | // The token will be valid for 1 hour 11 | // and will be signed with the secret 12 | app.get( 13 | "/", 14 | |_req: &mut feather::Request, res: &mut feather::Response, ctx: &mut AppContext| { 15 | let token = generate_jwt(Some("Subject"), "secretcode").unwrap(); 16 | res.send_text(format!("Token: {}", token)); 17 | ctx.set_state(token); 18 | next!() 19 | }, 20 | ); 21 | // This route will be used to test the JWT authentication 22 | // It will check if the token is valid and not expired 23 | // If the token is valid, it will send back a message(Hello, JWT!) 24 | // If the token is invalid or expired, it will send back a 401 error 25 | 26 | app.get( 27 | "/auth", 28 | with_jwt_auth("secretcode", |_req, res, _ctx, claim| { 29 | println!("Claim: {:?}", claim); 30 | let token = _ctx.get_state::(); 31 | println!("Toke: {}", token.unwrap()); 32 | res.send_text("Hello, JWT!"); 33 | next!() 34 | }), 35 | ); 36 | // Of course lets listen on port 8080 37 | app.listen("127.0.0.1:8080") 38 | } 39 | -------------------------------------------------------------------------------- /examples/middleware/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "middleware" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true } 9 | -------------------------------------------------------------------------------- /examples/middleware/src/main.rs: -------------------------------------------------------------------------------- 1 | use feather::{chain, middleware::builtins, next, App, AppContext, Outcome, Request, Response}; 2 | mod middleware; 3 | use middleware::MyMiddleware; 4 | fn main(){ 5 | let mut app = App::new(); 6 | 7 | app.use_middleware(builtins::Logger);// We can easily use middlewares using this syntax 8 | // We can also put Closures as a middleware parameter. that what makes Feather "Middleware-First" 9 | app.use_middleware(|_req: &mut feather::Request,_res: &mut feather::Response,_ctx: &mut feather::AppContext|{ 10 | println!("Ow a Request: I am a Closure Middleware BTW"); 11 | next!() 12 | }); 13 | app.use_middleware(MyMiddleware("Secret Codee".to_string())); 14 | 15 | app.get("/", |_req: &mut Request,res: &mut Response,_ctx: &mut AppContext|{ 16 | res.send_text("Hellooo Feather Middleware Example"); 17 | res.set_status(200); 18 | next!() 19 | }); 20 | // You can also chain middlewares using the `chain!` macro 21 | // the first given middleware will always run first! 22 | // You can also chain more than 2 middlewares 23 | app.get("/chain", chain!(first,second)); 24 | 25 | app.listen("127.0.0.1:5050"); 26 | } 27 | 28 | fn first(_req:&mut Request,res:&mut Response, _ctx: &mut AppContext) -> Outcome{ 29 | println!("First Middleware Ran"); 30 | res.set_status(201); 31 | next!() 32 | } 33 | fn second(_req:&mut Request,res:&mut Response, _ctx: &mut AppContext) -> Outcome{ 34 | println!("Second Ran"); 35 | res.send_text("Yep Chained middlewares"); 36 | next!() 37 | } -------------------------------------------------------------------------------- /examples/middleware/src/middleware.rs: -------------------------------------------------------------------------------- 1 | use feather::{middleware::Middleware, next}; 2 | 3 | // Middlewares can be any type that implements the `Middleware` Trait 4 | pub struct MyMiddleware(pub String); 5 | 6 | // All middlewares have access to the app context and request/response objects 7 | impl Middleware for MyMiddleware { 8 | fn handle( 9 | &self, 10 | _request: &mut feather::Request, 11 | _response: &mut feather::Response, 12 | _ctx: &mut feather::AppContext, 13 | ) -> feather::Outcome { 14 | // Structs also have the `self` parameter but its behind a non mutuable referance so you cant just mutate the struct 15 | println!("Hii I am a Struct Middleware and this is my data: {}",self.0); 16 | 17 | next!() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/serve/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serve" 3 | version = "0.0.0" 4 | edition = "2024" 5 | publish = false 6 | 7 | [dependencies] 8 | feather = { workspace = true } 9 | -------------------------------------------------------------------------------- /examples/serve/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Feather Document 7 | 21 | 22 |

Hey Mate I am a Static file Served by Feather

23 |

Here's some lorem for you

24 |

25 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. 26 | Voluptas temporibus odit excepturi dicta recusandae, 27 | dolorum suscipit totam cupiditate minus, ipsum neque 28 | consequuntur cumque quas et voluptates, harum eligendi quo aut praesentium 29 | quae officiis? Ab laboriosam temporibus vitae, soluta quibusdam ipsam. 30 |

31 | 32 | -------------------------------------------------------------------------------- /examples/serve/src/main.rs: -------------------------------------------------------------------------------- 1 | use feather::middleware::builtins::ServeStatic; 2 | use feather::{next, App, AppContext, Request, Response}; 3 | // To Use this example you need to have a 'public' directory with some static files in it 4 | // in the same directory as this file. 5 | // This example shows how to use the ServeStatic middleware to serve static files from a directory. 6 | fn main() { 7 | // Create a new instance of App 8 | let mut app = App::new(); 9 | // Define a route for the root path 10 | app.get( 11 | "/", 12 | |_req: &mut Request, res: &mut Response, _ctx: &mut AppContext| { 13 | res.send_text("Hello, world!"); 14 | next!() 15 | }, 16 | ); 17 | // Use the ServeStatic middleware to serve static files from the "public" directory 18 | app.use_middleware(ServeStatic::new("./public".to_string())); // You can change the path to your static files here 19 | 20 | //Lets Listen on port 8080 21 | app.listen("127.0.0.1:8080"); 22 | } 23 | --------------------------------------------------------------------------------