5 ms·
I have a handful of services, some in Warp and some in Rocket, and I dislike both of those frameworks. I've been looking into axum so this is a nice read. Hone
by zemo 4y ago
I have a handful of services, some in Warp and some in Rocket, and I dislike both of those frameworks. I've been looking into axum so this is a nice read.
Honestly I don't think that Axum is right either. So for example, this:
async fn create_user(
Json(payload): Json<CreateUser>,
) -> impl IntoResponse {
let user = User {
id: 1337,
username: payload.username,
};
(StatusCode::CREATED, Json(user))
}
For context, if you haven't checked out Axum, this is from the Axum docs.
Rocket has a similar thing with its request guards, it has a similar json type that you put into the signature of a handler function and it automatically plucks the value from the body and parses it as json.
What's weird to me is that it's coupling the request and response format to the logic. What if a client wants to post this as a form body? Write a separate endpoint? What if some old clients put the arguments in the query string? When I see this snippet as one of the intro examples for Axum, it feels like a red flag to me.
- Kinrany 4y agoA perfect API would consist of a few parts: 1. A request type and an associated response type 2. Impls for converting an HTTP request into the request type, and the reverse for response 3. A server type with an impl for handling the request 4. A client type with an impl for sending the request The remaining challenge is making all this ergonomic for the simple cases.
- xena 4y agoFun fact: you just described Go's net/http package.
- metadaemon 4y agoIt's also pretty much Express.js
- zemo 4y agonot really, point 2 isn't in the standard library.
- xena 4y agohttps://pkg.go.dev/net/http?utm_source=godoc#ReadRequest https://pkg.go.dev/net/http?utm_source=godoc#ReadRequest
- zemo 4y agothat’s not what the Axum example does. The function you’re linking turns an opaque run of bytes into an http request object. What the Axum example does is turn an http request object into a value of some other type T.
- masklinn 4y agoThat's essentially what happens in Rust, but it's a layer of crates.
- zemo 4y ago1, 3, and 4 are already there, they're just a part of Hyper, not Axum/Warp/Rocket. 2 is basically the thing that Axum/Warp/Rocket provide. Hyper is kinda like net/http and Axum/Warp/Rocket are more feature rich. The thing is, they're super early. They don't remind me of the pared-down quality simplicity of the Gorilla Toolkit or even the feature paradise of Gin. Honestly it a lot of the Rust http frameworks strike me as eerily similar to either Falcore or Revel. Falcore was a very early Go http application framework built by ngmoco, a now-defunct game company. Falcore didn't really gain a lot of traction, partially because it provided abstractions that weren't very ergonomic. It's whole thing was that the core abstraction was a modular pipeline. https://github.com/ngmoco/falcore https://github.com/ngmoco/falcore I think most people know Revel, it's a little less obscure. It's philosophically the precursor to Gin.
- Kinrany 4y agoThe frameworks provide 2 by hiding 1. This makes it impossible to use the request and response types for other purposes.
- dman-os 4y agoThe examples showcased by Axum and co. are the "ergonomic simple cases" and it's easy to morph what's provided into any flavor you personally prefer with as many `impl`s and types. Here's[0] my jam rn. [0]: https://github.com/dman-os/template_rust_web_api/blob/main/src/user/create.rs https://github.com/dman-os/template_rust_web_api/blob/main/s...
- nerdponx 4y agoThe Python framework FastAPI does this too. I think it does so because it's convenient, easy, and it's one less line of boilerplate. You trade off flexibility for having a very clean and simple "happy path".
- masklinn 4y ago> You trade off flexibility for having a very clean and simple "happy path". You don't trade anything though, if you want the raw information you can just ask for that.
- jamincan 4y agoYou don't have to specify the type in the signature; you can just as easily parse the request body manually. But in the instance where the endpoint only accepts json, it's simpler to write it this way.
- zemo 4y agohttps://news.ycombinator.com/item?id=33721070 https://news.ycombinator.com/item?id=33721070 same line of reasoning as here
- masklinn 4y ago> What's weird to me is that it's coupling the request and response format to the logic. It does not though? It lets you do it for your personal convenience. If you define a JSON API, you can just tell the framework that it takes JSON data, and it'll do the deserialisation for you. > What if a client wants to post this as a form body? Write a separate endpoint? What if some old clients put the arguments in the query string? Take a raw body and query strings and do the dispatching internally. Hell, you can ask for the request (https://docs.rs/http/latest/http/request/struct.Request.html https://docs.rs/http/latest/http/request/struct.Request.html) itself: async fn handler(request: Request<Body>) { // ... } There you go, knock yourself out. I think Warp would actually let you write different handlers for each of those cases, because it routes on the entire thing e.g. let some_route = path!("foo" / usize / "bar"); some_route.and(json()).map(handler_json) .or(some_route.and(form()).map(handler_form) .or(some_route.and(query()).map(handler_query) or you could tell it to unify these three filters and pass the data from whatever source it got to the same handler: path!("foo" / usize / "bar").and( json().or(form()).or(query()) ).map(handler) I'm sure this wouldn't work as-is and would require some tuning up or `unify()` calls, but you get the gist. IIRC Axum only routes on the URL, so it can't do that, that's both why it doesn't build types as giantic as warp, and why you have to specify the extractors in the function where warp doesn't need that (you'd just tell it that `payload: CreateUser).
- zemo 4y agosure you can get the underlying request, but if that’s the answer to everything that the framework author didn’t think of, that’s just an admission that the abstraction is wrong, which is kinda what I’m getting at. The entire conceptual model that views the Json<T> type as a handler argument type and then parsing the body based off of that is what Rocket does too. I think the entire strategy is conceptually incorrect. Axum may do Rocket better than Rocket, but if it’s using the same conceptual model, it seems like a lateral move. I’m looking for a new abstraction and conceptual model, not a better implementation of the same concepts or the same concepts with a larger pool of maintainers.
- infogulch 4y ago
- elysian-breeze 4y agoYou can just have `Request` as a parameter and do those edge cases yourself. Or write your own Extractor which would handle that pretty easily. I'm not sure of your use case where clients can send any format they want and the HTTP server is supposed to know and handle any format automatically (form, json, querystring, etc), but seems more like a legacy edge case than something you would do building a server from scratch. Something like this (completely untested) but pretty straight forward to handle your usecase. #[derive(Debug, Clone, Copy, Default)] #[cfg_attr(docsrs, doc(cfg(feature = "json")))] pub struct FormOrJson<T>(pub T); #[async_trait] impl<T, S, B> FromRequest<S, B> for FormOrJson<T> where T: DeserializeOwned, B: HttpBody + Send + 'static, B::Data: Send, B::Error: Into<BoxError>, S: Send + Sync, { type Rejection = InvalidFormOrJson; async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> { if json_content_type(req.headers()) { let bytes = Bytes::from_request(req, state).await?; let deserializer = &mut serde_json::Deserializer::from_slice(&bytes); let value = match serde_path_to_error::deserialize(deserializer) { Ok(value) => value, Err(err) => { let rejection = match err.inner().classify() { serde_json::error::Category::Data => JsonDataError::from_err(err).into(), serde_json::error::Category::Syntax | serde_json::error::Category::Eof => { JsonSyntaxError::from_err(err).into() } serde_json::error::Category::Io => { if cfg!(debug_assertions) { // we don't use `serde_json::from_reader` and instead always buffer // bodies first, so we shouldn't encounter any IO errors unreachable!() } else { JsonSyntaxError::from_err(err).into() } } }; return Err(rejection); } }; Ok(FormOrJson(value)) } else if has_content_type(req, &mime::APPLICATION_WWW_FORM_URLENCODED) { let bytes = Bytes::from_request(req).await?; let value = serde_urlencoded::from_bytes(&bytes) .map_err(FailedToDeserializeQueryString::__private_new::<(), _>)?; Ok(FormOrJson(value)) } else { Err(InvalidFormOrJson.into()) } } }
- dman-os 4y agoI'm curious, have you attempted and found difficulties writing an extractor similar to the decoder you describe?