After wasting a couple days trying to wrangle Axum, Hyper, Rocket and a couple others I tried Poem and it solves every problem I had with the other frameworks. Waiting for the other shoe to drop.
1. The response object type isn't generic (as per project goals). In other frameworks, if I had branches returning different response types the types would conflict - no such issue here.
2. The cookie middleware is easy to use. In axum you had to return the cookie jar from your handler and trust the response tuple magic to do something with it to set the cookie in the response. I wasn't using tuples though, and due to the above it was non-obvious (and awkward) how to set the cookie in the response using response objects.
3. When you enable TLS via rustls it automatically makes non-default config changes to negotiate http2 (this is boilerplate in other frameworks).
4. Endpoint handlers are non-closures, and state must be shared via extensions/middleware (not captures). Rust closures are a hornet's nest so I'm plenty happy just being able to avoid them, but I also like that there's a single way to do things (and that way has good DX). In other frameworks you need to use async closures in order to get the compiler to intuit the future type since they're complex and you'd have to define it manually with a non-closure.
5. TLS configs are a stream to handle live cert refreshes.
It feels like a batteries included/one size fits all solution, but it's very well integrated and there are a lot of batteries. It's pretty locked down, where you can't easily tweak the internals. I haven't hit limitations from that yet, despite having some somewhat weird use cases.
There are also a lot of examples in the repo.
I was fighting with other frameworks for the last 3 days (a good 20+ hours), and when I hit another future-handler-generic-incompatibility error I threw my hands up and tried Poem, and got my code compiling in 30m.
Does anyone else think generics detract from the usefulness of Rust docs? I often hit walls on learning libs when trying to figure out what type to use when the API uses a generic. A non-generic flow might be like this: fn accepts param y. Param y is of type z. Docs for z shows it constructed with z::new(), or z { (fields }. When it's a generic instead, there's no link to click, and you're on your own to figure it out, possibly by browsing source code.
This is exacerbated by the same libs using minimal examples that don't show the code used in a function, struct etc where explicit types are required.
I find that rust docs are entirely unhelpful for learning a library if they don’t come with plenty of realistic examples. I find the docs are just good for reference while you already generally know what you’re doing.
Yeah, I feel like learning the conventions is 90% of the battle especially with Rust. Otherwise you're playing a puzzle trying to figure out which combination of traits and objects will solve all the constraints.
I started a Rust project last year and found the documentation pretty unhelpful for a beginner. I think Java is one that (depending on the part of the standard library) can lack any decent examples. Python and PHP's documentation generally seem to have good examples though (at least in my experience).
They look too simple to be useful for learning the basics. This is common with rust libs. An example of a simple, practical project demonstrating structure would be more valuable than all of them.
> Shame you didn't try ActixWeb. That's the one I'd start with -- I heard a lot of praise for it but haven't tried it yet.
actix_web is what I go for as well, really simple to setup and haven't hit any major blockers for as long as I've been using it (~7 months, 3-4 projects)
Just to be clear, I didn't mean to be critical of Axum here. I wanted a simple web server I could get going quickly with, and I'm not (overly) concerned with maximum performance or flexibility, etc. I think this may differentiate my use case with the typical Axum use case.
Can't talk about hyper and rocket, but I use Axum extensively.
1. This one is annoying, but easily solvable by making your own return type that implements IntoResponse
2. I'm not sure what you're talking about? You just add `Cookies` extractor to your handler and use that. You don't need to add it to the return, middleware takes care of it.
Middleware in axum split into two: Tower middleware and Axum middleware. Tower middleware is...hard to write, but it's faster, axum middleware is easier to write, but it's slower.
4. axum doesn't force using closures, and state is shared via extensions?
I'm currently working on a service where I have tonic (gRPC) and axum on the same port, and they share a lot of middleware.
> I'm not sure what you're talking about? You just add `Cookies` extractor to your handler and use that. You don't need to add it to the return, middleware takes care of it.
I think you're thinking of tower-cookies which works the same way poem's CookieJarManager does.
axum-extra's cookies work differently in that you have to return the updated jar from handlers. We chose this design because it composes better when you have multiple libraries that all wanna set cookies. tower-cookies doesn't work if you accidentally add multiple cookie middleware, whereas axum-extra's approach does. So its a trade-off.
I was trying to return `Response` directly, which reading the above one would assume is the end-all be-all of `IntoResponse`. But `Response` is generic and you need to harmonize Body types across branches, etc...
Anyways implementing a custom `IntoResponse` shouldn't be necessary in the first 30m of using a framework (not saying I didn't just miss something, but I've read the docs multiple times).
Ah thanks! So `axum::response::Response` which is `http::Response<UnsyncBoxBody<Bytes, Error>>` is the fallback/catch-all response type, basically.
I actually read that, but I didn't realize that it was a specific type and my IDE auto-probably imported the wrong one. There's a lot of types named `Response` and with how common re-exports are I probably wouldn't have noticed unless I was expecting it. And since they're all part of the same type tree you aren't going to get clear error messages from the compiler either.
Naming it something like `DefaultResponse` or `AxumResponse` or calling it out explicitly by changing
> Use Response for more low level control:
to
> The above all eventually gets turned into axum::response::Response (a specialization of `http::Response`), which you can instantiate directly for more low level control:
would help immensely.
Edit: No, reading more closely `Response` has a default
body of `UnsyncBoxBody<Bytes, Error>` but other than that it's still generic and identical to `http::Response`? That's not going to help if you're having generic issues unless you explicitly convert things to `Bytes` AFAICT, and a default parameter is hardly obvious documentation.
axum's Response is an alias for `http::Response<UnsyncBoxBody<Bytes, Error>>`. It's not a fallback or catch-all, it's the type that axum expects to be returned by handler directly or indirectly via `IntoResponse` trait. All of that is very clear if you open : https://docs.rs/axum/latest/axum/index.html
Look at `IntoResponse` trait, you will see that it's the type you need to return. The difference between `impl IntoResponse`, Some type that implements `IntoResponse` and `http::Response<UnsyncBoxBody<Bytes, Error>>` is where conversion happens: explicitly by your handler or by axum implicitly.
You don't have to "explicitly convert to Bytes", you call `into_response` on whatever implements it.
Ah shoot, poem's websocket implementation uses tokio_tungstenite like most of the other hyper-adjacent Rust web frameworks. It's unfortunate that library has become the default for websockets because the performance is rather lacking. (new allocations for each incoming message IIRC)
soketto has a hyper example. I haven't found a websocket benchmark that compares them, but you can see here that soketto let's you re-use a buffer:
https://docs.rs/soketto/latest/src/soketto/connection.rs.htm...
It's on my to-do list to try soketto, but the thought of benchmarking websocket implementations sounds tedious.
soketto has a hyper example. I haven't found a websocket benchmark that compares them, but you can see here that soketto let's you re-use a buffer: https://docs.rs/soketto/latest/src/soketto/connection.rs.htm... It's on my to-do list to try soketto, but the thought of benchmarking websocket implementations sounds tedious.
The author of poem is not a native English speaker so the documentation is not extensive. However, the examples in the repo are superb, so treat those as the primary documentation.
He's also responsive to GitHub issues.
I love the native openapi integration. Its also convenient to exercise your API ad hoc.
Yeah, I intended to post this as a text submission and link poem, but the submission form says you can add both text + a url so I did that and this is how it ended up. In retrospect maybe I should have just added the link to the text body.
Is this practical for web programming? In the domains I'm used to (embedded and graphics programming), this isn't feasible. I recall some Actix drama re `unsafe` that this tag presumably alludes to. Perhaps web programming is abstract enough where this is a reasonable goal (?)
Unrelated: What have y'all been using these minimal Rust web frameworks for? I've found them too sparse for websites, eg missing automatic DB migrations, auth, admin, email etc. On the plus side, Serde and Chrono are both things that apply to web programming and are (IMO) best-in-class.
Most frameworks in rust a like Sinatra and Flask. There aren't any Rails level frameworks. I don't think there are any Rails level frameworks besides Django and a few JVM frameworks.
Rust web frameworks just give you tools to get from request to response, the rest is up to you. I think that is fine, yeah it leads to some boilerplate to wire up a few libraries together, but you did it once, and you're done.
Does it look like anything close to Ruby on Rails in terms of tooling? No, because Rust on Nails doesn't provide any tools at all. It literally doesn't take care of anything, but provides some (questionable) decisions made for you.
Unrelated to this project, but I dislike the obsession of "unsafe" within the rust community.
Sometimes I need to dereference a raw pointer (rare!).
Sometimes I actually know what I'm doing (very rare!!).
Sometimes I rigorously tested my code (exceptionally rare!!!).
When I see people making PRs (to e.g. Actix) to change unsafe code to safe code in an API the user *never* sees, which results in a performance penalty, just for the sake of not using the word "unsafe" in the code, I get mad. I totally understood Nikolay's reaction back then. Random people opened PRs and flamed him without knowing anything about the internals and the consequences.
The unsafe keyword means that I know what I'm doing. Just trust me for once, please.
Edit: if you actually want to know what you're doing too, I recommend you writing some linked lists. I hate linked lists with passion, I think they are a bad data structure and you should use Vectors 90% of the time and VecDeque the other 10% of cases. But they help you to understand what you're spending your electricity on.
Why should I? Trusting random people is exactly why C(++) libraries are under constant attack through use-after-free and buffer overflow exploits. You can use `unsafe` in your code just fine, but don't expect others to just trust that you know what you're doing. There's no clear way to distinguish an expert in ownership and multithreading semantics from someone who copy-pasted their unsafe code from Stackoverflow.
I trust libraries that don't use `unsafe` more than I trust libraries that say they know what they're doing. It's nothing personal, it's just a preference for the type of bugs and vulnerabilities I'd like to avoid if I can.
As for whether the user sees it or not, that's irrelevant. The library can be buggy and I would never know. I'd rather have the borrow checker verify that the code isn't buggy than take your word for it. I know the borrow checker isn't perfect and I know there are good reasons why one would use `unsafe` in their code, but if possible I'd like the code I (re)use to be as safe as possible.
Actix is a library that very loudly proclaims "trust me, I know what I'm doing". Some people believe the authors, I prefer to use safer alternatives at the cost of minor performance penalties. Power to you if you disagree, but that's your choice and opinion as much as the authors' of libraries.
I don't think writing linked lists is enough to learn how to use `unsafe` code. You'd have to write multithreaded linked list at the very least to get an understanding of why safe Rust code has all of these limitations. Even then you may never encounter race conditions when you run your code but at least it's a start.
I, for one, know that I'm not capable enough a Rust programmer to write well-tested, provably correct, multithreaded pointer magic code for performance optimization and I don't care enough to learn that art at the moment. If I were to publish a Rust crate, I'd much prefer the code to be at a level I can trust myself to maintain, which means no unsafe code. You may be better versed in the necessary semantics than I am but as a library owner I'd need to be able to maintain your code if you create a PR for my library which means you'll have to dumb down your unsafe code for me, sorry.
The problem is, do people know what they are doing?
I didn't follow the whole Actix situation carefully, but here is a discussion where someone found of 15 ways to trigger undefined behaviour in safe code, caused by the unsafes in Actix:
Personally, I'd take halving the speed of my project to reduce the possibility of remote security holes. We live in a dangerous world nowadays, and we should take every chance to minimise the risk of serious security issues.
What does it matter if a user never interacts with that API or not?
Rust is focused around -safety- and performance. I would rather have a slight performance hit and safe code, rather than trusting some random person to 100% correctly write unsafe code. Which is why tools like cargo-audit and cargo-geiger exist. IIRC Nikolay didn't communicate well about -why- unsafe was used, and just closed PRs that converted unsafe code to safe code.
> The unsafe keyword means that I know what I'm doing. Just trust me for once, please.
No, it means you think you know what you're doing.
It's more likely that you don't know what you're doing and/or are unnecessarily invoking unsafe for convenience, than the opposite. Theoretically I can look at your code and see if it's correct... or I could just use projects that don't use unsafe at all and save the time/headache.
When it comes to web server frameworks and security, I would like to see as little unsafe usage as possible, and documentation as to exactly why it's needed. Which is why people switched to Warp/Tower and now Axum which forbids unsafe code entirely.
If all I cared about were eking out all performance at the cost of safety, I wouldn't be using Rust in the first place.
I think the different philosophies you see re `unsafe` may be due to 2 related use-case pairs that both come up here:
#1: Low level vice applications programming. In the former, unsafe is a regular part of (at least certain layers) of code; ie you're working with memory (MMIO etc) as core operations, so will need `unsafe`. The situation gets ambiguous for things like peripheral typestates and owned singletons for register blocks etc; the line is blurred about what you're using the ownership model for, and what APIs should be marked as `unsafe`. For higher level uses like desktop programs and web servers, you may not need any `unsafe`.
#2: Libraries vice programs
This is directly related to your main point: If using someone else's code as a dependency, unsafe can be a liability if you don't know why it's is used. This is one aspect of the broader topic of whether you can/should trust any given dependency, and balancing not re-inventing wheels with learning library quirks, edge-cases, subtle bugs, complexity etc. A spin on this is making infrastructure specifically; I think Actix's creators and users may have had different opinions on this.
It's all context-dependent. You're right that people shouldn't just drop into a project they don't understand and demand that all unsafes be factored out, but just because an unsafe block is internal and carefully vetted that doesn't mean it's totally fine and chill either.
1. The response object type isn't generic (as per project goals). In other frameworks, if I had branches returning different response types the types would conflict - no such issue here.
2. The cookie middleware is easy to use. In axum you had to return the cookie jar from your handler and trust the response tuple magic to do something with it to set the cookie in the response. I wasn't using tuples though, and due to the above it was non-obvious (and awkward) how to set the cookie in the response using response objects.
3. When you enable TLS via rustls it automatically makes non-default config changes to negotiate http2 (this is boilerplate in other frameworks).
4. Endpoint handlers are non-closures, and state must be shared via extensions/middleware (not captures). Rust closures are a hornet's nest so I'm plenty happy just being able to avoid them, but I also like that there's a single way to do things (and that way has good DX). In other frameworks you need to use async closures in order to get the compiler to intuit the future type since they're complex and you'd have to define it manually with a non-closure.
5. TLS configs are a stream to handle live cert refreshes.
It feels like a batteries included/one size fits all solution, but it's very well integrated and there are a lot of batteries. It's pretty locked down, where you can't easily tweak the internals. I haven't hit limitations from that yet, despite having some somewhat weird use cases.
There are also a lot of examples in the repo.
I was fighting with other frameworks for the last 3 days (a good 20+ hours), and when I hit another future-handler-generic-incompatibility error I threw my hands up and tried Poem, and got my code compiling in 30m.