Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"d9611b395b9df5aeaaec4281edcab7f80f38634c87515ae40558c2e816ccda3e","CHANGELOG.md":"88e8c02262c8ab764d362f2d8467fde8dd78e84ed68431810d93c2c813aaa14e","Cargo.lock":"138c5a686b165d6f9bb84f0000fa9123b8d53fdeff55d343ad217a14b2d70a06","Cargo.toml":"f2c03167006e53ca56f86052b67fdb2bd2962e6c75ad7d6e5f81c0e764e8b21f","Cargo.toml.orig":"62a4d66388094cc02dce32c3fd985f5b740c0152f4946c0a5f04e1cca3bf6390","LICENSE":"2c417b11aaf663ec7cbc8c8127afd748bfbb4de78df864395866897daaca71fa","README.md":"268929d5d6b5ca59c86a9bfc899e9b94686c6ccb2d6c6b264931db2e17392a37","src/body/async_read_body.rs":"e69f8ca167a21c99c6d2d0f66f8b25325fe2aa210e7aded900f5553e7a19ad36","src/body/mod.rs":"5473bdeb5046c3a6e5dd484fe867f7fc6332b01fbd5ed2f150ab1b4089311476","src/either.rs":"37d8750602b7bb7493663474af3cc125e93461e8ddf6637f486b7dc5b29adbc6","src/extract/cached.rs":"0aae82b35087c126e8e98045a5baa4161ea808a3914fd21507cb3d73b97d9fc3","src/extract/cookie/mod.rs":"597b741ed092a67c8d8c0ede058cfb7cae9cdeb53814317ebdad09bc388a9dc7","src/extract/cookie/private.rs":"64e5050d601bc4c0262da8862ee28a176169f85328ae04284bfc09b31c4a2cd4","src/extract/cookie/signed.rs":"4056bdf591fff96a396aced693bc0c630e69fb15f49c27d964c2932d9e3e8ce1","src/extract/form.rs":"f6c63bcdb1a9c293061cf047c1e807ae5aeb04969c91dafd179fea55d6dd51b6","src/extract/host.rs":"173be49494168b36f1349b701e67575d663a73689e058e54796d78d5e7f167ff","src/extract/json_deserializer.rs":"9a06bebc30786e0d56927f4ee58d7b995e7617750086fc4545e99df55f204987","src/extract/mod.rs":"3f86cca538b1705e96945d4d099b2e481c64741ecdebf62639fbe7379bf898c4","src/extract/multipart.rs":"5fcdb8730757f7fff90993d55e64b1618e165616a70b88dfd1ede9e337bbf8e8","src/extract/optional_path.rs":"4468f779dbd198557676d38e60a91c63fdee100c109a58254014285042ea0d9e","src/extract/query.rs":"7baba7c10c882b250cee30dab479238d5f41eb8ef50440c5c7572e1913c7bd0a","src/extract/rejection.rs":"d41e871842423a2e8e16569527764e79739d4882e4d70d7a83d268a4ca3021eb","src/extract/scheme.rs":"fe46c9a168fb4cfa72caa675448fee2192ae77c9eddff4febd2da7b94d8933fc","src/extract/with_rejection.rs":"dc63b3b5233b8d1d11ebaefab108a7eda4deca4094c33d0c4a3af130350bb0f9","src/handler/mod.rs":"bb680b50135b57292e1463fe2edd14ef1cfc696da4657bb2819063d13496bee3","src/handler/or.rs":"f4920c676464eb53f4c7ee19e53830470ce1af11413c179739af6eeb0673b618","src/json_lines.rs":"047cec6046b62b1791246d10d1f6a370ac4c95b26ec677a76922675e84100044","src/lib.rs":"5d552fb8bb411428e676201260e4b32cce24f5db886ad3b6d3f42b174eec3c1c","src/middleware.rs":"df9435942590ec93521b697e1bafe188ebde6f104b61efb27b7e7237a2e4aa56","src/protobuf.rs":"c993863656bf4e2424a8d6e6a27221d70851c401bd9896844ec94e77e2e7a5d3","src/response/attachment.rs":"bbbbf6a0bd5ca217a18911dd77dafb0b030961c2bdc4ddb848d582ee9c562dd5","src/response/erased_json.rs":"62473b5237884c2e40a0c91758b3615c3fce52ab5b219f6b05b1a683627bef62","src/response/error_response.rs":"9b77ac98871e3dc022c190ab62e091964db8fc4ea6b793a09e1564d3a4e4d513","src/response/file_stream.rs":"e410b8f26d461b5b8d146b9c367e92e818ec9adc263cc80b5531e986464192e2","src/response/mod.rs":"9ad531dddb47a82bb267cd3b8ceb685ec548f5827ff47eb470e378ef050788e0","src/response/multiple.rs":"424575720e47a4d7ae8323cce0d406119776948ab3aba1c5c71cbfd29bb65797","src/routing/mod.rs":"ee121fe45c3be3f3a222e25786a2e5f64a3b02a7ea8163c0e2a54abb9247e131","src/routing/resource.rs":"608cad2eed45c78847b71fe5d68151eb13aea342ce32a8203d605f3f4d9caf79","src/routing/typed.rs":"323f7c537cfe00e3fb67e92abf478bc518bd62f113b701ed6b298199a45bdcfa","src/typed_header.rs":"bf32d91cc69f5102e179046198f505b67065a151e75f8724cd256acd689add8d","test_files/index.html":"77df23536cfaa069d161e80620265c91f3f7f27c881463392794dc88fc2e49af","test_files/index_2.html":"f940066bde160e8b06a6f1281fdf85fccc24d430f3608908b5d1b9243016565e","test_files/script.js":"be3a2694e60e8af988979f0dd5559e9f2ad42b22a705fe85e4562bd86763594a"},"package":"9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "c1bb9c3e90625fb8de4f9c9750f669cb4edac3df"
},
"path_in_vcs": "axum-extra"
}
+491
View File
@@ -0,0 +1,491 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog],
and this project adheres to [Semantic Versioning].
# 0.11.0
Yanked from crates.io due to unforeseen breaking change, see [#3190] for details.
[#3190]: https://github.com/tokio-rs/axum/pull/3190
# 0.10.3
Released without changes to fix docs.rs build.
# 0.10.2
- **added:** Implement `OptionalFromRequest` for `Host` ([#3177])
[#3177]: https://github.com/tokio-rs/axum/pull/3177
# 0.10.1
- **fixed:** Fix a broken link in the documentation of `ErasedJson` ([#3186])
- **added:** Add `vpath!` for compile time path verification on static paths. ([#3288])
[#3186]: https://github.com/tokio-rs/axum/pull/3186
[#3288]: https://github.com/tokio-rs/axum/pull/3288
# 0.10.0
## since rc.1
<details>
- **breaking:** Remove `OptionalFromRequestParts` impl for `Query` ([#3088])
- **changed:** Query/Form: Use `serde_path_to_error` to report fields that failed to parse ([#3081])
[#3088]: https://github.com/tokio-rs/axum/pull/3088
</details>
## full changelog
- **breaking:** Update to prost 0.13. Used for the `Protobuf` extractor ([#2829])
- **changed:** Update minimum rust version to 1.75 ([#2943])
- **changed:** Deprecated `OptionalPath<T>` ([#2475])
- **changed:** Query/Form: Use `serde_path_to_error` to report fields that failed to parse ([#3081])
- **changed:** The `multipart` feature is no longer on by default ([#3058])
- **fixed:** `Host` extractor includes port number when parsing authority ([#2242])
- **added:** Add `RouterExt::typed_connect` ([#2961])
- **added:** Add `json!` for easy construction of JSON responses ([#2962])
- **added:** Add `InternalServerError` response for logging an internal error
and returning HTTP 500 in a convenient way. ([#3010])
- **added:** Add `FileStream` for easy construction of file stream responses ([#3047])
- **added:** Add `Scheme` extractor ([#2507])
[#3081]: https://github.com/tokio-rs/axum/pull/3081
## rc.1
- **breaking:** `Option<Query<T>>` no longer swallows all error conditions, instead rejecting the
request in many cases; see its documentation for details ([#2475])
- **changed:** Deprecated `OptionalPath<T>` and `OptionalQuery<T>` ([#2475])
- **fixed:** `Host` extractor includes port number when parsing authority ([#2242])
- **changed:** The `multipart` feature is no longer on by default ([#3058])
- **added:** Add `RouterExt::typed_connect` ([#2961])
- **added:** Add `json!` for easy construction of JSON responses ([#2962])
- **added:** Add `InternalServerError` response for logging an internal error
and returning HTTP 500 in a convenient way. ([#3010])
- **added:** Add `FileStream` for easy construction of file stream responses ([#3047])
- **added:** Add `Scheme` extractor ([#2507])
[#2242]: https://github.com/tokio-rs/axum/pull/2242
[#2475]: https://github.com/tokio-rs/axum/pull/2475
[#3058]: https://github.com/tokio-rs/axum/pull/3058
[#2961]: https://github.com/tokio-rs/axum/pull/2961
[#2962]: https://github.com/tokio-rs/axum/pull/2962
[#3010]: https://github.com/tokio-rs/axum/pull/3010
[#3047]: https://github.com/tokio-rs/axum/pull/3047
[#2507]: https://github.com/tokio-rs/axum/pull/2507
## alpha.1
- **breaking:** Update to prost 0.13. Used for the `Protobuf` extractor ([#2829])
- **change:** Update minimum rust version to 1.75 ([#2943])
[#2829]: https://github.com/tokio-rs/axum/pull/2829
[#2943]: https://github.com/tokio-rs/axum/pull/2943
# 0.9.6
- **docs:** Add links to features table ([#3030])
[#3030]: https://github.com/tokio-rs/axum/pull/3030
# 0.9.5
- **added:** Add `RouterExt::typed_connect` ([#2961])
- **added:** Add `json!` for easy construction of JSON responses ([#2962])
[#2961]: https://github.com/tokio-rs/axum/pull/2961
[#2962]: https://github.com/tokio-rs/axum/pull/2962
# 0.9.4
- **added:** The `response::Attachment` type ([#2789])
[#2789]: https://github.com/tokio-rs/axum/pull/2789
# 0.9.3 (24. March, 2024)
- **added:** New `tracing` feature which enables logging rejections from
built-in extractor with the `axum::rejection=trace` target ([#2584])
[#2584]: https://github.com/tokio-rs/axum/pull/2584
# 0.9.2 (13. January, 2024)
- **added:** Implement `TypedPath` for `WithRejection<TypedPath, _>`
- **fixed:** Documentation link to `serde::Deserialize` in `JsonDeserializer` extractor ([#2498])
- **added:** Add `is_missing` function for `TypedHeaderRejection` and `TypedHeaderRejectionReason` ([#2503])
[#2498]: https://github.com/tokio-rs/axum/pull/2498
[#2503]: https://github.com/tokio-rs/axum/pull/2503
# 0.9.1 (29. December, 2023)
- **change:** Update version of multer used internally for multipart ([#2433])
- **added:** `JsonDeserializer` extractor ([#2431])
[#2433]: https://github.com/tokio-rs/axum/pull/2433
[#2431]: https://github.com/tokio-rs/axum/pull/2431
# 0.9.0 (27. November, 2023)
- **added:** `OptionalQuery` extractor ([#2310])
- **added:** `TypedHeader` which used to be in `axum` ([#1850])
- **breaking:** Update to prost 0.12. Used for the `Protobuf` extractor
- **breaking:** Make `tokio` an optional dependency
- **breaking:** Upgrade `cookie` dependency to 0.18 ([#2343])
- **breaking:** Functions and methods that previously accepted a `Cookie`
now accept any `T: Into<Cookie>` ([#2348])
[#1850]: https://github.com/tokio-rs/axum/pull/1850
[#2310]: https://github.com/tokio-rs/axum/pull/2310
[#2343]: https://github.com/tokio-rs/axum/pull/2343
[#2348]: https://github.com/tokio-rs/axum/pull/2348
# 0.8.0 (16. September, 2023)
- **breaking:** Update to prost 0.12. Used for the `Protobuf` extractor ([#2224])
[#2224]: https://github.com/tokio-rs/axum/pull/2224
# 0.7.7 (03. August, 2023)
- **added:** `Clone` implementation for `ErasedJson` ([#2142])
[#2142]: https://github.com/tokio-rs/axum/pull/2142
# 0.7.6 (02. August, 2023)
- **fixed:** Remove unused dependency ([#2135])
[#2135]: https://github.com/tokio-rs/axum/pull/2135
# 0.7.5 (17. July, 2023)
- **fixed:** Remove explicit auto deref from `PrivateCookieJar` example ([#2028])
[#2028]: https://github.com/tokio-rs/axum/pull/2028
# 0.7.4 (18. April, 2023)
- **added:** Add `Html` response type ([#1921])
- **added:** Add `Css` response type ([#1921])
- **added:** Add `JavaScript` response type ([#1921])
- **added:** Add `Wasm` response type ([#1921])
[#1921]: https://github.com/tokio-rs/axum/pull/1921
# 0.7.3 (11. April, 2023)
- **added:** Implement `Deref` and `DerefMut` for built-in extractors ([#1922])
- **added:** Add `OptionalPath` extractor ([#1889])
[#1889]: https://github.com/tokio-rs/axum/pull/1889
[#1922]: https://github.com/tokio-rs/axum/pull/1922
# 0.7.2 (22. March, 2023)
- **added:** Implement `IntoResponse` for `MultipartError` ([#1861])
[#1861]: https://github.com/tokio-rs/axum/pull/1861
# 0.7.1 (13. March, 2023)
- Updated to latest `axum-macros`
# 0.7.0 (03. March, 2023)
- **breaking:** Remove the `spa` feature which should have been removed in 0.6.0 ([#1802])
- **added:** Add `Multipart`. This is similar to `axum::extract::Multipart`
except that it enforces field exclusivity at runtime instead of compile time,
as this improves usability ([#1692])
- **added:** Implement `Clone` for `CookieJar`, `PrivateCookieJar` and `SignedCookieJar` ([#1808])
- **fixed:** Add `#[must_use]` attributes to types that do nothing unless used ([#1809])
[#1692]: https://github.com/tokio-rs/axum/pull/1692
[#1802]: https://github.com/tokio-rs/axum/pull/1802
[#1808]: https://github.com/tokio-rs/axum/pull/1808
[#1809]: https://github.com/tokio-rs/axum/pull/1809
# 0.6.0 (24. February, 2022)
- **breaking:** Change casing of `ProtoBuf` to `Protobuf` ([#1595])
- **breaking:** `SpaRouter` has been removed. Use `ServeDir` and `ServeFile`
from `tower-http` instead:
```rust
// before
Router::new().merge(SpaRouter::new("/assets", "dist"));
// with ServeDir
Router::new().nest_service("/assets", ServeDir::new("dist"));
// before with `index_file`
Router::new().merge(SpaRouter::new("/assets", "dist").index_file("index.html"));
// with ServeDir + ServeFile
Router::new().nest_service(
"/assets",
ServeDir::new("dist").not_found_service(ServeFile::new("dist/index.html")),
);
```
See the [static-file-server-example] for more examples ([#1784])
[#1595]: https://github.com/tokio-rs/axum/pull/1595
[#1784]: https://github.com/tokio-rs/axum/pull/1784
[static-file-server-example]: https://github.com/tokio-rs/axum/blob/main/examples/static-file-server/src/main.rs
# 0.5.0 (12. February, 2022)
- **added:** Add `option_layer` for converting an `Option<Layer>` into a `Layer` ([#1696])
- **added:** Implement `Layer` and `Service` for `Either` ([#1696])
- **added:** Add `TypedPath::with_query_params` ([#1744])
- **breaking:** Update to [`cookie`] 0.17 ([#1747])
[#1696]: https://github.com/tokio-rs/axum/pull/1696
[#1744]: https://github.com/tokio-rs/axum/pull/1744
[#1747]: https://github.com/tokio-rs/axum/pull/1747
[`cookie`]: https://crates.io/crates/cookie
# 0.4.2 (02. December, 2022)
- **fixed:** Bug fixes for `RouterExt:{route_with_tsr, route_service_with_tsr}` ([#1608]):
- Redirects to the correct URI if the route contains path parameters
- Keeps query parameters when redirecting
- Better improved error message if adding route for `/`
[#1608]: https://github.com/tokio-rs/axum/pull/1608
# 0.4.1 (29. November, 2022)
- **fixed:** Fix wrong `From` impl for `Resource` ([#1589])
[#1589]: https://github.com/tokio-rs/axum/pull/1589
# 0.4.0 (25. November, 2022)
- **added:** Add `RouterExt::route_with_tsr` for adding routes with an
additional "trailing slash redirect" route ([#1119])
- **added:** Support chaining handlers with `HandlerCallWithExtractors::or` ([#1170])
- **added:** Add Protocol Buffer extractor and response ([#1239])
- **added:** Add `Either*` types for combining extractors and responses into a
single type ([#1263])
- **added:** `WithRejection` extractor for customizing other extractors' rejections ([#1262])
- **added:** Add sync constructors to `CookieJar`, `PrivateCookieJar`, and
`SignedCookieJar` so they're easier to use in custom middleware
- **changed:** For methods that accept some `S: Service`, the bounds have been
relaxed so the return type can be any type that implements `IntoResponse` rather than being a
literal `Response`
- **change:** axum-extra's MSRV is now 1.60 ([#1239])
- **breaking:** `Form` has a new rejection type ([#1496])
- **breaking:** `Query` has a new rejection type ([#1496])
- **breaking:** `Resource::nest` and `Resource::nest_collection` have been
removed. You can instead convert the `Resource` into a `Router` and
add additional routes as necessary ([#1086])
- **breaking:** `SignedCookieJar` and `PrivateCookieJar` now extracts the keys
from the router's state, rather than extensions
- **breaking:** `Resource` has a new `S` type param which represents the state ([#1155])
- **breaking:** `RouterExt::route_with_tsr` now only accepts `MethodRouter`s ([#1155])
- **added:** `RouterExt::route_service_with_tsr` for routing to any `Service` ([#1155])
[#1086]: https://github.com/tokio-rs/axum/pull/1086
[#1119]: https://github.com/tokio-rs/axum/pull/1119
[#1155]: https://github.com/tokio-rs/axum/pull/1155
[#1170]: https://github.com/tokio-rs/axum/pull/1170
[#1214]: https://github.com/tokio-rs/axum/pull/1214
[#1239]: https://github.com/tokio-rs/axum/pull/1239
[#1262]: https://github.com/tokio-rs/axum/pull/1262
[#1263]: https://github.com/tokio-rs/axum/pull/1263
[#1496]: https://github.com/tokio-rs/axum/pull/1496
<details>
<summary>0.4.0 Pre-Releases</summary>
# 0.4.0-rc.3 (19. November, 2022)
- **breaking:** Depend axum 0.6.0-rc.5 and axum-macros 0.3.0-rc.3
# 0.4.0-rc.2 (8. November, 2022)
- **breaking:** `Form` has a new rejection type ([#1496])
- **breaking:** `Query` has a new rejection type ([#1496])
[#1496]: https://github.com/tokio-rs/axum/pull/1496
# 0.4.0-rc.1 (23. August, 2022)
- **added:** Add `RouterExt::route_with_tsr` for adding routes with an
additional "trailing slash redirect" route ([#1119])
- **breaking:** `Resource::nest` and `Resource::nest_collection` has been
removed. You can instead convert the `Resource` into a `Router` and
add additional routes as necessary ([#1086])
- **changed:** For methods that accept some `S: Service`, the bounds have been
relaxed so the response type must implement `IntoResponse` rather than being a
literal `Response`
- **added:** Support chaining handlers with `HandlerCallWithExtractors::or` ([#1170])
- **change:** axum-extra's MSRV is now 1.60 ([#1239])
- **breaking:** `SignedCookieJar` and `PrivateCookieJar` now extracts the keys
from the router's state, rather than extensions
- **added:** Add Protocol Buffer extractor and response ([#1239])
- **added:** Add `Either*` types for combining extractors and responses into a
single type ([#1263])
- **added:** `WithRejection` extractor for customizing other extractors' rejections ([#1262])
- **added:** Add sync constructors to `CookieJar`, `PrivateCookieJar`, and
`SignedCookieJar` so they're easier to use in custom middleware
- **breaking:** `Resource` has a new `S` type param which represents the state ([#1155])
- **breaking:** `RouterExt::route_with_tsr` now only accepts `MethodRouter`s ([#1155])
- **added:** `RouterExt::route_service_with_tsr` for routing to any `Service` ([#1155])
[#1086]: https://github.com/tokio-rs/axum/pull/1086
[#1119]: https://github.com/tokio-rs/axum/pull/1119
[#1155]: https://github.com/tokio-rs/axum/pull/1155
[#1170]: https://github.com/tokio-rs/axum/pull/1170
[#1214]: https://github.com/tokio-rs/axum/pull/1214
[#1239]: https://github.com/tokio-rs/axum/pull/1239
[#1262]: https://github.com/tokio-rs/axum/pull/1262
[#1263]: https://github.com/tokio-rs/axum/pull/1263
</details>
# 0.3.7 (09. August, 2022)
- **fixed:** Depend on axum 0.5.15 which contains a fix for an accidental breaking change.
# 0.3.6 (02. July, 2022)
- **fixed:** Fix feature labels missing in generated docs ([#1137])
[#1137]: https://github.com/tokio-rs/axum/pull/1137
# 0.3.5 (27. June, 2022)
- **added:** Add `JsonLines` for streaming newline delimited JSON ([#1093])
- **change:** axum-extra's MSRV is now 1.56 ([#1098])
[#1093]: https://github.com/tokio-rs/axum/pull/1093
[#1098]: https://github.com/tokio-rs/axum/pull/1098
# 0.3.4 (08. June, 2022)
- **fixed:** Use `impl IntoResponse` less in docs ([#1049])
- **added:** Add `AsyncReadBody` for creating a body from a `tokio::io::AsyncRead` ([#1072])
[#1049]: https://github.com/tokio-rs/axum/pull/1049
[#1072]: https://github.com/tokio-rs/axum/pull/1072
# 0.3.3 (18. May, 2022)
- **added:** Add `extract::Query` which supports multi-value items ([#1041])
- **added:** Support customizing rejections for `#[derive(TypedPath)]` ([#1012])
[#1041]: https://github.com/tokio-rs/axum/pull/1041
[#1012]: https://github.com/tokio-rs/axum/pull/1012
# 0.3.2 (15. May, 2022)
- **added:** Add `extract::Form` which supports multi-value items ([#1031])
[#1031]: https://github.com/tokio-rs/axum/pull/1031
# 0.3.1 (10. May, 2022)
- **fixed:** `Option` and `Result` are now supported in typed path route handler parameters ([#1001])
- **fixed:** Support wildcards in typed paths ([#1003])
- **added:** Support using a custom rejection type for `#[derive(TypedPath)]`
instead of `PathRejection` ([#1012])
[#1001]: https://github.com/tokio-rs/axum/pull/1001
[#1003]: https://github.com/tokio-rs/axum/pull/1003
[#1012]: https://github.com/tokio-rs/axum/pull/1012
# 0.3.0 (27. April, 2022)
- **fixed:** Don't depend on axum with default features enabled ([#913])
- **breaking:** Private and signed cookies now requires enabling the
`cookie-private` and `cookie-signed` features respectively ([#949])
- **changed:** Update to tower-http 0.3 ([#965])
[#913]: https://github.com/tokio-rs/axum/pull/913
[#949]: https://github.com/tokio-rs/axum/pull/949
[#965]: https://github.com/tokio-rs/axum/pull/965
# 0.2.1 (03. April, 2022)
- **added:** Re-export `SameSite` and `Expiration` from the `cookie` crate ([#898])
- **added:** Add `PrivateCookieJar` for managing private cookies ([#900])
- **added:** Add `SpaRouter` for routing setups commonly used for single page applications ([#904])
- **fixed:** Fix `SignedCookieJar` when using custom key types ([#899])
[#898]: https://github.com/tokio-rs/axum/pull/898
[#899]: https://github.com/tokio-rs/axum/pull/899
[#900]: https://github.com/tokio-rs/axum/pull/900
[#904]: https://github.com/tokio-rs/axum/pull/904
# 0.2.0 (31. March, 2022)
- **added:** Add `TypedPath::to_uri` for converting the path into a `Uri` ([#790])
- **added:** Extractors and responses for dealing with cookies. See `extract::cookies` for more
details ([#816])
- **breaking:** `CachedRejection` has been removed ([#699])
- **breaking:** `<Cached<T> as FromRequest>::Rejection` is now `T::Rejection`. ([#699])
- **breaking:** `middleware::from_fn` has been remove from axum-extra and moved into the main
axum crate ([#719])
- **breaking:** `HasRoutes` has been removed. `Router::merge` now accepts `Into<Router>` ([#819])
- **breaking:** `RouterExt::with` method has been removed. Use `Router::merge` instead. It works
identically ([#819])
[#699]: https://github.com/tokio-rs/axum/pull/699
[#719]: https://github.com/tokio-rs/axum/pull/719
[#790]: https://github.com/tokio-rs/axum/pull/790
[#816]: https://github.com/tokio-rs/axum/pull/816
[#819]: https://github.com/tokio-rs/axum/pull/819
# 0.1.5 (1. March, 2022)
- **added:** Add `TypedPath::to_uri` for converting the path into a `Uri` ([#790])
[#790]: https://github.com/tokio-rs/axum/pull/790
# 0.1.4 (22. February, 2022)
- **fix:** Depend on the right versions of axum and axum-macros ([#782])
[#782]: https://github.com/tokio-rs/axum/pull/782
# 0.1.3 (22. February, 2022)
- **added:** Add type safe routing. See `axum_extra::routing::typed` for more details ([#756])
- **fix:** Depend on tower with `default_features = false` ([#666])
- **change:** `middleware::from_fn` has been deprecated and moved into the main
axum crate ([#719])
[#666]: https://github.com/tokio-rs/axum/pull/666
[#719]: https://github.com/tokio-rs/axum/pull/719
[#756]: https://github.com/tokio-rs/axum/pull/756
# 0.1.2 (13. January, 2022)
- **fix:** Depend on tower with `default_features = false` ([#666])
# 0.1.1 (27. December, 2021)
- Add `middleware::from_fn` for creating middleware from async functions ([#656])
- Add support for returning pretty JSON response in `response::ErasedJson` ([#662])
[#656]: https://github.com/tokio-rs/axum/pull/656
[#662]: https://github.com/tokio-rs/axum/pull/662
# 0.1.0 (02. December, 2021)
- Initial release.
[Keep a Changelog]: https://keepachangelog.com/en/1.0.0/
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
+1883
View File
File diff suppressed because it is too large Load Diff
+347
View File
@@ -0,0 +1,347 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.78"
name = "axum-extra"
version = "0.10.3"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Extra utilities for axum"
homepage = "https://github.com/tokio-rs/axum"
readme = "README.md"
keywords = [
"http",
"web",
"framework",
]
categories = [
"asynchronous",
"network-programming",
"web-programming",
]
license = "MIT"
repository = "https://github.com/tokio-rs/axum"
[package.metadata.docs.rs]
all-features = true
[package.metadata.cargo-public-api-crates]
allowed = [
"axum",
"axum_core",
"axum_macros",
"bytes",
"cookie",
"futures_core",
"futures_util",
"headers",
"headers_core",
"http",
"http_body",
"pin_project_lite",
"prost",
"serde_core",
"tokio",
"tokio_util",
"tower_layer",
"tower_service",
]
[features]
__private_docs = [
"axum/json",
"dep:serde",
"dep:tower",
]
async-read-body = [
"dep:tokio-util",
"tokio-util?/io",
"dep:tokio",
]
async-stream = []
attachment = ["dep:tracing"]
cookie = ["dep:cookie"]
cookie-key-expansion = [
"cookie",
"cookie?/key-expansion",
]
cookie-private = [
"cookie",
"cookie?/private",
]
cookie-signed = [
"cookie",
"cookie?/signed",
]
default = ["tracing"]
erased-json = [
"dep:serde_json",
"dep:typed-json",
]
error-response = [
"dep:tracing",
"tracing/std",
]
file-stream = [
"dep:tokio-util",
"tokio-util?/io",
"dep:tokio",
"tokio?/fs",
"tokio?/io-util",
]
form = [
"dep:form_urlencoded",
"dep:serde_html_form",
"dep:serde_path_to_error",
]
json-deserializer = [
"dep:serde_json",
"dep:serde_path_to_error",
]
json-lines = [
"dep:serde_json",
"dep:tokio-util",
"dep:tokio-stream",
"tokio-util?/io",
"tokio-stream?/io-util",
"dep:tokio",
]
multipart = [
"dep:multer",
"dep:fastrand",
]
protobuf = ["dep:prost"]
query = [
"dep:form_urlencoded",
"dep:serde_html_form",
"dep:serde_path_to_error",
]
scheme = []
tracing = [
"axum-core/tracing",
"axum/tracing",
"dep:tracing",
]
typed-header = ["dep:headers"]
typed-routing = [
"dep:axum-macros",
"dep:percent-encoding",
"dep:serde_html_form",
"dep:form_urlencoded",
]
[lib]
name = "axum_extra"
path = "src/lib.rs"
[dependencies.axum]
version = "0.8.6"
features = ["original-uri"]
default-features = false
[dependencies.axum-core]
version = "0.5.5"
[dependencies.axum-macros]
version = "0.5.0"
optional = true
[dependencies.bytes]
version = "1.1.0"
[dependencies.cookie]
version = "0.18.0"
features = ["percent-encode"]
optional = true
package = "cookie"
[dependencies.fastrand]
version = "2.1.0"
optional = true
[dependencies.form_urlencoded]
version = "1.1.0"
optional = true
[dependencies.futures-util]
version = "0.3"
features = ["alloc"]
default-features = false
[dependencies.headers]
version = "0.4.0"
optional = true
[dependencies.http]
version = "1.0.0"
[dependencies.http-body]
version = "1.0.0"
[dependencies.http-body-util]
version = "0.1.0"
[dependencies.mime]
version = "0.3"
[dependencies.multer]
version = "3.0.0"
optional = true
[dependencies.percent-encoding]
version = "2.1"
optional = true
[dependencies.pin-project-lite]
version = "0.2"
[dependencies.prost]
version = "0.13"
optional = true
[dependencies.rustversion]
version = "1.0.9"
[dependencies.serde]
version = "1.0.221"
optional = true
[dependencies.serde_core]
version = "1.0.221"
[dependencies.serde_html_form]
version = "0.2.0"
optional = true
[dependencies.serde_json]
version = "1.0.71"
optional = true
[dependencies.serde_path_to_error]
version = "0.1.8"
optional = true
[dependencies.tokio]
version = "1.19"
optional = true
[dependencies.tokio-stream]
version = "0.1.9"
optional = true
[dependencies.tokio-util]
version = "0.7"
optional = true
[dependencies.tower]
version = "0.5.2"
features = ["util"]
optional = true
default-features = false
[dependencies.tower-layer]
version = "0.3"
[dependencies.tower-service]
version = "0.3"
[dependencies.tracing]
version = "0.1.37"
optional = true
default-features = false
[dependencies.typed-json]
version = "0.1.1"
optional = true
[dev-dependencies.hyper]
version = "1.0.0"
[dev-dependencies.reqwest]
version = "0.12"
features = [
"json",
"stream",
"multipart",
]
default-features = false
[dev-dependencies.serde]
version = "1.0.221"
features = ["derive"]
[dev-dependencies.serde_json]
version = "1.0.71"
[dev-dependencies.tokio]
version = "1.14"
features = ["full"]
[dev-dependencies.tower]
version = "0.5.2"
features = ["util"]
[dev-dependencies.tower-http]
version = "0.6.0"
features = [
"map-response-body",
"timeout",
]
[dev-dependencies.tracing-subscriber]
version = "0.3.19"
[lints.clippy]
await_holding_lock = "warn"
dbg_macro = "warn"
empty_enum = "warn"
enum_glob_use = "warn"
exit = "warn"
filter_map_next = "warn"
fn_params_excessive_bools = "warn"
if_let_mutex = "warn"
imprecise_flops = "warn"
inefficient_to_string = "warn"
linkedlist = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
match_wildcard_for_single_variants = "warn"
mem_forget = "warn"
must_use_candidate = "warn"
needless_borrow = "warn"
needless_continue = "warn"
option_option = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_self_not_must_use = "warn"
str_to_string = "warn"
suboptimal_flops = "warn"
todo = "warn"
type_complexity = "allow"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
unused_self = "warn"
verbose_file_reads = "warn"
[lints.rust]
missing_debug_implementations = "warn"
missing_docs = "warn"
unreachable_pub = "warn"
unsafe_code = "forbid"
[lints.rust.rust_2018_idioms]
level = "warn"
priority = -1
+124
View File
@@ -0,0 +1,124 @@
[package]
categories = ["asynchronous", "network-programming", "web-programming"]
description = "Extra utilities for axum"
edition = "2021"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
license = "MIT"
name = "axum-extra"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.10.3"
[features]
default = ["tracing"]
async-read-body = ["dep:tokio-util", "tokio-util?/io", "dep:tokio"]
async-stream = [] # unused, remove before the next breaking-change release
file-stream = ["dep:tokio-util", "tokio-util?/io", "dep:tokio", "tokio?/fs", "tokio?/io-util"]
attachment = ["dep:tracing"]
error-response = ["dep:tracing", "tracing/std"]
cookie = ["dep:cookie"]
cookie-private = ["cookie", "cookie?/private"]
cookie-signed = ["cookie", "cookie?/signed"]
cookie-key-expansion = ["cookie", "cookie?/key-expansion"]
erased-json = ["dep:serde_json", "dep:typed-json"]
form = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"]
json-deserializer = ["dep:serde_json", "dep:serde_path_to_error"]
json-lines = [
"dep:serde_json",
"dep:tokio-util",
"dep:tokio-stream",
"tokio-util?/io",
"tokio-stream?/io-util",
"dep:tokio",
]
multipart = ["dep:multer", "dep:fastrand"]
protobuf = ["dep:prost"]
scheme = []
query = ["dep:form_urlencoded", "dep:serde_html_form", "dep:serde_path_to_error"]
tracing = ["axum-core/tracing", "axum/tracing", "dep:tracing"]
typed-header = ["dep:headers"]
typed-routing = ["dep:axum-macros", "dep:percent-encoding", "dep:serde_html_form", "dep:form_urlencoded"]
# Enabled by docs.rs because it uses all-features
# Enables upstream things linked to in docs
__private_docs = ["axum/json", "dep:serde", "dep:tower"]
[dependencies]
axum = { path = "../axum", version = "0.8.6", default-features = false, features = ["original-uri"] }
axum-core = { path = "../axum-core", version = "0.5.5" }
bytes = "1.1.0"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0"
http-body = "1.0.0"
http-body-util = "0.1.0"
mime = "0.3"
pin-project-lite = "0.2"
rustversion = "1.0.9"
serde_core = "1.0.221"
tower-layer = "0.3"
tower-service = "0.3"
# optional dependencies
axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true }
cookie = { package = "cookie", version = "0.18.0", features = ["percent-encode"], optional = true }
fastrand = { version = "2.1.0", optional = true }
form_urlencoded = { version = "1.1.0", optional = true }
headers = { version = "0.4.0", optional = true }
multer = { version = "3.0.0", optional = true }
percent-encoding = { version = "2.1", optional = true }
prost = { version = "0.13", optional = true }
serde_html_form = { version = "0.2.0", optional = true }
serde_json = { version = "1.0.71", optional = true }
serde_path_to_error = { version = "0.1.8", optional = true }
tokio = { version = "1.19", optional = true }
tokio-stream = { version = "0.1.9", optional = true }
tokio-util = { version = "0.7", optional = true }
tracing = { version = "0.1.37", default-features = false, optional = true }
typed-json = { version = "0.1.1", optional = true }
# doc dependencies
serde = { version = "1.0.221", optional = true }
tower = { version = "0.5.2", default-features = false, features = ["util"], optional = true }
[dev-dependencies]
axum = { path = "../axum", features = ["macros", "__private"] }
axum-macros = { path = "../axum-macros", features = ["__private"] }
hyper = "1.0.0"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0.221", features = ["derive"] }
serde_json = "1.0.71"
tokio = { version = "1.14", features = ["full"] }
tower = { version = "0.5.2", features = ["util"] }
tower-http = { version = "0.6.0", features = ["map-response-body", "timeout"] }
tracing-subscriber = "0.3.19"
[lints]
workspace = true
[package.metadata.docs.rs]
all-features = true
[package.metadata.cargo-public-api-crates]
allowed = [
"axum",
"axum_core",
"axum_macros",
"bytes",
"cookie",
"futures_core",
"futures_util",
"headers",
"headers_core",
"http",
"http_body",
"pin_project_lite",
"prost",
"serde_core",
"tokio",
"tokio_util",
"tower_layer",
"tower_service",
]
+7
View File
@@ -0,0 +1,7 @@
Copyright 2021 axum Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+45
View File
@@ -0,0 +1,45 @@
# axum-extra
[![Build status](https://github.com/tokio-rs/axum/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/tokio-rs/axum-extra/actions/workflows/CI.yml)
[![Crates.io](https://img.shields.io/crates/v/axum-extra)](https://crates.io/crates/axum-extra)
[![Documentation](https://docs.rs/axum-extra/badge.svg)](https://docs.rs/axum-extra)
Extra utilities for [`axum`].
More information about this crate can be found in the [crate documentation][docs].
## Safety
This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in 100% safe Rust.
## Minimum supported Rust version
axum-extra's MSRV is 1.75.
## Getting Help
You're also welcome to ask in the [Discord channel][chat] or open an [issue]
with your question.
## Contributing
🎈 Thanks for your help improving the project! We are so happy to have
you! We have a [contributing guide][contributing] to help you get involved in the
`axum` project.
## License
This project is licensed under the [MIT license][license].
### Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in `axum` by you, shall be licensed as MIT, without any
additional terms or conditions.
[`axum`]: https://crates.io/crates/axum
[chat]: https://discord.gg/tokio
[contributing]: /CONTRIBUTING.md
[docs]: https://docs.rs/axum-extra
[license]: /axum-extra/LICENSE
[issue]: https://github.com/tokio-rs/axum/issues/new
@@ -0,0 +1,94 @@
use axum::{
body::{Body, Bytes, HttpBody},
response::{IntoResponse, Response},
Error,
};
use pin_project_lite::pin_project;
use std::{
pin::Pin,
task::{Context, Poll},
};
use tokio::io::AsyncRead;
use tokio_util::io::ReaderStream;
pin_project! {
/// An [`HttpBody`] created from an [`AsyncRead`].
///
/// # Example
///
/// `AsyncReadBody` can be used to stream the contents of a file:
///
/// ```rust
/// use axum::{
/// Router,
/// routing::get,
/// http::{StatusCode, header::CONTENT_TYPE},
/// response::{Response, IntoResponse},
/// };
/// use axum_extra::body::AsyncReadBody;
/// use tokio::fs::File;
///
/// async fn cargo_toml() -> Result<Response, (StatusCode, String)> {
/// let file = File::open("Cargo.toml")
/// .await
/// .map_err(|err| {
/// (StatusCode::NOT_FOUND, format!("File not found: {err}"))
/// })?;
///
/// let headers = [(CONTENT_TYPE, "text/x-toml")];
/// let body = AsyncReadBody::new(file);
/// Ok((headers, body).into_response())
/// }
///
/// let app = Router::new().route("/Cargo.toml", get(cargo_toml));
/// # let _: Router = app;
/// ```
#[cfg(feature = "async-read-body")]
#[derive(Debug)]
#[must_use]
pub struct AsyncReadBody {
#[pin]
body: Body,
}
}
impl AsyncReadBody {
/// Create a new `AsyncReadBody`.
pub fn new<R>(read: R) -> Self
where
R: AsyncRead + Send + 'static,
{
Self {
body: Body::from_stream(ReaderStream::new(read)),
}
}
}
impl HttpBody for AsyncReadBody {
type Data = Bytes;
type Error = Error;
#[inline]
fn poll_frame(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
self.project().body.poll_frame(cx)
}
#[inline]
fn is_end_stream(&self) -> bool {
self.body.is_end_stream()
}
#[inline]
fn size_hint(&self) -> http_body::SizeHint {
self.body.size_hint()
}
}
impl IntoResponse for AsyncReadBody {
fn into_response(self) -> Response {
self.body.into_response()
}
}
+7
View File
@@ -0,0 +1,7 @@
//! Additional bodies.
#[cfg(feature = "async-read-body")]
mod async_read_body;
#[cfg(feature = "async-read-body")]
pub use self::async_read_body::AsyncReadBody;
+314
View File
@@ -0,0 +1,314 @@
//! `Either*` types for combining extractors or responses into a single type.
//!
//! # As an extractor
//!
//! ```
//! use axum_extra::either::Either3;
//! use axum::{
//! body::Bytes,
//! Router,
//! routing::get,
//! extract::FromRequestParts,
//! };
//!
//! // extractors for checking permissions
//! struct AdminPermissions {}
//!
//! impl<S> FromRequestParts<S> for AdminPermissions
//! where
//! S: Send + Sync,
//! {
//! // check for admin permissions...
//! # type Rejection = ();
//! # async fn from_request_parts(parts: &mut axum::http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
//! # todo!()
//! # }
//! }
//!
//! struct User {}
//!
//! impl<S> FromRequestParts<S> for User
//! where
//! S: Send + Sync,
//! {
//! // check for a logged in user...
//! # type Rejection = ();
//! # async fn from_request_parts(parts: &mut axum::http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
//! # todo!()
//! # }
//! }
//!
//! async fn handler(
//! body: Either3<AdminPermissions, User, ()>,
//! ) {
//! match body {
//! Either3::E1(admin) => { /* ... */ }
//! Either3::E2(user) => { /* ... */ }
//! Either3::E3(guest) => { /* ... */ }
//! }
//! }
//! #
//! # let _: axum::routing::MethodRouter = axum::routing::get(handler);
//! ```
//!
//! Note that if all the inner extractors reject the request, the rejection from the last
//! extractor will be returned. For the example above that would be [`BytesRejection`].
//!
//! # As a response
//!
//! ```
//! use axum_extra::either::Either3;
//! use axum::{Json, http::StatusCode, response::IntoResponse};
//! use serde_json::{Value, json};
//!
//! async fn handler() -> Either3<Json<Value>, &'static str, StatusCode> {
//! if something() {
//! Either3::E1(Json(json!({ "data": "..." })))
//! } else if something_else() {
//! Either3::E2("foobar")
//! } else {
//! Either3::E3(StatusCode::NOT_FOUND)
//! }
//! }
//!
//! fn something() -> bool {
//! // ...
//! # false
//! }
//!
//! fn something_else() -> bool {
//! // ...
//! # false
//! }
//! #
//! # let _: axum::routing::MethodRouter = axum::routing::get(handler);
//! ```
//!
//! The general recommendation is to use [`IntoResponse::into_response`] to return different response
//! types, but if you need to preserve the exact type then `Either*` works as well.
//!
//! [`BytesRejection`]: axum::extract::rejection::BytesRejection
//! [`IntoResponse::into_response`]: https://docs.rs/axum/0.8/axum/response/index.html#returning-different-response-types
use std::task::{Context, Poll};
use axum::{
extract::FromRequestParts,
response::{IntoResponse, Response},
};
use http::request::Parts;
use tower_layer::Layer;
use tower_service::Service;
/// Combines two extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either<E1, E2> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
}
/// Combines three extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either3<E1, E2, E3> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
#[allow(missing_docs)]
E3(E3),
}
/// Combines four extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either4<E1, E2, E3, E4> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
#[allow(missing_docs)]
E3(E3),
#[allow(missing_docs)]
E4(E4),
}
/// Combines five extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either5<E1, E2, E3, E4, E5> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
#[allow(missing_docs)]
E3(E3),
#[allow(missing_docs)]
E4(E4),
#[allow(missing_docs)]
E5(E5),
}
/// Combines six extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either6<E1, E2, E3, E4, E5, E6> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
#[allow(missing_docs)]
E3(E3),
#[allow(missing_docs)]
E4(E4),
#[allow(missing_docs)]
E5(E5),
#[allow(missing_docs)]
E6(E6),
}
/// Combines seven extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either7<E1, E2, E3, E4, E5, E6, E7> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
#[allow(missing_docs)]
E3(E3),
#[allow(missing_docs)]
E4(E4),
#[allow(missing_docs)]
E5(E5),
#[allow(missing_docs)]
E6(E6),
#[allow(missing_docs)]
E7(E7),
}
/// Combines eight extractors or responses into a single type.
///
/// See the [module docs](self) for examples.
#[derive(Debug, Clone)]
#[must_use]
pub enum Either8<E1, E2, E3, E4, E5, E6, E7, E8> {
#[allow(missing_docs)]
E1(E1),
#[allow(missing_docs)]
E2(E2),
#[allow(missing_docs)]
E3(E3),
#[allow(missing_docs)]
E4(E4),
#[allow(missing_docs)]
E5(E5),
#[allow(missing_docs)]
E6(E6),
#[allow(missing_docs)]
E7(E7),
#[allow(missing_docs)]
E8(E8),
}
macro_rules! impl_traits_for_either {
(
$either:ident =>
[$($ident:ident),* $(,)?],
$last:ident $(,)?
) => {
impl<S, $($ident),*, $last> FromRequestParts<S> for $either<$($ident),*, $last>
where
$($ident: FromRequestParts<S>),*,
$last: FromRequestParts<S>,
S: Send + Sync,
{
type Rejection = $last::Rejection;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
$(
if let Ok(value) = <$ident as FromRequestParts<S>>::from_request_parts(parts, state).await {
return Ok(Self::$ident(value));
}
)*
<$last as FromRequestParts<S>>::from_request_parts(parts, state).await.map(Self::$last)
}
}
impl<$($ident),*, $last> IntoResponse for $either<$($ident),*, $last>
where
$($ident: IntoResponse),*,
$last: IntoResponse,
{
fn into_response(self) -> Response {
match self {
$( Self::$ident(value) => value.into_response(), )*
Self::$last(value) => value.into_response(),
}
}
}
};
}
impl_traits_for_either!(Either => [E1], E2);
impl_traits_for_either!(Either3 => [E1, E2], E3);
impl_traits_for_either!(Either4 => [E1, E2, E3], E4);
impl_traits_for_either!(Either5 => [E1, E2, E3, E4], E5);
impl_traits_for_either!(Either6 => [E1, E2, E3, E4, E5], E6);
impl_traits_for_either!(Either7 => [E1, E2, E3, E4, E5, E6], E7);
impl_traits_for_either!(Either8 => [E1, E2, E3, E4, E5, E6, E7], E8);
impl<E1, E2, S> Layer<S> for Either<E1, E2>
where
E1: Layer<S>,
E2: Layer<S>,
{
type Service = Either<E1::Service, E2::Service>;
fn layer(&self, inner: S) -> Self::Service {
match self {
Either::E1(layer) => Either::E1(layer.layer(inner)),
Either::E2(layer) => Either::E2(layer.layer(inner)),
}
}
}
impl<R, E1, E2> Service<R> for Either<E1, E2>
where
E1: Service<R>,
E2: Service<R, Response = E1::Response, Error = E1::Error>,
{
type Response = E1::Response;
type Error = E1::Error;
type Future = futures_util::future::Either<E1::Future, E2::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self {
Either::E1(inner) => inner.poll_ready(cx),
Either::E2(inner) => inner.poll_ready(cx),
}
}
fn call(&mut self, req: R) -> Self::Future {
match self {
Either::E1(inner) => futures_util::future::Either::Left(inner.call(req)),
Either::E2(inner) => futures_util::future::Either::Right(inner.call(req)),
}
}
}
+158
View File
@@ -0,0 +1,158 @@
use axum::extract::{Extension, FromRequestParts};
use http::request::Parts;
/// Cache results of other extractors.
///
/// `Cached` wraps another extractor and caches its result in [request extensions].
///
/// This is useful if you have a tree of extractors that share common sub-extractors that
/// you only want to run once, perhaps because they're expensive.
///
/// The cache purely type based so you can only cache one value of each type. The cache is also
/// local to the current request and not reused across requests.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::Cached;
/// use axum::{
/// extract::FromRequestParts,
/// response::{IntoResponse, Response},
/// http::{StatusCode, request::Parts},
/// };
///
/// #[derive(Clone)]
/// struct Session { /* ... */ }
///
/// impl<S> FromRequestParts<S> for Session
/// where
/// S: Send + Sync,
/// {
/// type Rejection = (StatusCode, String);
///
/// async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
/// // load session...
/// # unimplemented!()
/// }
/// }
///
/// struct CurrentUser { /* ... */ }
///
/// impl<S> FromRequestParts<S> for CurrentUser
/// where
/// S: Send + Sync,
/// {
/// type Rejection = Response;
///
/// async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
/// // loading a `CurrentUser` requires first loading the `Session`
/// //
/// // by using `Cached<Session>` we avoid extracting the session more than
/// // once, in case other extractors for the same request also loads the session
/// let session: Session = Cached::<Session>::from_request_parts(parts, state)
/// .await
/// .map_err(|err| err.into_response())?
/// .0;
///
/// // load user from session...
/// # unimplemented!()
/// }
/// }
///
/// // handler that extracts the current user and the session
/// //
/// // the session will only be loaded once, even though `CurrentUser`
/// // also loads it
/// async fn handler(
/// current_user: CurrentUser,
/// // we have to use `Cached<Session>` here otherwise the
/// // cached session would not be used
/// Cached(session): Cached<Session>,
/// ) {
/// // ...
/// }
/// ```
///
/// [request extensions]: http::Extensions
#[derive(Debug, Clone, Default)]
pub struct Cached<T>(pub T);
#[derive(Clone)]
struct CachedEntry<T>(T);
impl<S, T> FromRequestParts<S> for Cached<T>
where
S: Send + Sync,
T: FromRequestParts<S> + Clone + Send + Sync + 'static,
{
type Rejection = T::Rejection;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match Extension::<CachedEntry<T>>::from_request_parts(parts, state).await {
Ok(Extension(CachedEntry(value))) => Ok(Self(value)),
Err(_) => {
let value = T::from_request_parts(parts, state).await?;
parts.extensions.insert(CachedEntry(value.clone()));
Ok(Self(value))
}
}
}
}
axum_core::__impl_deref!(Cached);
#[cfg(test)]
mod tests {
use super::*;
use axum::{http::Request, routing::get, Router};
use std::{
convert::Infallible,
sync::atomic::{AtomicU32, Ordering},
time::Instant,
};
#[tokio::test]
async fn works() {
static COUNTER: AtomicU32 = AtomicU32::new(0);
#[derive(Clone, Debug, PartialEq, Eq)]
struct Extractor(Instant);
impl<S> FromRequestParts<S> for Extractor
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(
_parts: &mut Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
COUNTER.fetch_add(1, Ordering::SeqCst);
Ok(Self(Instant::now()))
}
}
let (mut parts, _) = Request::new(()).into_parts();
let first = Cached::<Extractor>::from_request_parts(&mut parts, &())
.await
.unwrap()
.0;
assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
let second = Cached::<Extractor>::from_request_parts(&mut parts, &())
.await
.unwrap()
.0;
assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
assert_eq!(first, second);
}
// Not a #[test], we just want to know this compiles
async fn _last_handler_argument() {
async fn handler(_: http::Method, _: Cached<http::HeaderMap>) {}
let _r: Router = Router::new().route("/", get(handler));
}
}
@@ -0,0 +1,383 @@
//! Cookie parsing and cookie jar management.
//!
//! See [`CookieJar`], [`SignedCookieJar`], and [`PrivateCookieJar`] for more details.
use axum::{
extract::FromRequestParts,
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
use http::{
header::{COOKIE, SET_COOKIE},
request::Parts,
HeaderMap,
};
use std::convert::Infallible;
#[cfg(feature = "cookie-private")]
mod private;
#[cfg(feature = "cookie-signed")]
mod signed;
#[cfg(feature = "cookie-private")]
pub use self::private::PrivateCookieJar;
#[cfg(feature = "cookie-signed")]
pub use self::signed::SignedCookieJar;
pub use cookie::{Cookie, Expiration, SameSite};
#[cfg(any(feature = "cookie-signed", feature = "cookie-private"))]
pub use cookie::Key;
/// Extractor that grabs cookies from the request and manages the jar.
///
/// Note that methods like [`CookieJar::add`], [`CookieJar::remove`], etc updates the [`CookieJar`]
/// and returns it. This value _must_ be returned from the handler as part of the response for the
/// changes to be propagated.
///
/// # Example
///
/// ```rust
/// use axum::{
/// Router,
/// routing::{post, get},
/// response::{IntoResponse, Redirect},
/// http::StatusCode,
/// };
/// use axum_extra::{
/// TypedHeader,
/// headers::authorization::{Authorization, Bearer},
/// extract::cookie::{CookieJar, Cookie},
/// };
///
/// async fn create_session(
/// TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
/// jar: CookieJar,
/// ) -> Result<(CookieJar, Redirect), StatusCode> {
/// if let Some(session_id) = authorize_and_create_session(auth.token()).await {
/// Ok((
/// // the updated jar must be returned for the changes
/// // to be included in the response
/// jar.add(Cookie::new("session_id", session_id)),
/// Redirect::to("/me"),
/// ))
/// } else {
/// Err(StatusCode::UNAUTHORIZED)
/// }
/// }
///
/// async fn me(jar: CookieJar) -> Result<(), StatusCode> {
/// if let Some(session_id) = jar.get("session_id") {
/// // fetch and render user...
/// # Ok(())
/// } else {
/// Err(StatusCode::UNAUTHORIZED)
/// }
/// }
///
/// async fn authorize_and_create_session(token: &str) -> Option<String> {
/// // authorize the user and create a session...
/// # todo!()
/// }
///
/// let app = Router::new()
/// .route("/sessions", post(create_session))
/// .route("/me", get(me));
/// # let app: Router = app;
/// ```
#[must_use = "`CookieJar` should be returned as part of a `Response`, otherwise it does nothing."]
#[derive(Debug, Default, Clone)]
pub struct CookieJar {
jar: cookie::CookieJar,
}
impl<S> FromRequestParts<S> for CookieJar
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
Ok(Self::from_headers(&parts.headers))
}
}
fn cookies_from_request(headers: &HeaderMap) -> impl Iterator<Item = Cookie<'static>> + '_ {
headers
.get_all(COOKIE)
.into_iter()
.filter_map(|value| value.to_str().ok())
.flat_map(|value| value.split(';'))
.filter_map(|cookie| Cookie::parse_encoded(cookie.to_owned()).ok())
}
impl CookieJar {
/// Create a new `CookieJar` from a map of request headers.
///
/// The cookies in `headers` will be added to the jar.
///
/// This is intended to be used in middleware and other places where it might be difficult to
/// run extractors. Normally you should create `CookieJar`s through [`FromRequestParts`].
///
/// [`FromRequestParts`]: axum::extract::FromRequestParts
pub fn from_headers(headers: &HeaderMap) -> Self {
let mut jar = cookie::CookieJar::new();
for cookie in cookies_from_request(headers) {
jar.add_original(cookie);
}
Self { jar }
}
/// Create a new empty `CookieJar`.
///
/// This is intended to be used in middleware and other places where it might be difficult to
/// run extractors. Normally you should create `CookieJar`s through [`FromRequestParts`].
///
/// If you need a jar that contains the headers from a request use `impl From<&HeaderMap> for
/// CookieJar`.
///
/// [`FromRequestParts`]: axum::extract::FromRequestParts
pub fn new() -> Self {
Self::default()
}
/// Get a cookie from the jar.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::CookieJar;
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: CookieJar) {
/// let value: Option<String> = jar
/// .get("foo")
/// .map(|cookie| cookie.value().to_owned());
/// }
/// ```
#[must_use]
pub fn get(&self, name: &str) -> Option<&Cookie<'static>> {
self.jar.get(name)
}
/// Remove a cookie from the jar.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{CookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: CookieJar) -> CookieJar {
/// jar.remove(Cookie::from("foo"))
/// }
/// ```
pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
self.jar.remove(cookie);
self
}
/// Add a cookie to the jar.
///
/// The value will automatically be percent-encoded.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{CookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: CookieJar) -> CookieJar {
/// jar.add(Cookie::new("foo", "bar"))
/// }
/// ```
#[allow(clippy::should_implement_trait)]
pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
self.jar.add(cookie);
self
}
/// Get an iterator over all cookies in the jar.
pub fn iter(&self) -> impl Iterator<Item = &'_ Cookie<'static>> {
self.jar.iter()
}
}
impl IntoResponseParts for CookieJar {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
set_cookies(self.jar, res.headers_mut());
Ok(res)
}
}
impl IntoResponse for CookieJar {
fn into_response(self) -> Response {
(self, ()).into_response()
}
}
fn set_cookies(jar: cookie::CookieJar, headers: &mut HeaderMap) {
for cookie in jar.delta() {
if let Ok(header_value) = cookie.encoded().to_string().parse() {
headers.append(SET_COOKIE, header_value);
}
}
// we don't need to call `jar.reset_delta()` because `into_response_parts` consumes the cookie
// jar so it cannot be called multiple times.
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{body::Body, extract::FromRef, http::Request, routing::get, Router};
use http_body_util::BodyExt;
use tower::ServiceExt;
macro_rules! cookie_test {
($name:ident, $jar:ty) => {
#[tokio::test]
async fn $name() {
async fn set_cookie(jar: $jar) -> impl IntoResponse {
jar.add(Cookie::new("key", "value"))
}
async fn get_cookie(jar: $jar) -> impl IntoResponse {
jar.get("key").unwrap().value().to_owned()
}
async fn remove_cookie(jar: $jar) -> impl IntoResponse {
jar.remove(Cookie::from("key"))
}
let state = AppState {
key: Key::generate(),
custom_key: CustomKey(Key::generate()),
};
let app = Router::new()
.route("/set", get(set_cookie))
.route("/get", get(get_cookie))
.route("/remove", get(remove_cookie))
.with_state(state);
let res = app
.clone()
.oneshot(Request::builder().uri("/set").body(Body::empty()).unwrap())
.await
.unwrap();
let cookie_value = res.headers()["set-cookie"].to_str().unwrap();
let res = app
.clone()
.oneshot(
Request::builder()
.uri("/get")
.header("cookie", cookie_value)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = body_text(res).await;
assert_eq!(body, "value");
let res = app
.clone()
.oneshot(
Request::builder()
.uri("/remove")
.header("cookie", cookie_value)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert!(res.headers()["set-cookie"]
.to_str()
.unwrap()
.contains("key=;"));
}
};
}
cookie_test!(plaintext_cookies, CookieJar);
#[cfg(feature = "cookie-signed")]
cookie_test!(signed_cookies, SignedCookieJar);
#[cfg(feature = "cookie-signed")]
cookie_test!(signed_cookies_with_custom_key, SignedCookieJar<CustomKey>);
#[cfg(feature = "cookie-private")]
cookie_test!(private_cookies, PrivateCookieJar);
#[cfg(feature = "cookie-private")]
cookie_test!(private_cookies_with_custom_key, PrivateCookieJar<CustomKey>);
#[derive(Clone)]
struct AppState {
key: Key,
custom_key: CustomKey,
}
impl FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Key {
state.key.clone()
}
}
impl FromRef<AppState> for CustomKey {
fn from_ref(state: &AppState) -> CustomKey {
state.custom_key.clone()
}
}
#[derive(Clone)]
struct CustomKey(Key);
impl From<CustomKey> for Key {
fn from(custom: CustomKey) -> Self {
custom.0
}
}
#[cfg(feature = "cookie-signed")]
#[tokio::test]
async fn signed_cannot_access_invalid_cookies() {
async fn get_cookie(jar: SignedCookieJar) -> impl IntoResponse {
format!("{:?}", jar.get("key"))
}
let state = AppState {
key: Key::generate(),
custom_key: CustomKey(Key::generate()),
};
let app = Router::new()
.route("/get", get(get_cookie))
.with_state(state);
let res = app
.clone()
.oneshot(
Request::builder()
.uri("/get")
.header("cookie", "key=value")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let body = body_text(res).await;
assert_eq!(body, "None");
}
async fn body_text<B>(body: B) -> String
where
B: axum::body::HttpBody,
B::Error: std::fmt::Debug,
{
let bytes = body.collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
}
@@ -0,0 +1,316 @@
use super::{cookies_from_request, set_cookies, Cookie, Key};
use axum::{
extract::{FromRef, FromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
use cookie::PrivateJar;
use http::{request::Parts, HeaderMap};
use std::{convert::Infallible, fmt, marker::PhantomData};
/// Extractor that grabs private cookies from the request and manages the jar.
///
/// All cookies will be private and encrypted with a [`Key`]. This makes it suitable for storing
/// private data.
///
/// Note that methods like [`PrivateCookieJar::add`], [`PrivateCookieJar::remove`], etc updates the
/// [`PrivateCookieJar`] and returns it. This value _must_ be returned from the handler as part of
/// the response for the changes to be propagated.
///
/// # Example
///
/// ```rust
/// use axum::{
/// Router,
/// routing::{post, get},
/// extract::FromRef,
/// response::{IntoResponse, Redirect},
/// http::StatusCode,
/// };
/// use axum_extra::{
/// TypedHeader,
/// headers::authorization::{Authorization, Bearer},
/// extract::cookie::{PrivateCookieJar, Cookie, Key},
/// };
///
/// async fn set_secret(
/// jar: PrivateCookieJar,
/// ) -> (PrivateCookieJar, Redirect) {
/// let updated_jar = jar.add(Cookie::new("secret", "secret-data"));
/// (updated_jar, Redirect::to("/get"))
/// }
///
/// async fn get_secret(jar: PrivateCookieJar) {
/// if let Some(data) = jar.get("secret") {
/// // ...
/// }
/// }
///
/// // our application state
/// #[derive(Clone)]
/// struct AppState {
/// // that holds the key used to encrypt cookies
/// key: Key,
/// }
///
/// // this impl tells `PrivateCookieJar` how to access the key from our state
/// impl FromRef<AppState> for Key {
/// fn from_ref(state: &AppState) -> Self {
/// state.key.clone()
/// }
/// }
///
/// let state = AppState {
/// // Generate a secure key
/// //
/// // You probably don't wanna generate a new one each time the app starts though
/// key: Key::generate(),
/// };
///
/// let app = Router::new()
/// .route("/set", post(set_secret))
/// .route("/get", get(get_secret))
/// .with_state(state);
/// # let _: axum::Router = app;
/// ```
///
/// If you have been using `Arc<AppState>` you cannot implement `FromRef<Arc<AppState>> for Key`.
/// You can use a new type instead:
///
/// ```rust
/// # use axum::extract::FromRef;
/// # use axum_extra::extract::cookie::{PrivateCookieJar, Cookie, Key};
/// use std::sync::Arc;
/// use std::ops::Deref;
///
/// #[derive(Clone)]
/// struct AppState(Arc<InnerState>);
///
/// // deref so you can still access the inner fields easily
/// impl Deref for AppState {
/// type Target = InnerState;
///
/// fn deref(&self) -> &Self::Target {
/// &self.0
/// }
/// }
///
/// struct InnerState {
/// key: Key
/// }
///
/// impl FromRef<AppState> for Key {
/// fn from_ref(state: &AppState) -> Self {
/// state.0.key.clone()
/// }
/// }
/// ```
#[must_use = "`PrivateCookieJar` should be returned as part of a `Response`, otherwise it does nothing."]
pub struct PrivateCookieJar<K = Key> {
jar: cookie::CookieJar,
key: Key,
// The key used to extract the key. Allows users to use multiple keys for different
// jars. Maybe a library wants its own key.
_marker: PhantomData<K>,
}
impl<K> fmt::Debug for PrivateCookieJar<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PrivateCookieJar")
.field("jar", &self.jar)
.field("key", &"REDACTED")
.finish()
}
}
impl<S, K> FromRequestParts<S> for PrivateCookieJar<K>
where
S: Send + Sync,
K: FromRef<S> + Into<Key>,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let k = K::from_ref(state);
let key = k.into();
let PrivateCookieJar {
jar,
key,
_marker: _,
} = PrivateCookieJar::from_headers(&parts.headers, key);
Ok(PrivateCookieJar {
jar,
key,
_marker: PhantomData,
})
}
}
impl PrivateCookieJar {
/// Create a new `PrivateCookieJar` from a map of request headers.
///
/// The valid cookies in `headers` will be added to the jar.
///
/// This is intended to be used in middleware and other where places it might be difficult to
/// run extractors. Normally you should create `PrivateCookieJar`s through [`FromRequestParts`].
///
/// [`FromRequestParts`]: axum::extract::FromRequestParts
pub fn from_headers(headers: &HeaderMap, key: Key) -> Self {
let mut jar = cookie::CookieJar::new();
let mut private_jar = jar.private_mut(&key);
for cookie in cookies_from_request(headers) {
if let Some(cookie) = private_jar.decrypt(cookie) {
private_jar.add_original(cookie);
}
}
Self {
jar,
key,
_marker: PhantomData,
}
}
/// Create a new empty `PrivateCookieJarIter`.
///
/// This is intended to be used in middleware and other places where it might be difficult to
/// run extractors. Normally you should create `PrivateCookieJar`s through [`FromRequestParts`].
///
/// [`FromRequestParts`]: axum::extract::FromRequestParts
pub fn new(key: Key) -> Self {
Self {
jar: Default::default(),
key,
_marker: PhantomData,
}
}
}
impl<K> PrivateCookieJar<K> {
/// Get a cookie from the jar.
///
/// If the cookie exists and can be decrypted then it is returned in plaintext.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::PrivateCookieJar;
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: PrivateCookieJar) {
/// let value: Option<String> = jar
/// .get("foo")
/// .map(|cookie| cookie.value().to_owned());
/// }
/// ```
#[must_use]
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
self.private_jar().get(name)
}
/// Remove a cookie from the jar.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{PrivateCookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: PrivateCookieJar) -> PrivateCookieJar {
/// jar.remove(Cookie::from("foo"))
/// }
/// ```
pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
self.private_jar_mut().remove(cookie);
self
}
/// Add a cookie to the jar.
///
/// The value will automatically be percent-encoded.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{PrivateCookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: PrivateCookieJar) -> PrivateCookieJar {
/// jar.add(Cookie::new("foo", "bar"))
/// }
/// ```
#[allow(clippy::should_implement_trait)]
pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
self.private_jar_mut().add(cookie);
self
}
/// Authenticates and decrypts `cookie`, returning the plaintext version if decryption succeeds
/// or `None` otherwise.
#[must_use]
pub fn decrypt(&self, cookie: Cookie<'static>) -> Option<Cookie<'static>> {
self.private_jar().decrypt(cookie)
}
/// Get an iterator over all cookies in the jar.
///
/// Only cookies with valid authenticity and integrity are yielded by the iterator.
pub fn iter(&self) -> impl Iterator<Item = Cookie<'static>> + '_ {
PrivateCookieJarIter {
jar: self,
iter: self.jar.iter(),
}
}
fn private_jar(&self) -> PrivateJar<&'_ cookie::CookieJar> {
self.jar.private(&self.key)
}
fn private_jar_mut(&mut self) -> PrivateJar<&'_ mut cookie::CookieJar> {
self.jar.private_mut(&self.key)
}
}
impl<K> IntoResponseParts for PrivateCookieJar<K> {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
set_cookies(self.jar, res.headers_mut());
Ok(res)
}
}
impl<K> IntoResponse for PrivateCookieJar<K> {
fn into_response(self) -> Response {
(self, ()).into_response()
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
struct PrivateCookieJarIter<'a, K> {
jar: &'a PrivateCookieJar<K>,
iter: cookie::Iter<'a>,
}
impl<K> Iterator for PrivateCookieJarIter<'_, K> {
type Item = Cookie<'static>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let cookie = self.iter.next()?;
if let Some(cookie) = self.jar.get(cookie.name()) {
return Some(cookie);
}
}
}
}
impl<K> Clone for PrivateCookieJar<K> {
fn clone(&self) -> Self {
Self {
jar: self.jar.clone(),
key: self.key.clone(),
_marker: self._marker,
}
}
}
@@ -0,0 +1,334 @@
use super::{cookies_from_request, set_cookies};
use axum::{
extract::{FromRef, FromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
use cookie::SignedJar;
use cookie::{Cookie, Key};
use http::{request::Parts, HeaderMap};
use std::{convert::Infallible, fmt, marker::PhantomData};
/// Extractor that grabs signed cookies from the request and manages the jar.
///
/// All cookies will be signed and verified with a [`Key`]. Do not use this to store private data
/// as the values are still transmitted in plaintext.
///
/// Note that methods like [`SignedCookieJar::add`], [`SignedCookieJar::remove`], etc updates the
/// [`SignedCookieJar`] and returns it. This value _must_ be returned from the handler as part of
/// the response for the changes to be propagated.
///
/// # Example
///
/// ```rust
/// use axum::{
/// Router,
/// routing::{post, get},
/// extract::FromRef,
/// response::{IntoResponse, Redirect},
/// http::StatusCode,
/// };
/// use axum_extra::{
/// TypedHeader,
/// headers::authorization::{Authorization, Bearer},
/// extract::cookie::{SignedCookieJar, Cookie, Key},
/// };
///
/// async fn create_session(
/// TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
/// jar: SignedCookieJar,
/// ) -> Result<(SignedCookieJar, Redirect), StatusCode> {
/// if let Some(session_id) = authorize_and_create_session(auth.token()).await {
/// Ok((
/// // the updated jar must be returned for the changes
/// // to be included in the response
/// jar.add(Cookie::new("session_id", session_id)),
/// Redirect::to("/me"),
/// ))
/// } else {
/// Err(StatusCode::UNAUTHORIZED)
/// }
/// }
///
/// async fn me(jar: SignedCookieJar) -> Result<(), StatusCode> {
/// if let Some(session_id) = jar.get("session_id") {
/// // fetch and render user...
/// # Ok(())
/// } else {
/// Err(StatusCode::UNAUTHORIZED)
/// }
/// }
///
/// async fn authorize_and_create_session(token: &str) -> Option<String> {
/// // authorize the user and create a session...
/// # todo!()
/// }
///
/// // our application state
/// #[derive(Clone)]
/// struct AppState {
/// // that holds the key used to sign cookies
/// key: Key,
/// }
///
/// // this impl tells `SignedCookieJar` how to access the key from our state
/// impl FromRef<AppState> for Key {
/// fn from_ref(state: &AppState) -> Self {
/// state.key.clone()
/// }
/// }
///
/// let state = AppState {
/// // Generate a secure key
/// //
/// // You probably don't wanna generate a new one each time the app starts though
/// key: Key::generate(),
/// };
///
/// let app = Router::new()
/// .route("/sessions", post(create_session))
/// .route("/me", get(me))
/// .with_state(state);
/// # let _: axum::Router = app;
/// ```
/// If you have been using `Arc<AppState>` you cannot implement `FromRef<Arc<AppState>> for Key`.
/// You can use a new type instead:
///
/// ```rust
/// # use axum::extract::FromRef;
/// # use axum_extra::extract::cookie::{PrivateCookieJar, Cookie, Key};
/// use std::sync::Arc;
/// use std::ops::Deref;
///
/// #[derive(Clone)]
/// struct AppState(Arc<InnerState>);
///
/// // deref so you can still access the inner fields easily
/// impl Deref for AppState {
/// type Target = InnerState;
///
/// fn deref(&self) -> &Self::Target {
/// &*self.0
/// }
/// }
///
/// struct InnerState {
/// key: Key
/// }
///
/// impl FromRef<AppState> for Key {
/// fn from_ref(state: &AppState) -> Self {
/// state.0.key.clone()
/// }
/// }
/// ```
#[must_use = "`SignedCookieJar` should be returned as part of a `Response`, otherwise it does nothing."]
pub struct SignedCookieJar<K = Key> {
jar: cookie::CookieJar,
key: Key,
// The key used to extract the key. Allows users to use multiple keys for different
// jars. Maybe a library wants its own key.
_marker: PhantomData<K>,
}
impl<K> fmt::Debug for SignedCookieJar<K> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SignedCookieJar")
.field("jar", &self.jar)
.field("key", &"REDACTED")
.finish()
}
}
impl<S, K> FromRequestParts<S> for SignedCookieJar<K>
where
S: Send + Sync,
K: FromRef<S> + Into<Key>,
{
type Rejection = Infallible;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let k = K::from_ref(state);
let key = k.into();
let SignedCookieJar {
jar,
key,
_marker: _,
} = SignedCookieJar::from_headers(&parts.headers, key);
Ok(SignedCookieJar {
jar,
key,
_marker: PhantomData,
})
}
}
impl SignedCookieJar {
/// Create a new `SignedCookieJar` from a map of request headers.
///
/// The valid cookies in `headers` will be added to the jar.
///
/// This is intended to be used in middleware and other places where it might be difficult to
/// run extractors. Normally you should create `SignedCookieJar`s through [`FromRequestParts`].
///
/// [`FromRequestParts`]: axum::extract::FromRequestParts
pub fn from_headers(headers: &HeaderMap, key: Key) -> Self {
let mut jar = cookie::CookieJar::new();
let mut signed_jar = jar.signed_mut(&key);
for cookie in cookies_from_request(headers) {
if let Some(cookie) = signed_jar.verify(cookie) {
signed_jar.add_original(cookie);
}
}
Self {
jar,
key,
_marker: PhantomData,
}
}
/// Create a new empty `SignedCookieJar`.
///
/// This is intended to be used in middleware and other places where it might be difficult to
/// run extractors. Normally you should create `SignedCookieJar`s through [`FromRequestParts`].
///
/// [`FromRequestParts`]: axum::extract::FromRequestParts
pub fn new(key: Key) -> Self {
Self {
jar: Default::default(),
key,
_marker: PhantomData,
}
}
}
impl<K> SignedCookieJar<K> {
/// Get a cookie from the jar.
///
/// If the cookie exists and its authenticity and integrity can be verified then it is returned
/// in plaintext.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::SignedCookieJar;
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: SignedCookieJar) {
/// let value: Option<String> = jar
/// .get("foo")
/// .map(|cookie| cookie.value().to_owned());
/// }
/// ```
#[must_use]
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
self.signed_jar().get(name)
}
/// Remove a cookie from the jar.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{SignedCookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: SignedCookieJar) -> SignedCookieJar {
/// jar.remove(Cookie::from("foo"))
/// }
/// ```
pub fn remove<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
self.signed_jar_mut().remove(cookie);
self
}
/// Add a cookie to the jar.
///
/// The value will automatically be percent-encoded.
///
/// # Example
///
/// ```rust
/// use axum_extra::extract::cookie::{SignedCookieJar, Cookie};
/// use axum::response::IntoResponse;
///
/// async fn handle(jar: SignedCookieJar) -> SignedCookieJar {
/// jar.add(Cookie::new("foo", "bar"))
/// }
/// ```
#[allow(clippy::should_implement_trait)]
pub fn add<C: Into<Cookie<'static>>>(mut self, cookie: C) -> Self {
self.signed_jar_mut().add(cookie);
self
}
/// Verifies the authenticity and integrity of `cookie`, returning the plaintext version if
/// verification succeeds or `None` otherwise.
#[must_use]
pub fn verify(&self, cookie: Cookie<'static>) -> Option<Cookie<'static>> {
self.signed_jar().verify(cookie)
}
/// Get an iterator over all cookies in the jar.
///
/// Only cookies with valid authenticity and integrity are yielded by the iterator.
pub fn iter(&self) -> impl Iterator<Item = Cookie<'static>> + '_ {
SignedCookieJarIter {
jar: self,
iter: self.jar.iter(),
}
}
fn signed_jar(&self) -> SignedJar<&'_ cookie::CookieJar> {
self.jar.signed(&self.key)
}
fn signed_jar_mut(&mut self) -> SignedJar<&'_ mut cookie::CookieJar> {
self.jar.signed_mut(&self.key)
}
}
impl<K> IntoResponseParts for SignedCookieJar<K> {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
set_cookies(self.jar, res.headers_mut());
Ok(res)
}
}
impl<K> IntoResponse for SignedCookieJar<K> {
fn into_response(self) -> Response {
(self, ()).into_response()
}
}
#[must_use = "iterators are lazy and do nothing unless consumed"]
struct SignedCookieJarIter<'a, K> {
jar: &'a SignedCookieJar<K>,
iter: cookie::Iter<'a>,
}
impl<K> Iterator for SignedCookieJarIter<'_, K> {
type Item = Cookie<'static>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let cookie = self.iter.next()?;
if let Some(cookie) = self.jar.get(cookie.name()) {
return Some(cookie);
}
}
}
}
impl<K> Clone for SignedCookieJar<K> {
fn clone(&self) -> Self {
Self {
jar: self.jar.clone(),
key: self.key.clone(),
_marker: self._marker,
}
}
}
+172
View File
@@ -0,0 +1,172 @@
use axum::extract::rejection::RawFormRejection;
use axum::{
extract::{FromRequest, RawForm, Request},
RequestExt,
};
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use serde_core::de::DeserializeOwned;
/// Extractor that deserializes `application/x-www-form-urlencoded` requests
/// into some type.
///
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Differences from `axum::extract::Form`
///
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
/// container.
///
/// # Example
///
/// ```rust,no_run
/// use axum_extra::extract::Form;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Payload {
/// #[serde(rename = "value")]
/// values: Vec<String>,
/// }
///
/// async fn accept_form(Form(payload): Form<Payload>) {
/// // ...
/// }
/// ```
///
/// [`serde_html_form`]: https://crates.io/crates/serde_html_form
#[derive(Debug, Clone, Copy, Default)]
#[cfg(feature = "form")]
pub struct Form<T>(pub T);
axum_core::__impl_deref!(Form);
impl<T, S> FromRequest<S> for Form<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = FormRejection;
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let is_get_or_head =
req.method() == http::Method::GET || req.method() == http::Method::HEAD;
let RawForm(bytes) = req.extract().await?;
let deserializer = serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes));
serde_path_to_error::deserialize::<_, T>(deserializer)
.map(Self)
.map_err(|err| {
if is_get_or_head {
FailedToDeserializeForm::from_err(err).into()
} else {
FailedToDeserializeFormBody::from_err(err).into()
}
})
}
}
define_rejection! {
#[status = BAD_REQUEST]
#[body = "Failed to deserialize form"]
/// Rejection type used if the [`Form`](Form) extractor is unable to
/// deserialize the form into the target type.
pub struct FailedToDeserializeForm(Error);
}
define_rejection! {
#[status = UNPROCESSABLE_ENTITY]
#[body = "Failed to deserialize form body"]
/// Rejection type used if the [`Form`](Form) extractor is unable to
/// deserialize the form body into the target type.
pub struct FailedToDeserializeFormBody(Error);
}
composite_rejection! {
/// Rejection used for [`Form`].
///
/// Contains one variant for each way the [`Form`] extractor can fail.
pub enum FormRejection {
RawFormRejection,
FailedToDeserializeForm,
FailedToDeserializeFormBody,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::routing::{on, post, MethodFilter};
use axum::Router;
use http::header::CONTENT_TYPE;
use http::StatusCode;
use mime::APPLICATION_WWW_FORM_URLENCODED;
use serde::Deserialize;
#[tokio::test]
async fn supports_multiple_values() {
#[derive(Deserialize)]
struct Data {
#[serde(rename = "value")]
values: Vec<String>,
}
let app = Router::new().route(
"/",
post(|Form(data): Form<Data>| async move { data.values.join(",") }),
);
let client = TestClient::new(app);
let res = client
.post("/")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("value=one&value=two")
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "one,two");
}
#[tokio::test]
async fn deserialize_error_status_codes() {
#[allow(dead_code)]
#[derive(Deserialize)]
struct Payload {
a: i32,
}
let app = Router::new().route(
"/",
on(
MethodFilter::GET.or(MethodFilter::POST),
|_: Form<Payload>| async {},
),
);
let client = TestClient::new(app);
let res = client.get("/?a=false").await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(
res.text().await,
"Failed to deserialize form: a: invalid digit found in string"
);
let res = client
.post("/")
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
.body("a=false")
.await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
assert_eq!(
res.text().await,
"Failed to deserialize form body: a: invalid digit found in string"
);
}
}
+246
View File
@@ -0,0 +1,246 @@
use super::rejection::{FailedToResolveHost, HostRejection};
use axum::{
extract::{FromRequestParts, OptionalFromRequestParts},
RequestPartsExt,
};
use http::{
header::{HeaderMap, FORWARDED},
request::Parts,
uri::Authority,
};
use std::convert::Infallible;
const X_FORWARDED_HOST_HEADER_KEY: &str = "X-Forwarded-Host";
/// Extractor that resolves the host of the request.
///
/// Host is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-Host` header
/// - `Host` header
/// - Authority of the request URI
///
/// See <https://www.rfc-editor.org/rfc/rfc9110.html#name-host-and-authority> for the definition of
/// host.
///
/// Note that user agents can set `X-Forwarded-Host` and `Host` headers to arbitrary values so make
/// sure to validate them to avoid security issues.
#[derive(Debug, Clone)]
pub struct Host(pub String);
impl<S> FromRequestParts<S> for Host
where
S: Send + Sync,
{
type Rejection = HostRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
parts
.extract::<Option<Host>>()
.await
.ok()
.flatten()
.ok_or(HostRejection::FailedToResolveHost(FailedToResolveHost))
}
}
impl<S> OptionalFromRequestParts<S> for Host
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Option<Self>, Self::Rejection> {
if let Some(host) = parse_forwarded(&parts.headers) {
return Ok(Some(Host(host.to_owned())));
}
if let Some(host) = parts
.headers
.get(X_FORWARDED_HOST_HEADER_KEY)
.and_then(|host| host.to_str().ok())
{
return Ok(Some(Host(host.to_owned())));
}
if let Some(host) = parts
.headers
.get(http::header::HOST)
.and_then(|host| host.to_str().ok())
{
return Ok(Some(Host(host.to_owned())));
}
if let Some(authority) = parts.uri.authority() {
return Ok(Some(Host(parse_authority(authority).to_owned())));
}
Ok(None)
}
}
#[allow(warnings)]
fn parse_forwarded(headers: &HeaderMap) -> Option<&str> {
// if there are multiple `Forwarded` `HeaderMap::get` will return the first one
let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?;
// get the first set of values
let first_value = forwarded_values.split(',').nth(0)?;
// find the value of the `host` field
first_value.split(';').find_map(|pair| {
let (key, value) = pair.split_once('=')?;
key.trim()
.eq_ignore_ascii_case("host")
.then(|| value.trim().trim_matches('"'))
})
}
fn parse_authority(auth: &Authority) -> &str {
auth.as_str()
.rsplit('@')
.next()
.expect("split always has at least 1 item")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::TestClient;
use axum::{routing::get, Router};
use http::{header::HeaderName, Request};
fn test_client() -> TestClient {
async fn host_as_body(Host(host): Host) -> String {
host
}
TestClient::new(Router::new().route("/", get(host_as_body)))
}
#[crate::test]
async fn host_header() {
let original_host = "some-domain:123";
let host = test_client()
.get("/")
.header(http::header::HOST, original_host)
.await
.text()
.await;
assert_eq!(host, original_host);
}
#[crate::test]
async fn x_forwarded_host_header() {
let original_host = "some-domain:456";
let host = test_client()
.get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, original_host)
.await
.text()
.await;
assert_eq!(host, original_host);
}
#[crate::test]
async fn x_forwarded_host_precedence_over_host_header() {
let x_forwarded_host_header = "some-domain:456";
let host_header = "some-domain:123";
let host = test_client()
.get("/")
.header(X_FORWARDED_HOST_HEADER_KEY, x_forwarded_host_header)
.header(http::header::HOST, host_header)
.await
.text()
.await;
assert_eq!(host, x_forwarded_host_header);
}
#[crate::test]
async fn uri_host() {
let client = test_client();
let port = client.server_port();
let host = client.get("/").await.text().await;
assert_eq!(host, format!("127.0.0.1:{port}"));
}
#[crate::test]
async fn ip4_uri_host() {
let mut parts = Request::new(()).into_parts().0;
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
let host = parts.extract::<Host>().await.unwrap();
assert_eq!(host.0, "127.0.0.1:1234");
}
#[crate::test]
async fn ip6_uri_host() {
let mut parts = Request::new(()).into_parts().0;
parts.uri = "http://cool:user@[::1]:456/file.txt".parse().unwrap();
let host = parts.extract::<Host>().await.unwrap();
assert_eq!(host.0, "[::1]:456");
}
#[crate::test]
async fn missing_host() {
let mut parts = Request::new(()).into_parts().0;
let host = parts.extract::<Host>().await.unwrap_err();
assert!(matches!(host, HostRejection::FailedToResolveHost(_)));
}
#[crate::test]
async fn optional_extractor() {
let mut parts = Request::new(()).into_parts().0;
parts.uri = "https://127.0.0.1:1234/image.jpg".parse().unwrap();
let host = parts.extract::<Option<Host>>().await.unwrap();
assert!(host.is_some());
}
#[crate::test]
async fn optional_extractor_none() {
let mut parts = Request::new(()).into_parts().0;
let host = parts.extract::<Option<Host>>().await.unwrap();
assert!(host.is_none());
}
#[test]
fn forwarded_parsing() {
// the basic case
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "192.0.2.60");
// is case insensitive
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "192.0.2.60");
// ipv6
let headers = header_map(&[(FORWARDED, "host=\"[2001:db8:cafe::17]:4711\"")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "[2001:db8:cafe::17]:4711");
// multiple values in one header
let headers = header_map(&[(FORWARDED, "host=192.0.2.60, host=127.0.0.1")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "192.0.2.60");
// multiple header values
let headers = header_map(&[
(FORWARDED, "host=192.0.2.60"),
(FORWARDED, "host=127.0.0.1"),
]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "192.0.2.60");
}
fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap {
let mut headers = HeaderMap::new();
for (key, value) in values {
headers.append(key, value.parse().unwrap());
}
headers
}
}
@@ -0,0 +1,428 @@
use axum::extract::{FromRequest, Request};
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use axum_core::extract::rejection::BytesRejection;
use bytes::Bytes;
use http::{header, HeaderMap};
use serde_core::Deserialize;
use std::marker::PhantomData;
/// JSON Extractor for zero-copy deserialization.
///
/// Deserialize request bodies into some type that implements [`serde::Deserialize<'de>`][serde::Deserialize].
/// Parsing JSON is delayed until [`deserialize`](JsonDeserializer::deserialize) is called.
/// If the type implements [`serde::de::DeserializeOwned`], the [`Json`](axum::Json) extractor should
/// be preferred.
///
/// The request will be rejected (and a [`JsonDeserializerRejection`] will be returned) if:
///
/// - The request doesn't have a `Content-Type: application/json` (or similar) header.
/// - Buffering the request body fails.
///
/// Additionally, a `JsonRejection` error will be returned, when calling `deserialize` if:
///
/// - The body doesn't contain syntactically valid JSON.
/// - The body contains syntactically valid JSON, but it couldn't be deserialized into the target type.
/// - Attempting to deserialize escaped JSON into a type that must be borrowed (e.g. `&'a str`).
///
/// ⚠️ `serde` will implicitly try to borrow for `&str` and `&[u8]` types, but will error if the
/// input contains escaped characters. Use `Cow<'a, str>` or `Cow<'a, [u8]>`, with the
/// `#[serde(borrow)]` attribute, to allow serde to fall back to an owned type when encountering
/// escaped characters.
///
/// ⚠️ Since parsing JSON requires consuming the request body, the `Json` extractor must be
/// *last* if there are multiple extractors in a handler.
/// See ["the order of extractors"][order-of-extractors]
///
/// [order-of-extractors]: axum::extract#the-order-of-extractors
///
/// See [`JsonDeserializerRejection`] for more details.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{
/// routing::post,
/// Router,
/// response::{IntoResponse, Response}
/// };
/// use axum_extra::extract::JsonDeserializer;
/// use serde::Deserialize;
/// use std::borrow::Cow;
/// use http::StatusCode;
///
/// #[derive(Deserialize)]
/// struct Data<'a> {
/// #[serde(borrow)]
/// borrow_text: Cow<'a, str>,
/// #[serde(borrow)]
/// borrow_bytes: Cow<'a, [u8]>,
/// borrow_dangerous: &'a str,
/// not_borrowed: String,
/// }
///
/// async fn upload(deserializer: JsonDeserializer<Data<'_>>) -> Response {
/// let data = match deserializer.deserialize() {
/// Ok(data) => data,
/// Err(e) => return e.into_response(),
/// };
///
/// // payload is a `Data` with borrowed data from `deserializer`,
/// // which owns the request body (`Bytes`).
///
/// StatusCode::OK.into_response()
/// }
///
/// let app = Router::new().route("/upload", post(upload));
/// # let _: Router = app;
/// ```
#[derive(Debug, Clone, Default)]
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
pub struct JsonDeserializer<T> {
bytes: Bytes,
_marker: PhantomData<T>,
}
impl<T, S> FromRequest<S> for JsonDeserializer<T>
where
T: Deserialize<'static>,
S: Send + Sync,
{
type Rejection = JsonDeserializerRejection;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
if json_content_type(req.headers()) {
let bytes = Bytes::from_request(req, state).await?;
Ok(Self {
bytes,
_marker: PhantomData,
})
} else {
Err(MissingJsonContentType.into())
}
}
}
impl<'de, 'a: 'de, T> JsonDeserializer<T>
where
T: Deserialize<'de>,
{
/// Deserialize the request body into the target type.
/// See [`JsonDeserializer`] for more details.
pub fn deserialize(&'a self) -> Result<T, JsonDeserializerRejection> {
let deserializer = &mut serde_json::Deserializer::from_slice(&self.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(value)
}
}
define_rejection! {
#[status = UNPROCESSABLE_ENTITY]
#[body = "Failed to deserialize the JSON body into the target type"]
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
/// Rejection type for [`JsonDeserializer`].
///
/// This rejection is used if the request body is syntactically valid JSON but couldn't be
/// deserialized into the target type.
pub struct JsonDataError(Error);
}
define_rejection! {
#[status = BAD_REQUEST]
#[body = "Failed to parse the request body as JSON"]
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
/// Rejection type for [`JsonDeserializer`].
///
/// This rejection is used if the request body didn't contain syntactically valid JSON.
pub struct JsonSyntaxError(Error);
}
define_rejection! {
#[status = UNSUPPORTED_MEDIA_TYPE]
#[body = "Expected request with `Content-Type: application/json`"]
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
/// Rejection type for [`JsonDeserializer`] used if the `Content-Type`
/// header is missing.
pub struct MissingJsonContentType;
}
composite_rejection! {
/// Rejection used for [`JsonDeserializer`].
///
/// Contains one variant for each way the [`JsonDeserializer`] extractor
/// can fail.
#[cfg_attr(docsrs, doc(cfg(feature = "json-deserializer")))]
pub enum JsonDeserializerRejection {
JsonDataError,
JsonSyntaxError,
MissingJsonContentType,
BytesRejection,
}
}
fn json_content_type(headers: &HeaderMap) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
content_type
} else {
return false;
};
let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
return false;
};
let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
mime
} else {
return false;
};
let is_json_content_type = mime.type_() == "application"
&& (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"));
is_json_content_type
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{
response::{IntoResponse, Response},
routing::post,
Router,
};
use http::StatusCode;
use serde::Deserialize;
use serde_json::{json, Value};
use std::borrow::Cow;
#[tokio::test]
async fn deserialize_body() {
#[derive(Debug, Deserialize)]
struct Input<'a> {
#[serde(borrow)]
foo: Cow<'a, str>,
}
async fn handler(deserializer: JsonDeserializer<Input<'_>>) -> Response {
match deserializer.deserialize() {
Ok(input) => {
assert!(matches!(input.foo, Cow::Borrowed(_)));
input.foo.into_owned().into_response()
}
Err(e) => e.into_response(),
}
}
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").json(&json!({ "foo": "bar" })).await;
let body = res.text().await;
assert_eq!(body, "bar");
}
#[tokio::test]
async fn deserialize_body_escaped_to_cow() {
#[derive(Debug, Deserialize)]
struct Input<'a> {
#[serde(borrow)]
foo: Cow<'a, str>,
}
async fn handler(deserializer: JsonDeserializer<Input<'_>>) -> Response {
match deserializer.deserialize() {
Ok(Input { foo }) => {
let Cow::Owned(foo) = foo else {
panic!("Deserializer is expected to fallback to Cow::Owned when encountering escaped characters")
};
foo.into_response()
}
Err(e) => e.into_response(),
}
}
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
// The escaped characters prevent serde_json from borrowing.
let res = client.post("/").json(&json!({ "foo": "\"bar\"" })).await;
let body = res.text().await;
assert_eq!(body, r#""bar""#);
}
#[tokio::test]
async fn deserialize_body_escaped_to_str() {
#[derive(Debug, Deserialize)]
struct Input<'a> {
// Explicit `#[serde(borrow)]` attribute is not required for `&str` or &[u8].
// See: https://serde.rs/lifetimes.html#borrowing-data-in-a-derived-impl
foo: &'a str,
}
async fn handler(deserializer: JsonDeserializer<Input<'_>>) -> Response {
match deserializer.deserialize() {
Ok(Input { foo }) => foo.to_owned().into_response(),
Err(e) => e.into_response(),
}
}
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").json(&json!({ "foo": "good" })).await;
let body = res.text().await;
assert_eq!(body, "good");
let res = client.post("/").json(&json!({ "foo": "\"bad\"" })).await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body_text = res.text().await;
assert_eq!(
body_text,
"Failed to deserialize the JSON body into the target type: foo: invalid type: string \"\\\"bad\\\"\", expected a borrowed string at line 1 column 16"
);
}
#[tokio::test]
async fn consume_body_to_json_requires_json_content_type() {
#[derive(Debug, Deserialize)]
struct Input<'a> {
#[allow(dead_code)]
foo: Cow<'a, str>,
}
async fn handler(_deserializer: JsonDeserializer<Input<'_>>) -> Response {
panic!("This handler should not be called")
}
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client.post("/").body(r#"{ "foo": "bar" }"#).await;
let status = res.status();
assert_eq!(status, StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
#[tokio::test]
async fn json_content_types() {
async fn valid_json_content_type(content_type: &str) -> bool {
println!("testing {content_type:?}");
async fn handler(_deserializer: JsonDeserializer<Value>) -> Response {
StatusCode::OK.into_response()
}
let app = Router::new().route("/", post(handler));
let res = TestClient::new(app)
.post("/")
.header("content-type", content_type)
.body("{}")
.await;
res.status() == StatusCode::OK
}
assert!(valid_json_content_type("application/json").await);
assert!(valid_json_content_type("application/json; charset=utf-8").await);
assert!(valid_json_content_type("application/json;charset=utf-8").await);
assert!(valid_json_content_type("application/cloudevents+json").await);
assert!(!valid_json_content_type("text/json").await);
}
#[tokio::test]
async fn invalid_json_syntax() {
async fn handler(deserializer: JsonDeserializer<Value>) -> Response {
match deserializer.deserialize() {
Ok(_) => panic!("Should have matched `Err`"),
Err(e) => e.into_response(),
}
}
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client
.post("/")
.body("{")
.header("content-type", "application/json")
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[derive(Deserialize)]
struct Foo {
#[allow(dead_code)]
a: i32,
#[allow(dead_code)]
b: Vec<Bar>,
}
#[derive(Deserialize)]
struct Bar {
#[allow(dead_code)]
x: i32,
#[allow(dead_code)]
y: i32,
}
#[tokio::test]
async fn invalid_json_data() {
async fn handler(deserializer: JsonDeserializer<Foo>) -> Response {
match deserializer.deserialize() {
Ok(_) => panic!("Should have matched `Err`"),
Err(e) => e.into_response(),
}
}
let app = Router::new().route("/", post(handler));
let client = TestClient::new(app);
let res = client
.post("/")
.body("{\"a\": 1, \"b\": [{\"x\": 2}]}")
.header("content-type", "application/json")
.await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let body_text = res.text().await;
assert_eq!(
body_text,
"Failed to deserialize the JSON body into the target type: b[0]: missing field `y` at line 1 column 23"
);
}
}
+67
View File
@@ -0,0 +1,67 @@
//! Additional extractors.
mod cached;
mod host;
mod optional_path;
pub mod rejection;
mod with_rejection;
#[cfg(feature = "form")]
mod form;
#[cfg(feature = "cookie")]
pub mod cookie;
#[cfg(feature = "json-deserializer")]
mod json_deserializer;
#[cfg(feature = "query")]
mod query;
#[cfg(feature = "multipart")]
pub mod multipart;
#[cfg(feature = "scheme")]
mod scheme;
#[allow(deprecated)]
pub use self::optional_path::OptionalPath;
pub use self::{cached::Cached, host::Host, with_rejection::WithRejection};
#[cfg(feature = "cookie")]
pub use self::cookie::CookieJar;
#[cfg(feature = "cookie-private")]
pub use self::cookie::PrivateCookieJar;
#[cfg(feature = "cookie-signed")]
pub use self::cookie::SignedCookieJar;
#[cfg(feature = "form")]
pub use self::form::{Form, FormRejection};
#[cfg(feature = "query")]
pub use self::query::OptionalQuery;
#[cfg(feature = "query")]
pub use self::query::{OptionalQueryRejection, Query, QueryRejection};
#[cfg(feature = "multipart")]
pub use self::multipart::Multipart;
#[cfg(feature = "scheme")]
#[doc(no_inline)]
pub use self::scheme::{Scheme, SchemeMissing};
#[cfg(feature = "json-deserializer")]
pub use self::json_deserializer::{
JsonDataError, JsonDeserializer, JsonDeserializerRejection, JsonSyntaxError,
MissingJsonContentType,
};
#[cfg(feature = "json-lines")]
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
#[cfg(feature = "typed-header")]
#[doc(no_inline)]
pub use crate::typed_header::TypedHeader;
+446
View File
@@ -0,0 +1,446 @@
//! Extractor that parses `multipart/form-data` requests commonly used with file uploads.
//!
//! See [`Multipart`] for more details.
use axum::{
body::{Body, Bytes},
extract::FromRequest,
response::{IntoResponse, Response},
RequestExt,
};
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use futures_util::stream::Stream;
use http::{
header::{HeaderMap, CONTENT_TYPE},
Request, StatusCode,
};
use std::{
error::Error,
fmt,
pin::Pin,
task::{Context, Poll},
};
/// Extractor that parses `multipart/form-data` requests (commonly used with file uploads).
///
/// ⚠️ Since extracting multipart form data from the request requires consuming the body, the
/// `Multipart` extractor must be *last* if there are multiple extractors in a handler.
/// See ["the order of extractors"][order-of-extractors]
///
/// [order-of-extractors]: crate::extract#the-order-of-extractors
///
/// # Example
///
/// ```
/// use axum::{
/// routing::post,
/// Router,
/// };
/// use axum_extra::extract::Multipart;
///
/// async fn upload(mut multipart: Multipart) {
/// while let Some(mut field) = multipart.next_field().await.unwrap() {
/// let name = field.name().unwrap().to_string();
/// let data = field.bytes().await.unwrap();
///
/// println!("Length of `{}` is {} bytes", name, data.len());
/// }
/// }
///
/// let app = Router::new().route("/upload", post(upload));
/// # let _: Router = app;
/// ```
///
/// # Field Exclusivity
///
/// A [`Field`] represents a raw, self-decoding stream into multipart data. As such, only one
/// [`Field`] from a given Multipart instance may be live at once. That is, a [`Field`] emitted by
/// [`next_field()`] must be dropped before calling [`next_field()`] again. Failure to do so will
/// result in an error.
///
/// ```
/// use axum_extra::extract::Multipart;
///
/// async fn handler(mut multipart: Multipart) {
/// let field_1 = multipart.next_field().await;
///
/// // We cannot get the next field while `field_1` is still alive. Have to drop `field_1`
/// // first.
/// let field_2 = multipart.next_field().await;
/// assert!(field_2.is_err());
/// }
/// ```
///
/// In general you should consume `Multipart` by looping over the fields in order and make sure not
/// to keep `Field`s around from previous loop iterations. That will minimize the risk of runtime
/// errors.
///
/// # Differences between this and `axum::extract::Multipart`
///
/// `axum::extract::Multipart` uses lifetimes to enforce field exclusivity at compile time, however
/// that leads to significant usability issues such as `Field` not being `'static`.
///
/// `axum_extra::extract::Multipart` instead enforces field exclusivity at runtime which makes
/// things easier to use at the cost of possible runtime errors.
///
/// [`next_field()`]: Multipart::next_field
#[cfg_attr(docsrs, doc(cfg(feature = "multipart")))]
#[derive(Debug)]
pub struct Multipart {
inner: multer::Multipart<'static>,
}
impl<S> FromRequest<S> for Multipart
where
S: Send + Sync,
{
type Rejection = MultipartRejection;
async fn from_request(req: Request<Body>, _state: &S) -> Result<Self, Self::Rejection> {
let boundary = parse_boundary(req.headers()).ok_or(InvalidBoundary)?;
let stream = req.with_limited_body().into_body();
let multipart = multer::Multipart::new(stream.into_data_stream(), boundary);
Ok(Self { inner: multipart })
}
}
impl Multipart {
/// Yields the next [`Field`] if available.
pub async fn next_field(&mut self) -> Result<Option<Field>, MultipartError> {
let field = self
.inner
.next_field()
.await
.map_err(MultipartError::from_multer)?;
if let Some(field) = field {
Ok(Some(Field { inner: field }))
} else {
Ok(None)
}
}
/// Convert the `Multipart` into a stream of its fields.
pub fn into_stream(self) -> impl Stream<Item = Result<Field, MultipartError>> + Send + 'static {
futures_util::stream::try_unfold(self, |mut multipart| async move {
let field = multipart.next_field().await?;
Ok(field.map(|field| (field, multipart)))
})
}
}
/// A single field in a multipart stream.
#[derive(Debug)]
pub struct Field {
inner: multer::Field<'static>,
}
impl Stream for Field {
type Item = Result<Bytes, MultipartError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.inner)
.poll_next(cx)
.map_err(MultipartError::from_multer)
}
}
impl Field {
/// The field name found in the
/// [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
/// header.
#[must_use]
pub fn name(&self) -> Option<&str> {
self.inner.name()
}
/// The file name found in the
/// [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition)
/// header.
#[must_use]
pub fn file_name(&self) -> Option<&str> {
self.inner.file_name()
}
/// Get the [content type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) of the field.
#[must_use]
pub fn content_type(&self) -> Option<&str> {
self.inner.content_type().map(|m| m.as_ref())
}
/// Get a map of headers as [`HeaderMap`].
#[must_use]
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
/// Get the full data of the field as [`Bytes`].
pub async fn bytes(self) -> Result<Bytes, MultipartError> {
self.inner
.bytes()
.await
.map_err(MultipartError::from_multer)
}
/// Get the full field data as text.
pub async fn text(self) -> Result<String, MultipartError> {
self.inner.text().await.map_err(MultipartError::from_multer)
}
/// Stream a chunk of the field data.
///
/// When the field data has been exhausted, this will return [`None`].
///
/// Note this does the same thing as `Field`'s [`Stream`] implementation.
///
/// # Example
///
/// ```
/// use axum::{
/// routing::post,
/// response::IntoResponse,
/// http::StatusCode,
/// Router,
/// };
/// use axum_extra::extract::Multipart;
///
/// async fn upload(mut multipart: Multipart) -> Result<(), (StatusCode, String)> {
/// while let Some(mut field) = multipart
/// .next_field()
/// .await
/// .map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?
/// {
/// while let Some(chunk) = field
/// .chunk()
/// .await
/// .map_err(|err| (StatusCode::BAD_REQUEST, err.to_string()))?
/// {
/// println!("received {} bytes", chunk.len());
/// }
/// }
///
/// Ok(())
/// }
///
/// let app = Router::new().route("/upload", post(upload));
/// # let _: Router = app;
/// ```
pub async fn chunk(&mut self) -> Result<Option<Bytes>, MultipartError> {
self.inner
.chunk()
.await
.map_err(MultipartError::from_multer)
}
}
/// Errors associated with parsing `multipart/form-data` requests.
#[derive(Debug)]
pub struct MultipartError {
source: multer::Error,
}
impl MultipartError {
fn from_multer(multer: multer::Error) -> Self {
Self { source: multer }
}
/// Get the response body text used for this rejection.
pub fn body_text(&self) -> String {
let body = self.source.to_string();
axum_core::__log_rejection!(
rejection_type = Self,
body_text = body,
status = self.status(),
);
body
}
/// Get the status code used for this rejection.
#[must_use]
pub fn status(&self) -> http::StatusCode {
status_code_from_multer_error(&self.source)
}
}
fn status_code_from_multer_error(err: &multer::Error) -> StatusCode {
match err {
multer::Error::UnknownField { .. }
| multer::Error::IncompleteFieldData { .. }
| multer::Error::IncompleteHeaders
| multer::Error::ReadHeaderFailed(..)
| multer::Error::DecodeHeaderName { .. }
| multer::Error::DecodeContentType(..)
| multer::Error::NoBoundary
| multer::Error::DecodeHeaderValue { .. }
| multer::Error::NoMultipart
| multer::Error::IncompleteStream => StatusCode::BAD_REQUEST,
multer::Error::FieldSizeExceeded { .. } | multer::Error::StreamSizeExceeded { .. } => {
StatusCode::PAYLOAD_TOO_LARGE
}
multer::Error::StreamReadFailed(err) => {
if let Some(err) = err.downcast_ref::<multer::Error>() {
return status_code_from_multer_error(err);
}
if err
.downcast_ref::<axum::Error>()
.and_then(|err| err.source())
.and_then(|err| err.downcast_ref::<http_body_util::LengthLimitError>())
.is_some()
{
return StatusCode::PAYLOAD_TOO_LARGE;
}
StatusCode::INTERNAL_SERVER_ERROR
}
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
impl IntoResponse for MultipartError {
fn into_response(self) -> Response {
(self.status(), self.body_text()).into_response()
}
}
impl fmt::Display for MultipartError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Error parsing `multipart/form-data` request")
}
}
impl std::error::Error for MultipartError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.source)
}
}
fn parse_boundary(headers: &HeaderMap) -> Option<String> {
let content_type = headers.get(CONTENT_TYPE)?.to_str().ok()?;
multer::parse_boundary(content_type).ok()
}
composite_rejection! {
/// Rejection used for [`Multipart`].
///
/// Contains one variant for each way the [`Multipart`] extractor can fail.
pub enum MultipartRejection {
InvalidBoundary,
}
}
define_rejection! {
#[status = BAD_REQUEST]
#[body = "Invalid `boundary` for `multipart/form-data` request"]
/// Rejection type used if the `boundary` in a `multipart/form-data` is
/// missing or invalid.
pub struct InvalidBoundary;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{extract::DefaultBodyLimit, routing::post, Router};
#[tokio::test]
async fn content_type_with_encoding() {
const BYTES: &[u8] = "<!doctype html><title>🦀</title>".as_bytes();
const FILE_NAME: &str = "index.html";
const CONTENT_TYPE: &str = "text/html; charset=utf-8";
async fn handle(mut multipart: Multipart) -> impl IntoResponse {
let field = multipart.next_field().await.unwrap().unwrap();
assert_eq!(field.file_name().unwrap(), FILE_NAME);
assert_eq!(field.content_type().unwrap(), CONTENT_TYPE);
assert_eq!(field.bytes().await.unwrap(), BYTES);
assert!(multipart.next_field().await.unwrap().is_none());
}
let app = Router::new().route("/", post(handle));
let client = TestClient::new(app);
let form = reqwest::multipart::Form::new().part(
"file",
reqwest::multipart::Part::bytes(BYTES)
.file_name(FILE_NAME)
.mime_str(CONTENT_TYPE)
.unwrap(),
);
client.post("/").multipart(form).await;
}
// No need for this to be a #[test], we just want to make sure it compiles
fn _multipart_from_request_limited() {
async fn handler(_: Multipart) {}
let _app: Router<()> = Router::new().route("/", post(handler));
}
#[tokio::test]
async fn body_too_large() {
const BYTES: &[u8] = "<!doctype html><title>🦀</title>".as_bytes();
async fn handle(mut multipart: Multipart) -> Result<(), MultipartError> {
while let Some(field) = multipart.next_field().await? {
field.bytes().await?;
}
Ok(())
}
let app = Router::new()
.route("/", post(handle))
.layer(DefaultBodyLimit::max(BYTES.len() - 1));
let client = TestClient::new(app);
let form =
reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(BYTES));
let res = client.post("/").multipart(form).await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
#[cfg(feature = "tracing")]
async fn body_too_large_with_tracing() {
const BYTES: &[u8] = "<!doctype html><title>🦀</title>".as_bytes();
async fn handle(mut multipart: Multipart) -> impl IntoResponse {
let result: Result<(), MultipartError> = async {
while let Some(field) = multipart.next_field().await? {
field.bytes().await?;
}
Ok(())
}
.await;
let subscriber = tracing_subscriber::FmtSubscriber::builder()
.with_max_level(tracing::level_filters::LevelFilter::TRACE)
.with_writer(std::io::sink)
.finish();
let guard = tracing::subscriber::set_default(subscriber);
let response = result.into_response();
drop(guard);
response
}
let app = Router::new()
.route("/", post(handle))
.layer(DefaultBodyLimit::max(BYTES.len() - 1));
let client = TestClient::new(app);
let form =
reqwest::multipart::Form::new().part("file", reqwest::multipart::Part::bytes(BYTES));
let res = client.post("/").multipart(form).await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
}
@@ -0,0 +1,98 @@
use axum::{
extract::{rejection::PathRejection, FromRequestParts, Path},
RequestPartsExt,
};
use serde_core::de::DeserializeOwned;
/// Extractor that extracts path arguments the same way as [`Path`], except if there aren't any.
///
/// This extractor can be used in place of `Path` when you have two routes that you want to handle
/// in mostly the same way, where one has a path parameter and the other one doesn't.
///
/// # Example
///
/// ```
/// use std::num::NonZeroU32;
/// use axum::{
/// response::IntoResponse,
/// routing::get,
/// Router,
/// };
/// use axum_extra::extract::OptionalPath;
///
/// async fn render_blog(OptionalPath(page): OptionalPath<NonZeroU32>) -> impl IntoResponse {
/// // Convert to u32, default to page 1 if not specified
/// let page = page.map_or(1, |param| param.get());
/// // ...
/// }
///
/// let app = Router::new()
/// .route("/blog", get(render_blog))
/// .route("/blog/{page}", get(render_blog));
/// # let app: Router = app;
/// ```
#[deprecated = "Use Option<Path<_>> instead"]
#[derive(Debug)]
pub struct OptionalPath<T>(pub Option<T>);
#[allow(deprecated)]
impl<T, S> FromRequestParts<S> for OptionalPath<T>
where
T: DeserializeOwned + Send + 'static,
S: Send + Sync,
{
type Rejection = PathRejection;
async fn from_request_parts(
parts: &mut http::request::Parts,
_: &S,
) -> Result<Self, Self::Rejection> {
parts
.extract::<Option<Path<T>>>()
.await
.map(|opt| Self(opt.map(|Path(x)| x)))
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests {
use std::num::NonZeroU32;
use axum::{routing::get, Router};
use super::OptionalPath;
use crate::test_helpers::TestClient;
#[crate::test]
async fn supports_128_bit_numbers() {
async fn handle(OptionalPath(param): OptionalPath<NonZeroU32>) -> String {
let num = param.map_or(0, |p| p.get());
format!("Success: {num}")
}
let app = Router::new()
.route("/", get(handle))
.route("/{num}", get(handle));
let client = TestClient::new(app);
let res = client.get("/").await;
assert_eq!(res.text().await, "Success: 0");
let res = client.get("/1").await;
assert_eq!(res.text().await, "Success: 1");
let res = client.get("/0").await;
assert_eq!(
res.text().await,
"Invalid URL: invalid value: integer `0`, expected a nonzero u32"
);
let res = client.get("/NaN").await;
assert_eq!(
res.text().await,
"Invalid URL: Cannot parse `NaN` to a `u32`"
);
}
}
+402
View File
@@ -0,0 +1,402 @@
use axum::extract::FromRequestParts;
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use http::{request::Parts, Uri};
use serde_core::de::DeserializeOwned;
/// Extractor that deserializes query strings into some type.
///
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Differences from `axum::extract::Query`
///
/// This extractor uses [`serde_html_form`] under-the-hood which supports multi-value items. These
/// are sent by multiple `<input>` attributes of the same name (e.g. checkboxes) and `<select>`s
/// with the `multiple` attribute. Those values can be collected into a `Vec` or other sequential
/// container.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{routing::get, Router};
/// use axum_extra::extract::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Pagination {
/// page: usize,
/// per_page: usize,
/// }
///
/// // This will parse query strings like `?page=2&per_page=30` into `Pagination`
/// // structs.
/// async fn list_things(pagination: Query<Pagination>) {
/// let pagination: Pagination = pagination.0;
///
/// // ...
/// }
///
/// let app = Router::new().route("/list_things", get(list_things));
/// # let _: Router = app;
/// ```
///
/// If the query string cannot be parsed it will reject the request with a `400
/// Bad Request` response.
///
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
/// example.
///
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
///
/// While `Option<T>` will handle empty parameters (e.g. `param=`), beware when using this with a
/// `Vec<T>`. If your list is optional, use `Vec<T>` in combination with `#[serde(default)]`
/// instead of `Option<Vec<T>>`. `Option<Vec<T>>` will handle 0, 2, or more arguments, but not one
/// argument.
///
/// # Example
///
/// ```rust,no_run
/// use axum::{routing::get, Router};
/// use axum_extra::extract::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Params {
/// #[serde(default)]
/// items: Vec<usize>,
/// }
///
/// // This will parse 0 occurrences of `items` as an empty `Vec`.
/// async fn process_items(Query(params): Query<Params>) {
/// // ...
/// }
///
/// let app = Router::new().route("/process_items", get(process_items));
/// # let _: Router = app;
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T);
impl<T, S> FromRequestParts<S> for Query<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = QueryRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let query = parts.uri.query().unwrap_or_default();
let deserializer =
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
let value = serde_path_to_error::deserialize(deserializer)
.map_err(FailedToDeserializeQueryString::from_err)?;
Ok(Query(value))
}
}
impl<T> Query<T>
where
T: DeserializeOwned,
{
/// Attempts to construct a [`Query`] from a reference to a [`Uri`].
///
/// # Example
/// ```
/// use axum_extra::extract::Query;
/// use http::Uri;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct ExampleParams {
/// foo: String,
/// bar: u32,
/// }
///
/// let uri: Uri = "http://example.com/path?foo=hello&bar=42".parse().unwrap();
/// let result: Query<ExampleParams> = Query::try_from_uri(&uri).unwrap();
/// assert_eq!(result.foo, String::from("hello"));
/// assert_eq!(result.bar, 42);
/// ```
pub fn try_from_uri(value: &Uri) -> Result<Self, QueryRejection> {
let query = value.query().unwrap_or_default();
let params =
serde_html_form::from_str(query).map_err(FailedToDeserializeQueryString::from_err)?;
Ok(Self(params))
}
}
axum_core::__impl_deref!(Query);
define_rejection! {
#[status = BAD_REQUEST]
#[body = "Failed to deserialize query string"]
/// Rejection type used if the [`Query`] extractor is unable to
/// deserialize the query string into the target type.
pub struct FailedToDeserializeQueryString(Error);
}
composite_rejection! {
/// Rejection used for [`Query`].
///
/// Contains one variant for each way the [`Query`] extractor can fail.
pub enum QueryRejection {
FailedToDeserializeQueryString,
}
}
/// Extractor that deserializes query strings into `None` if no query parameters are present.
///
/// Otherwise behaviour is identical to [`Query`].
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Example
///
/// ```rust,no_run
/// use axum::{routing::get, Router};
/// use axum_extra::extract::OptionalQuery;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Pagination {
/// page: usize,
/// per_page: usize,
/// }
///
/// // This will parse query strings like `?page=2&per_page=30` into `Some(Pagination)` and
/// // empty query string into `None`
/// async fn list_things(OptionalQuery(pagination): OptionalQuery<Pagination>) {
/// match pagination {
/// Some(Pagination{ page, per_page }) => { /* return specified page */ },
/// None => { /* return fist page */ }
/// }
/// // ...
/// }
///
/// let app = Router::new().route("/list_things", get(list_things));
/// # let _: Router = app;
/// ```
///
/// If the query string cannot be parsed it will reject the request with a `400
/// Bad Request` response.
///
/// For handling values being empty vs missing see the [query-params-with-empty-strings][example]
/// example.
///
/// [example]: https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct OptionalQuery<T>(pub Option<T>);
impl<T, S> FromRequestParts<S> for OptionalQuery<T>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = OptionalQueryRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
if let Some(query) = parts.uri.query() {
let deserializer =
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
let value = serde_path_to_error::deserialize(deserializer)
.map_err(FailedToDeserializeQueryString::from_err)?;
Ok(OptionalQuery(Some(value)))
} else {
Ok(OptionalQuery(None))
}
}
}
impl<T> std::ops::Deref for OptionalQuery<T> {
type Target = Option<T>;
#[inline]
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> std::ops::DerefMut for OptionalQuery<T> {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
composite_rejection! {
/// Rejection used for [`OptionalQuery`].
///
/// Contains one variant for each way the [`OptionalQuery`] extractor can fail.
pub enum OptionalQueryRejection {
FailedToDeserializeQueryString,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::routing::{get, post};
use axum::Router;
use http::header::CONTENT_TYPE;
use http::StatusCode;
use serde::Deserialize;
#[tokio::test]
async fn query_supports_multiple_values() {
#[derive(Deserialize)]
struct Data {
#[serde(rename = "value")]
values: Vec<String>,
}
let app = Router::new().route(
"/",
post(|Query(data): Query<Data>| async move { data.values.join(",") }),
);
let client = TestClient::new(app);
let res = client
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "one,two");
}
#[tokio::test]
async fn correct_rejection_status_code() {
#[derive(Deserialize)]
#[allow(dead_code)]
struct Params {
n: i32,
}
async fn handler(_: Query<Params>) {}
let app = Router::new().route("/", get(handler));
let client = TestClient::new(app);
let res = client.get("/?n=hi").await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(
res.text().await,
"Failed to deserialize query string: n: invalid digit found in string"
);
}
#[tokio::test]
async fn optional_query_supports_multiple_values() {
#[derive(Deserialize)]
struct Data {
#[serde(rename = "value")]
values: Vec<String>,
}
let app = Router::new().route(
"/",
post(|OptionalQuery(data): OptionalQuery<Data>| async move {
data.map(|Data { values }| values.join(","))
.unwrap_or("None".to_owned())
}),
);
let client = TestClient::new(app);
let res = client
.post("/?value=one&value=two")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "one,two");
}
#[tokio::test]
async fn optional_query_deserializes_no_parameters_into_none() {
#[derive(Deserialize)]
struct Data {
value: String,
}
let app = Router::new().route(
"/",
post(|OptionalQuery(data): OptionalQuery<Data>| async move {
match data {
None => "None".into(),
Some(data) => data.value,
}
}),
);
let client = TestClient::new(app);
let res = client.post("/").body("").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "None");
}
#[tokio::test]
async fn optional_query_preserves_parsing_errors() {
#[derive(Deserialize)]
struct Data {
value: String,
}
let app = Router::new().route(
"/",
post(|OptionalQuery(data): OptionalQuery<Data>| async move {
match data {
None => "None".into(),
Some(data) => data.value,
}
}),
);
let client = TestClient::new(app);
let res = client
.post("/?other=something")
.header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.body("")
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_try_from_uri() {
#[derive(Deserialize)]
struct TestQueryParams {
foo: Vec<String>,
bar: u32,
}
let uri: Uri = "http://example.com/path?foo=hello&bar=42&foo=goodbye"
.parse()
.unwrap();
let result: Query<TestQueryParams> = Query::try_from_uri(&uri).unwrap();
assert_eq!(result.foo, [String::from("hello"), String::from("goodbye")]);
assert_eq!(result.bar, 42);
}
#[test]
fn test_try_from_uri_with_invalid_query() {
#[derive(Deserialize)]
struct TestQueryParams {
_foo: String,
_bar: u32,
}
let uri: Uri = "http://example.com/path?foo=hello&bar=invalid"
.parse()
.unwrap();
let result: Result<Query<TestQueryParams>, _> = Query::try_from_uri(&uri);
assert!(result.is_err());
}
}
@@ -0,0 +1,23 @@
//! Rejection response types.
use axum_core::{
__composite_rejection as composite_rejection, __define_rejection as define_rejection,
};
define_rejection! {
#[status = BAD_REQUEST]
#[body = "No host found in request"]
/// Rejection type used if the [`Host`](super::Host) extractor is unable to
/// resolve a host.
pub struct FailedToResolveHost;
}
composite_rejection! {
/// Rejection used for [`Host`](super::Host).
///
/// Contains one variant for each way the [`Host`](super::Host) extractor
/// can fail.
pub enum HostRejection {
FailedToResolveHost,
}
}
+147
View File
@@ -0,0 +1,147 @@
//! Extractor that parses the scheme of a request.
//! See [`Scheme`] for more details.
use axum::extract::FromRequestParts;
use axum_core::__define_rejection as define_rejection;
use http::{
header::{HeaderMap, FORWARDED},
request::Parts,
};
const X_FORWARDED_PROTO_HEADER_KEY: &str = "X-Forwarded-Proto";
/// Extractor that resolves the scheme / protocol of a request.
///
/// The scheme is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-Proto` header
/// - Request URI (If the request is an HTTP/2 request! e.g. use `--http2(-prior-knowledge)` with cURL)
///
/// Note that user agents can set the `X-Forwarded-Proto` header to arbitrary values so make
/// sure to validate them to avoid security issues.
#[derive(Debug, Clone)]
pub struct Scheme(pub String);
define_rejection! {
#[status = BAD_REQUEST]
#[body = "No scheme found in request"]
/// Rejection type used if the [`Scheme`] extractor is unable to
/// resolve a scheme.
pub struct SchemeMissing;
}
impl<S> FromRequestParts<S> for Scheme
where
S: Send + Sync,
{
type Rejection = SchemeMissing;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Within Forwarded header
if let Some(scheme) = parse_forwarded(&parts.headers) {
return Ok(Scheme(scheme.to_owned()));
}
// X-Forwarded-Proto
if let Some(scheme) = parts
.headers
.get(X_FORWARDED_PROTO_HEADER_KEY)
.and_then(|scheme| scheme.to_str().ok())
{
return Ok(Scheme(scheme.to_owned()));
}
// From parts of an HTTP/2 request
if let Some(scheme) = parts.uri.scheme_str() {
return Ok(Scheme(scheme.to_owned()));
}
Err(SchemeMissing)
}
}
fn parse_forwarded(headers: &HeaderMap) -> Option<&str> {
// if there are multiple `Forwarded` `HeaderMap::get` will return the first one
let forwarded_values = headers.get(FORWARDED)?.to_str().ok()?;
// get the first set of values
let first_value = forwarded_values.split(',').next()?;
// find the value of the `proto` field
first_value.split(';').find_map(|pair| {
let (key, value) = pair.split_once('=')?;
key.trim()
.eq_ignore_ascii_case("proto")
.then(|| value.trim().trim_matches('"'))
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::TestClient;
use axum::{routing::get, Router};
use http::header::HeaderName;
fn test_client() -> TestClient {
async fn scheme_as_body(Scheme(scheme): Scheme) -> String {
scheme
}
TestClient::new(Router::new().route("/", get(scheme_as_body)))
}
#[crate::test]
async fn forwarded_scheme_parsing() {
// the basic case
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;proto=http;by=203.0.113.43")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "http");
// is case insensitive
let headers = header_map(&[(FORWARDED, "host=192.0.2.60;PROTO=https;by=203.0.113.43")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "https");
// multiple values in one header
let headers = header_map(&[(FORWARDED, "proto=ftp, proto=https")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "ftp");
// multiple header values
let headers = header_map(&[(FORWARDED, "proto=ftp"), (FORWARDED, "proto=https")]);
let value = parse_forwarded(&headers).unwrap();
assert_eq!(value, "ftp");
}
#[crate::test]
async fn x_forwarded_scheme_header() {
let original_scheme = "https";
let scheme = test_client()
.get("/")
.header(X_FORWARDED_PROTO_HEADER_KEY, original_scheme)
.await
.text()
.await;
assert_eq!(scheme, original_scheme);
}
#[crate::test]
async fn precedence_forwarded_over_x_forwarded() {
let scheme = test_client()
.get("/")
.header(X_FORWARDED_PROTO_HEADER_KEY, "https")
.header(FORWARDED, "proto=ftp")
.await
.text()
.await;
assert_eq!(scheme, "ftp");
}
fn header_map(values: &[(HeaderName, &str)]) -> HeaderMap {
let mut headers = HeaderMap::new();
for (key, value) in values {
headers.append(key, value.parse().unwrap());
}
headers
}
}
@@ -0,0 +1,205 @@
use axum::extract::{FromRequest, FromRequestParts, Request};
use axum::response::IntoResponse;
use http::request::Parts;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
#[cfg(feature = "typed-routing")]
use crate::routing::TypedPath;
/// Extractor for customizing extractor rejections
///
/// `WithRejection` wraps another extractor and gives you the result. If the
/// extraction fails, the `Rejection` is transformed into `R` and returned as a
/// response
///
/// `E` is expected to implement [`FromRequest`]
///
/// `R` is expected to implement [`IntoResponse`] and [`From<E::Rejection>`]
///
///
/// # Example
///
/// ```rust
/// use axum::extract::rejection::JsonRejection;
/// use axum::response::{Response, IntoResponse};
/// use axum::Json;
/// use axum_extra::extract::WithRejection;
/// use serde::Deserialize;
///
/// struct MyRejection { /* ... */ }
///
/// impl From<JsonRejection> for MyRejection {
/// fn from(rejection: JsonRejection) -> MyRejection {
/// // ...
/// # todo!()
/// }
/// }
///
/// impl IntoResponse for MyRejection {
/// fn into_response(self) -> Response {
/// // ...
/// # todo!()
/// }
/// }
/// #[derive(Debug, Deserialize)]
/// struct Person { /* ... */ }
///
/// async fn handler(
/// // If the `Json` extractor ever fails, `MyRejection` will be sent to the
/// // client using the `IntoResponse` impl
/// WithRejection(Json(Person), _): WithRejection<Json<Person>, MyRejection>
/// ) { /* ... */ }
/// # let _: axum::Router = axum::Router::new().route("/", axum::routing::get(handler));
/// ```
///
/// [`FromRequest`]: axum::extract::FromRequest
/// [`IntoResponse`]: axum::response::IntoResponse
/// [`From<E::Rejection>`]: std::convert::From
pub struct WithRejection<E, R>(pub E, pub PhantomData<R>);
impl<E, R> WithRejection<E, R> {
/// Returns the wrapped extractor
pub fn into_inner(self) -> E {
self.0
}
}
impl<E, R> Debug for WithRejection<E, R>
where
E: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("WithRejection")
.field(&self.0)
.field(&self.1)
.finish()
}
}
impl<E, R> Clone for WithRejection<E, R>
where
E: Clone,
{
fn clone(&self) -> Self {
Self(self.0.clone(), self.1)
}
}
impl<E, R> Copy for WithRejection<E, R> where E: Copy {}
impl<E: Default, R> Default for WithRejection<E, R> {
fn default() -> Self {
Self(Default::default(), Default::default())
}
}
impl<E, R> Deref for WithRejection<E, R> {
type Target = E;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<E, R> DerefMut for WithRejection<E, R> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<E, R, S> FromRequest<S> for WithRejection<E, R>
where
S: Send + Sync,
E: FromRequest<S>,
R: From<E::Rejection> + IntoResponse,
{
type Rejection = R;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let extractor = E::from_request(req, state).await?;
Ok(WithRejection(extractor, PhantomData))
}
}
impl<E, R, S> FromRequestParts<S> for WithRejection<E, R>
where
S: Send + Sync,
E: FromRequestParts<S>,
R: From<E::Rejection> + IntoResponse,
{
type Rejection = R;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let extractor = E::from_request_parts(parts, state).await?;
Ok(WithRejection(extractor, PhantomData))
}
}
#[cfg(feature = "typed-routing")]
impl<E, R> TypedPath for WithRejection<E, R>
where
E: TypedPath,
{
const PATH: &'static str = E::PATH;
}
impl<E, R> Display for WithRejection<E, R>
where
E: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::Request;
use axum::response::Response;
#[tokio::test]
async fn extractor_rejection_is_transformed() {
struct TestExtractor;
struct TestRejection;
impl<S> FromRequestParts<S> for TestExtractor
where
S: Send + Sync,
{
type Rejection = ();
async fn from_request_parts(
_parts: &mut Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
Err(())
}
}
impl IntoResponse for TestRejection {
fn into_response(self) -> Response {
().into_response()
}
}
impl From<()> for TestRejection {
fn from(_: ()) -> Self {
TestRejection
}
}
let req = Request::new(Body::empty());
let result = WithRejection::<TestExtractor, TestRejection>::from_request(req, &()).await;
assert!(matches!(result, Err(TestRejection)));
let (mut parts, _) = Request::new(()).into_parts();
let result =
WithRejection::<TestExtractor, TestRejection>::from_request_parts(&mut parts, &())
.await;
assert!(matches!(result, Err(TestRejection)));
}
}
+198
View File
@@ -0,0 +1,198 @@
//! Additional handler utilities.
use axum::body::Body;
use axum::extract::Request;
use axum::{
extract::FromRequest,
handler::Handler,
response::{IntoResponse, Response},
};
use futures_util::future::{BoxFuture, FutureExt, Map};
use std::{future::Future, marker::PhantomData};
mod or;
pub use self::or::Or;
/// Trait for async functions that can be used to handle requests.
///
/// This trait is similar to [`Handler`] but rather than taking the request it takes the extracted
/// inputs.
///
/// The drawbacks of this trait is that you cannot apply middleware to individual handlers like you
/// can with [`Handler::layer`].
pub trait HandlerCallWithExtractors<T, S>: Sized {
/// The type of future calling this handler returns.
type Future: Future<Output = Response> + Send + 'static;
/// Call the handler with the extracted inputs.
fn call(self, extractors: T, state: S) -> <Self as HandlerCallWithExtractors<T, S>>::Future;
/// Convert this `HandlerCallWithExtractors` into [`Handler`].
fn into_handler(self) -> IntoHandler<Self, T, S> {
IntoHandler {
handler: self,
_marker: PhantomData,
}
}
/// Chain two handlers together, running the second one if the first one rejects.
///
/// Note that this only moves to the next handler if an extractor fails. The response from
/// handlers are not considered.
///
/// # Example
///
/// ```
/// use axum_extra::handler::HandlerCallWithExtractors;
/// use axum::{
/// Router,
/// routing::get,
/// extract::FromRequestParts,
/// };
///
/// // handlers for varying levels of access
/// async fn admin(admin: AdminPermissions) {
/// // request came from an admin
/// }
///
/// async fn user(user: User) {
/// // we have a `User`
/// }
///
/// async fn guest() {
/// // `AdminPermissions` and `User` failed, so we're just a guest
/// }
///
/// // extractors for checking permissions
/// struct AdminPermissions {}
///
/// impl<S> FromRequestParts<S> for AdminPermissions
/// where
/// S: Send + Sync,
/// {
/// // check for admin permissions...
/// # type Rejection = ();
/// # async fn from_request_parts(parts: &mut http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
/// # todo!()
/// # }
/// }
///
/// struct User {}
///
/// impl<S> FromRequestParts<S> for User
/// where
/// S: Send + Sync,
/// {
/// // check for a logged in user...
/// # type Rejection = ();
/// # async fn from_request_parts(parts: &mut http::request::Parts, state: &S) -> Result<Self, Self::Rejection> {
/// # todo!()
/// # }
/// }
///
/// let app = Router::new().route(
/// "/users/{id}",
/// get(
/// // first try `admin`, if that rejects run `user`, finally falling back
/// // to `guest`
/// admin.or(user).or(guest)
/// )
/// );
/// # let _: Router = app;
/// ```
fn or<R, Rt>(self, rhs: R) -> Or<Self, R, T, Rt, S>
where
R: HandlerCallWithExtractors<Rt, S>,
{
Or {
lhs: self,
rhs,
_marker: PhantomData,
}
}
}
macro_rules! impl_handler_call_with {
( $($ty:ident),* $(,)? ) => {
#[allow(non_snake_case)]
impl<F, Fut, S, $($ty,)*> HandlerCallWithExtractors<($($ty,)*), S> for F
where
F: FnOnce($($ty,)*) -> Fut,
Fut: Future + Send + 'static,
Fut::Output: IntoResponse,
{
// this puts `futures_util` in our public API but that's fine in axum-extra
type Future = Map<Fut, fn(Fut::Output) -> Response>;
fn call(
self,
($($ty,)*): ($($ty,)*),
_state: S,
) -> <Self as HandlerCallWithExtractors<($($ty,)*), S>>::Future {
self($($ty,)*).map(IntoResponse::into_response)
}
}
};
}
impl_handler_call_with!();
impl_handler_call_with!(T1);
impl_handler_call_with!(T1, T2);
impl_handler_call_with!(T1, T2, T3);
impl_handler_call_with!(T1, T2, T3, T4);
impl_handler_call_with!(T1, T2, T3, T4, T5);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
impl_handler_call_with!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
/// A [`Handler`] created from a [`HandlerCallWithExtractors`].
///
/// Created with [`HandlerCallWithExtractors::into_handler`].
#[allow(missing_debug_implementations)]
pub struct IntoHandler<H, T, S> {
handler: H,
_marker: PhantomData<fn() -> (T, S)>,
}
impl<H, T, S> Handler<T, S> for IntoHandler<H, T, S>
where
H: HandlerCallWithExtractors<T, S> + Clone + Send + Sync + 'static,
T: FromRequest<S> + Send + 'static,
T::Rejection: Send,
S: Send + Sync + 'static,
{
type Future = BoxFuture<'static, Response>;
fn call(self, req: Request, state: S) -> Self::Future {
let req = req.map(Body::new);
Box::pin(async move {
match T::from_request(req, &state).await {
Ok(t) => self.handler.call(t, state).await,
Err(rejection) => rejection.into_response(),
}
})
}
}
impl<H, T, S> Copy for IntoHandler<H, T, S> where H: Copy {}
impl<H, T, S> Clone for IntoHandler<H, T, S>
where
H: Clone,
{
fn clone(&self) -> Self {
Self {
handler: self.handler.clone(),
_marker: self._marker,
}
}
}
+150
View File
@@ -0,0 +1,150 @@
use super::HandlerCallWithExtractors;
use crate::either::Either;
use axum::{
extract::{FromRequest, FromRequestParts, Request},
handler::Handler,
response::{IntoResponse, Response},
};
use futures_util::future::{BoxFuture, Either as EitherFuture, FutureExt, Map};
use std::{future::Future, marker::PhantomData};
/// [`Handler`] that runs one [`Handler`] and if that rejects it'll fallback to another
/// [`Handler`].
///
/// Created with [`HandlerCallWithExtractors::or`](super::HandlerCallWithExtractors::or).
#[allow(missing_debug_implementations)]
pub struct Or<L, R, Lt, Rt, S> {
pub(super) lhs: L,
pub(super) rhs: R,
pub(super) _marker: PhantomData<fn() -> (Lt, Rt, S)>,
}
impl<S, L, R, Lt, Rt> HandlerCallWithExtractors<Either<Lt, Rt>, S> for Or<L, R, Lt, Rt, S>
where
L: HandlerCallWithExtractors<Lt, S> + Send + 'static,
R: HandlerCallWithExtractors<Rt, S> + Send + 'static,
Rt: Send + 'static,
Lt: Send + 'static,
{
// this puts `futures_util` in our public API but that's fine in axum-extra
type Future = EitherFuture<
Map<L::Future, fn(<L::Future as Future>::Output) -> Response>,
Map<R::Future, fn(<R::Future as Future>::Output) -> Response>,
>;
fn call(
self,
extractors: Either<Lt, Rt>,
state: S,
) -> <Self as HandlerCallWithExtractors<Either<Lt, Rt>, S>>::Future {
match extractors {
Either::E1(lt) => self
.lhs
.call(lt, state)
.map(IntoResponse::into_response as _)
.left_future(),
Either::E2(rt) => self
.rhs
.call(rt, state)
.map(IntoResponse::into_response as _)
.right_future(),
}
}
}
impl<S, L, R, Lt, Rt, M> Handler<(M, Lt, Rt), S> for Or<L, R, Lt, Rt, S>
where
L: HandlerCallWithExtractors<Lt, S> + Clone + Send + Sync + 'static,
R: HandlerCallWithExtractors<Rt, S> + Clone + Send + Sync + 'static,
Lt: FromRequestParts<S> + Send + 'static,
Rt: FromRequest<S, M> + Send + 'static,
Lt::Rejection: Send,
Rt::Rejection: Send,
S: Send + Sync + 'static,
{
// this puts `futures_util` in our public API but that's fine in axum-extra
type Future = BoxFuture<'static, Response>;
fn call(self, req: Request, state: S) -> Self::Future {
let (mut parts, body) = req.into_parts();
Box::pin(async move {
if let Ok(lt) = Lt::from_request_parts(&mut parts, &state).await {
return self.lhs.call(lt, state).await;
}
let req = Request::from_parts(parts, body);
match Rt::from_request(req, &state).await {
Ok(rt) => self.rhs.call(rt, state).await,
Err(rejection) => rejection.into_response(),
}
})
}
}
impl<L, R, Lt, Rt, S> Copy for Or<L, R, Lt, Rt, S>
where
L: Copy,
R: Copy,
{
}
impl<L, R, Lt, Rt, S> Clone for Or<L, R, Lt, Rt, S>
where
L: Clone,
R: Clone,
{
fn clone(&self) -> Self {
Self {
lhs: self.lhs.clone(),
rhs: self.rhs.clone(),
_marker: self._marker,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{
extract::{Path, Query},
routing::get,
Router,
};
use serde::Deserialize;
#[tokio::test]
async fn works() {
#[derive(Deserialize)]
struct Params {
a: String,
}
async fn one(Path(id): Path<u32>) -> String {
id.to_string()
}
async fn two(Query(params): Query<Params>) -> String {
params.a
}
async fn three() -> &'static str {
"fallback"
}
let app = Router::new().route("/{id}", get(one.or(two).or(three)));
let client = TestClient::new(app);
let res = client.get("/123").await;
assert_eq!(res.text().await, "123");
let res = client.get("/foo?a=bar").await;
assert_eq!(res.text().await, "bar");
let res = client.get("/foo").await;
assert_eq!(res.text().await, "fallback");
}
}
+259
View File
@@ -0,0 +1,259 @@
//! Newline delimited JSON extractor and response.
use axum::{
body::Body,
extract::{FromRequest, Request},
response::{IntoResponse, Response},
BoxError,
};
use bytes::{BufMut, BytesMut};
use futures_util::stream::{BoxStream, Stream, TryStream, TryStreamExt};
use pin_project_lite::pin_project;
use serde_core::{de::DeserializeOwned, Serialize};
use std::{
convert::Infallible,
io::{self, Write},
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use tokio::io::AsyncBufReadExt;
use tokio_stream::wrappers::LinesStream;
use tokio_util::io::StreamReader;
pin_project! {
/// A stream of newline delimited JSON.
///
/// This can be used both as an extractor and as a response.
///
/// # As extractor
///
/// ```rust
/// use axum_extra::json_lines::JsonLines;
/// use futures_util::stream::StreamExt;
///
/// async fn handler(mut stream: JsonLines<serde_json::Value>) {
/// while let Some(value) = stream.next().await {
/// // ...
/// }
/// }
/// ```
///
/// # As response
///
/// ```rust
/// use axum::{BoxError, response::{IntoResponse, Response}};
/// use axum_extra::json_lines::JsonLines;
/// use futures_util::stream::Stream;
///
/// fn stream_of_values() -> impl Stream<Item = Result<serde_json::Value, BoxError>> {
/// # futures_util::stream::empty()
/// }
///
/// async fn handler() -> Response {
/// JsonLines::new(stream_of_values()).into_response()
/// }
/// ```
// we use `AsExtractor` as the default because you're more likely to name this type if it's used
// as an extractor
#[must_use]
pub struct JsonLines<S, T = AsExtractor> {
#[pin]
inner: Inner<S>,
_marker: PhantomData<T>,
}
}
pin_project! {
#[project = InnerProj]
enum Inner<S> {
Response {
#[pin]
stream: S,
},
Extractor {
#[pin]
stream: BoxStream<'static, Result<S, axum::Error>>,
},
}
}
/// Marker type used to prove that an `JsonLines` was constructed via `FromRequest`.
#[derive(Debug)]
#[non_exhaustive]
pub struct AsExtractor;
/// Marker type used to prove that an `JsonLines` was constructed via `JsonLines::new`.
#[derive(Debug)]
#[non_exhaustive]
pub struct AsResponse;
impl<S> JsonLines<S, AsResponse> {
/// Create a new `JsonLines` from a stream of items.
pub fn new(stream: S) -> Self {
Self {
inner: Inner::Response { stream },
_marker: PhantomData,
}
}
}
impl<S, T> FromRequest<S> for JsonLines<T, AsExtractor>
where
T: DeserializeOwned,
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
// `Stream::lines` isn't a thing so we have to convert it into an `AsyncRead`
// so we can call `AsyncRead::lines` and then convert it back to a `Stream`
let body = req.into_body();
let stream = body.into_data_stream();
let stream = stream.map_err(io::Error::other);
let read = StreamReader::new(stream);
let lines_stream = LinesStream::new(read.lines());
let deserialized_stream =
lines_stream
.map_err(axum::Error::new)
.and_then(|value| async move {
serde_json::from_str::<T>(&value).map_err(axum::Error::new)
});
Ok(Self {
inner: Inner::Extractor {
stream: Box::pin(deserialized_stream),
},
_marker: PhantomData,
})
}
}
impl<T> Stream for JsonLines<T, AsExtractor> {
type Item = Result<T, axum::Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
match self.project().inner.project() {
InnerProj::Extractor { stream } => stream.poll_next(cx),
// `JsonLines<_, AsExtractor>` can only be constructed via `FromRequest`
// which doesn't use this variant
InnerProj::Response { .. } => unreachable!(),
}
}
}
impl<S> IntoResponse for JsonLines<S, AsResponse>
where
S: TryStream + Send + 'static,
S::Ok: Serialize + Send,
S::Error: Into<BoxError>,
{
fn into_response(self) -> Response {
let inner = match self.inner {
Inner::Response { stream } => stream,
// `JsonLines<_, AsResponse>` can only be constructed via `JsonLines::new`
// which doesn't use this variant
Inner::Extractor { .. } => unreachable!(),
};
let stream = inner.map_err(Into::into).and_then(|value| async move {
let mut buf = BytesMut::new().writer();
serde_json::to_writer(&mut buf, &value)?;
buf.write_all(b"\n")?;
Ok::<_, BoxError>(buf.into_inner().freeze())
});
let stream = Body::from_stream(stream);
// there is no consensus around mime type yet
// https://github.com/wardi/jsonlines/issues/36
stream.into_response()
}
}
#[cfg(test)]
mod tests {
use super::JsonLines;
use crate::test_helpers::*;
use axum::{
routing::{get, post},
Router,
};
use futures_util::StreamExt;
use http::StatusCode;
use serde::{Deserialize, Serialize};
use std::{convert::Infallible, error::Error};
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug)]
struct User {
id: i32,
}
#[tokio::test]
async fn extractor() {
let app = Router::new().route(
"/",
post(|mut stream: JsonLines<User>| async move {
assert_eq!(stream.next().await.unwrap().unwrap(), User { id: 1 });
assert_eq!(stream.next().await.unwrap().unwrap(), User { id: 2 });
assert_eq!(stream.next().await.unwrap().unwrap(), User { id: 3 });
// sources are downcastable to `serde_json::Error`
let err = stream.next().await.unwrap().unwrap_err();
let _: &serde_json::Error = err
.source()
.unwrap()
.downcast_ref::<serde_json::Error>()
.unwrap();
}),
);
let client = TestClient::new(app);
let res = client
.post("/")
.body(
[
"{\"id\":1}",
"{\"id\":2}",
"{\"id\":3}",
// to trigger an error for source downcasting
"{\"id\":false}",
]
.join("\n"),
)
.await;
assert_eq!(res.status(), StatusCode::OK);
}
#[tokio::test]
async fn response() {
let app = Router::new().route(
"/",
get(|| async {
let values = futures_util::stream::iter(vec![
Ok::<_, Infallible>(User { id: 1 }),
Ok::<_, Infallible>(User { id: 2 }),
Ok::<_, Infallible>(User { id: 3 }),
]);
JsonLines::new(values)
}),
);
let client = TestClient::new(app);
let res = client.get("/").await;
let values = res
.text()
.await
.lines()
.map(|line| serde_json::from_str::<User>(line).unwrap())
.collect::<Vec<_>>();
assert_eq!(
values,
vec![User { id: 1 }, User { id: 2 }, User { id: 3 },]
);
}
}
+83
View File
@@ -0,0 +1,83 @@
//! Extra utilities for [`axum`].
//!
//! # Feature flags
//!
//! axum-extra uses a set of [feature flags] to reduce the amount of compiled and
//! optional dependencies.
//!
//! The following optional features are available:
//!
//! Name | Description | Default?
//! ---|---|---
//! `async-read-body` | Enables the [`AsyncReadBody`](crate::body::AsyncReadBody) body | No
//! `attachment` | Enables the [`Attachment`](crate::response::Attachment) response | No
//! `cookie` | Enables the [`CookieJar`](crate::extract::CookieJar) extractor | No
//! `cookie-private` | Enables the [`PrivateCookieJar`](crate::extract::PrivateCookieJar) extractor | No
//! `cookie-signed` | Enables the [`SignedCookieJar`](crate::extract::SignedCookieJar) extractor | No
//! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method | No
//! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response | No
//! `error-response` | Enables the [`InternalServerError`](crate::response::InternalServerError) response | No
//! `form` | Enables the [`Form`](crate::extract::Form) extractor | No
//! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor | No
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response | No
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor | No
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response | No
//! `query` | Enables the [`Query`](crate::extract::Query) extractor | No
//! `tracing` | Log rejections from built-in extractors | Yes
//! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities | No
//! `typed-header` | Enables the [`TypedHeader`] extractor and response | No
//! `file-stream` | Enables the [`FileStream`](crate::response::FileStream) response | No
//!
//! [`axum`]: https://crates.io/crates/axum
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(test, allow(clippy::float_cmp))]
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
#[allow(unused_extern_crates)]
extern crate self as axum_extra;
pub mod body;
pub mod either;
pub mod extract;
pub mod handler;
pub mod middleware;
pub mod response;
pub mod routing;
#[cfg(feature = "json-lines")]
pub mod json_lines;
#[cfg(feature = "typed-header")]
pub mod typed_header;
#[cfg(feature = "typed-header")]
#[doc(no_inline)]
pub use headers;
#[cfg(feature = "typed-header")]
#[doc(inline)]
pub use typed_header::TypedHeader;
#[cfg(feature = "protobuf")]
pub mod protobuf;
/// _not_ public API
#[cfg(feature = "typed-routing")]
#[doc(hidden)]
pub mod __private {
use percent_encoding::{AsciiSet, CONTROLS};
pub use percent_encoding::utf8_percent_encode;
// from https://github.com/servo/rust-url/blob/master/url/src/parser.rs
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
pub const PATH_SEGMENT: &AsciiSet = &PATH.add(b'/').add(b'%');
}
#[cfg(test)]
use axum_macros::__private_axum_test as test;
#[cfg(test)]
pub(crate) use axum::test_helpers;
+44
View File
@@ -0,0 +1,44 @@
//! Additional middleware utilities.
use crate::either::Either;
use tower_layer::Identity;
/// Convert an `Option<Layer>` into a [`Layer`].
///
/// If the layer is a `Some` it'll be applied, otherwise not.
///
/// # Example
///
/// ```
/// use axum_extra::middleware::option_layer;
/// use axum::{Router, routing::get};
/// use std::time::Duration;
/// use tower_http::timeout::TimeoutLayer;
///
/// # let option_timeout = Some(Duration::new(10, 0));
/// let timeout_layer = option_timeout.map(TimeoutLayer::new);
///
/// let app = Router::new()
/// .route("/", get(|| async {}))
/// .layer(option_layer(timeout_layer));
/// # let _: Router = app;
/// ```
///
/// # Difference between this and [`tower::util::option_layer`]
///
/// [`tower::util::option_layer`] always changes the error type to [`BoxError`] which requires
/// using [`HandleErrorLayer`] when used with axum, even if the layer you're applying uses
/// [`Infallible`].
///
/// `axum_extra::middleware::option_layer` on the other hand doesn't change the error type so can
/// be applied directly.
///
/// [`Layer`]: tower_layer::Layer
/// [`BoxError`]: tower::BoxError
/// [`HandleErrorLayer`]: axum::error_handling::HandleErrorLayer
/// [`Infallible`]: std::convert::Infallible
pub fn option_layer<L>(layer: Option<L>) -> Either<L, Identity> {
layer
.map(Either::E1)
.unwrap_or_else(|| Either::E2(Identity::new()))
}
+259
View File
@@ -0,0 +1,259 @@
//! Protocol Buffer extractor and response.
use axum::{
extract::{rejection::BytesRejection, FromRequest, Request},
response::{IntoResponse, Response},
RequestExt,
};
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use bytes::BytesMut;
use http::StatusCode;
use http_body_util::BodyExt;
use prost::Message;
/// A Protocol Buffer message extractor and response.
///
/// This can be used both as an extractor and as a response.
///
/// # As extractor
///
/// When used as an extractor, it can decode request bodies into some type that
/// implements [`prost::Message`]. The request will be rejected (and a [`ProtobufRejection`] will
/// be returned) if:
///
/// - The body couldn't be decoded into the target Protocol Buffer message type.
/// - Buffering the request body fails.
///
/// See [`ProtobufRejection`] for more details.
///
/// The extractor does not expect a `Content-Type` header to be present in the request.
///
/// # Extractor example
///
/// ```rust,no_run
/// use axum::{routing::post, Router};
/// use axum_extra::protobuf::Protobuf;
///
/// #[derive(prost::Message)]
/// struct CreateUser {
/// #[prost(string, tag="1")]
/// email: String,
/// #[prost(string, tag="2")]
/// password: String,
/// }
///
/// async fn create_user(Protobuf(payload): Protobuf<CreateUser>) {
/// // payload is `CreateUser`
/// }
///
/// let app = Router::new().route("/users", post(create_user));
/// # let _: Router = app;
/// ```
///
/// # As response
///
/// When used as a response, it can encode any type that implements [`prost::Message`] to
/// a newly allocated buffer.
///
/// If no `Content-Type` header is set, the `Content-Type: application/octet-stream` header
/// will be used automatically.
///
/// # Response example
///
/// ```
/// use axum::{
/// extract::Path,
/// routing::get,
/// Router,
/// };
/// use axum_extra::protobuf::Protobuf;
///
/// #[derive(prost::Message)]
/// struct User {
/// #[prost(string, tag="1")]
/// username: String,
/// }
///
/// async fn get_user(Path(user_id) : Path<String>) -> Protobuf<User> {
/// let user = find_user(user_id).await;
/// Protobuf(user)
/// }
///
/// async fn find_user(user_id: String) -> User {
/// // ...
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(docsrs, doc(cfg(feature = "protobuf")))]
#[must_use]
pub struct Protobuf<T>(pub T);
impl<T, S> FromRequest<S> for Protobuf<T>
where
T: Message + Default,
S: Send + Sync,
{
type Rejection = ProtobufRejection;
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
let mut buf = req
.into_limited_body()
.collect()
.await
.map_err(ProtobufDecodeError)?
.aggregate();
match T::decode(&mut buf) {
Ok(value) => Ok(Protobuf(value)),
Err(err) => Err(ProtobufDecodeError::from_err(err).into()),
}
}
}
axum_core::__impl_deref!(Protobuf);
impl<T> From<T> for Protobuf<T> {
fn from(inner: T) -> Self {
Self(inner)
}
}
impl<T> IntoResponse for Protobuf<T>
where
T: Message + Default,
{
fn into_response(self) -> Response {
let mut buf = BytesMut::with_capacity(self.0.encoded_len());
match &self.0.encode(&mut buf) {
Ok(()) => buf.into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
}
}
}
define_rejection! {
#[status = UNPROCESSABLE_ENTITY]
#[body = "Failed to decode the body"]
/// Rejection type for [`Protobuf`].
///
/// This rejection is used if the request body couldn't be decoded into the target type.
pub struct ProtobufDecodeError(Error);
}
composite_rejection! {
/// Rejection used for [`Protobuf`].
///
/// Contains one variant for each way the [`Protobuf`] extractor
/// can fail.
pub enum ProtobufRejection {
ProtobufDecodeError,
BytesRejection,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
#[tokio::test]
async fn decode_body() {
#[derive(prost::Message)]
struct Input {
#[prost(string, tag = "1")]
foo: String,
}
let app = Router::new().route(
"/",
post(|input: Protobuf<Input>| async move { input.foo.to_owned() }),
);
let input = Input {
foo: "bar".to_owned(),
};
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).await;
let body = res.text().await;
assert_eq!(body, "bar");
}
#[tokio::test]
async fn prost_decode_error() {
#[derive(prost::Message)]
struct Input {
#[prost(string, tag = "1")]
foo: String,
}
#[derive(prost::Message)]
struct Expected {
#[prost(int32, tag = "1")]
test: i32,
}
let app = Router::new().route("/", post(|_: Protobuf<Expected>| async {}));
let input = Input {
foo: "bar".to_owned(),
};
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[tokio::test]
async fn encode_body() {
#[derive(prost::Message)]
struct Input {
#[prost(string, tag = "1")]
foo: String,
}
#[derive(prost::Message)]
struct Output {
#[prost(string, tag = "1")]
result: String,
}
#[axum::debug_handler]
async fn handler(input: Protobuf<Input>) -> Protobuf<Output> {
let output = Output {
result: input.foo.to_owned(),
};
Protobuf(output)
}
let app = Router::new().route("/", post(handler));
let input = Input {
foo: "bar".to_owned(),
};
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).await;
assert_eq!(
res.headers()["content-type"],
mime::APPLICATION_OCTET_STREAM.as_ref()
);
let body = res.bytes().await;
let output = Output::decode(body).unwrap();
assert_eq!(output.result, "bar");
}
}
@@ -0,0 +1,103 @@
use axum::response::IntoResponse;
use http::{header, HeaderMap, HeaderValue};
use tracing::error;
/// A file attachment response.
///
/// This type will set the `Content-Disposition` header to `attachment`. In response a webbrowser
/// will offer to download the file instead of displaying it directly.
///
/// Use the `filename` and `content_type` methods to set the filename or content-type of the
/// attachment. If these values are not set they will not be sent.
///
///
/// # Example
///
/// ```rust
/// use axum::{http::StatusCode, routing::get, Router};
/// use axum_extra::response::Attachment;
///
/// async fn cargo_toml() -> Result<Attachment<String>, (StatusCode, String)> {
/// let file_contents = tokio::fs::read_to_string("Cargo.toml")
/// .await
/// .map_err(|err| (StatusCode::NOT_FOUND, format!("File not found: {err}")))?;
/// Ok(Attachment::new(file_contents)
/// .filename("Cargo.toml")
/// .content_type("text/x-toml"))
/// }
///
/// let app = Router::new().route("/Cargo.toml", get(cargo_toml));
/// let _: Router = app;
/// ```
///
/// # Note
///
/// If you use axum with hyper, hyper will set the `Content-Length` if it is known.
#[derive(Debug)]
#[must_use]
pub struct Attachment<T> {
inner: T,
filename: Option<HeaderValue>,
content_type: Option<HeaderValue>,
}
impl<T: IntoResponse> Attachment<T> {
/// Creates a new [`Attachment`].
pub fn new(inner: T) -> Self {
Self {
inner,
filename: None,
content_type: None,
}
}
/// Sets the filename of the [`Attachment`].
///
/// This updates the `Content-Disposition` header to add a filename.
pub fn filename<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
self.filename = if let Ok(filename) = value.try_into() {
Some(filename)
} else {
error!("Attachment filename contains invalid characters");
None
};
self
}
/// Sets the content-type of the [`Attachment`]
pub fn content_type<H: TryInto<HeaderValue>>(mut self, value: H) -> Self {
if let Ok(content_type) = value.try_into() {
self.content_type = Some(content_type);
} else {
error!("Attachment content-type contains invalid characters");
}
self
}
}
impl<T> IntoResponse for Attachment<T>
where
T: IntoResponse,
{
fn into_response(self) -> axum::response::Response {
let mut headers = HeaderMap::new();
if let Some(content_type) = self.content_type {
headers.append(header::CONTENT_TYPE, content_type);
}
let content_disposition = if let Some(filename) = self.filename {
let mut bytes = b"attachment; filename=\"".to_vec();
bytes.extend_from_slice(filename.as_bytes());
bytes.push(b'\"');
HeaderValue::from_bytes(&bytes).expect("This was a HeaderValue so this can not fail")
} else {
HeaderValue::from_static("attachment")
};
headers.append(header::CONTENT_DISPOSITION, content_disposition);
(headers, self.inner).into_response()
}
}
@@ -0,0 +1,145 @@
use std::sync::Arc;
use axum::{
http::{header, HeaderValue, StatusCode},
response::{IntoResponse, Response},
};
use bytes::{BufMut, Bytes, BytesMut};
use serde_core::Serialize;
/// A response type that holds a JSON in serialized form.
///
/// This allows returning a borrowing type from a handler, or returning different response
/// types as JSON from different branches inside a handler.
///
/// Like [`axum::Json`],
/// if the [`Serialize`] implementation fails
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// This can be constructed using [`new`](ErasedJson::new)
/// or the [`json!`](crate::json) macro.
///
/// # Example
///
/// ```rust
/// # use axum::{response::IntoResponse};
/// # use axum_extra::response::ErasedJson;
/// async fn handler() -> ErasedJson {
/// # let condition = true;
/// # let foo = ();
/// # let bar = vec![()];
/// // ...
///
/// if condition {
/// ErasedJson::new(&foo)
/// } else {
/// ErasedJson::new(&bar)
/// }
/// }
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "erased-json")))]
#[derive(Clone, Debug)]
#[must_use]
pub struct ErasedJson(Result<Bytes, Arc<serde_json::Error>>);
impl ErasedJson {
/// Create an `ErasedJson` by serializing a value with the compact formatter.
pub fn new<T: Serialize>(val: T) -> Self {
let mut bytes = BytesMut::with_capacity(128);
let result = match serde_json::to_writer((&mut bytes).writer(), &val) {
Ok(()) => Ok(bytes.freeze()),
Err(e) => Err(Arc::new(e)),
};
Self(result)
}
/// Create an `ErasedJson` by serializing a value with the pretty formatter.
pub fn pretty<T: Serialize>(val: T) -> Self {
let mut bytes = BytesMut::with_capacity(128);
let result = match serde_json::to_writer_pretty((&mut bytes).writer(), &val) {
Ok(()) => Ok(bytes.freeze()),
Err(e) => Err(Arc::new(e)),
};
Self(result)
}
}
impl IntoResponse for ErasedJson {
fn into_response(self) -> Response {
match self.0 {
Ok(bytes) => (
[(
header::CONTENT_TYPE,
HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
)],
bytes,
)
.into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
}
}
}
/// Construct an [`ErasedJson`] response from a JSON literal.
///
/// A `Content-Type: application/json` header is automatically added.
/// Any variable or expression implementing [`Serialize`]
/// can be interpolated as a value in the literal.
/// If the [`Serialize`] implementation fails,
/// or if a map with non-string keys is used,
/// a 500 response will be issued
/// whose body is the error message in UTF-8.
///
/// Internally,
/// this function uses the [`typed_json::json!`] macro,
/// allowing it to perform far fewer allocations
/// than a dynamic macro like [`serde_json::json!`] would –
/// it's equivalent to if you had just written
/// `derive(Serialize)` on a struct.
///
/// # Examples
///
/// ```
/// use axum::{
/// Router,
/// extract::Path,
/// response::Response,
/// routing::get,
/// };
/// use axum_extra::response::ErasedJson;
///
/// async fn get_user(Path(user_id) : Path<u64>) -> ErasedJson {
/// let user_name = find_user_name(user_id).await;
/// axum_extra::json!({ "name": user_name })
/// }
///
/// async fn find_user_name(user_id: u64) -> String {
/// // ...
/// # unimplemented!()
/// }
///
/// let app = Router::new().route("/users/{id}", get(get_user));
/// # let _: Router = app;
/// ```
///
/// Trailing commas are allowed in both arrays and objects.
///
/// ```
/// let response = axum_extra::json!(["trailing",]);
/// ```
#[macro_export]
macro_rules! json {
($($t:tt)*) => {
$crate::response::ErasedJson::new(
$crate::response::__private_erased_json::typed_json::json!($($t)*)
)
}
}
/// Not public API. Re-exported as `crate::response::__private_erased_json`.
#[doc(hidden)]
pub mod private {
pub use typed_json;
}
@@ -0,0 +1,51 @@
use axum_core::response::{IntoResponse, Response};
use http::StatusCode;
use std::error::Error;
use tracing::error;
/// Convenience response to create an error response from a non-[`IntoResponse`] error
///
/// This provides a method to quickly respond with an error that does not implement
/// the `IntoResponse` trait itself. This type should only be used for debugging purposes or internal
/// facing applications, as it includes the full error chain with descriptions,
/// thus leaking information that could possibly be sensitive.
///
/// ```rust
/// use axum_extra::response::InternalServerError;
/// use axum_core::response::IntoResponse;
/// # use std::io::{Error, ErrorKind};
/// # fn try_thing() -> Result<(), Error> {
/// # Err(Error::new(ErrorKind::Other, "error"))
/// # }
///
/// async fn maybe_error() -> Result<String, InternalServerError<Error>> {
/// try_thing().map_err(InternalServerError)?;
/// // do something on success
/// # Ok(String::from("ok"))
/// }
/// ```
#[derive(Debug)]
pub struct InternalServerError<T>(pub T);
impl<T: Error + 'static> IntoResponse for InternalServerError<T> {
fn into_response(self) -> Response {
error!(error = &self.0 as &dyn Error);
(
StatusCode::INTERNAL_SERVER_ERROR,
"An error occurred while processing your request.",
)
.into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Error;
#[test]
fn internal_server_error() {
let response = InternalServerError(Error::other("Test")).into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
}
@@ -0,0 +1,598 @@
use axum::{
body,
response::{IntoResponse, Response},
BoxError,
};
use bytes::Bytes;
use futures_util::TryStream;
use http::{header, StatusCode};
use std::{io, path::Path};
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncSeekExt},
};
use tokio_util::io::ReaderStream;
/// Encapsulate the file stream.
///
/// The encapsulated file stream construct requires passing in a stream.
///
/// # Examples
///
/// ```
/// use axum::{
/// http::StatusCode,
/// response::{IntoResponse, Response},
/// routing::get,
/// Router,
/// };
/// use axum_extra::response::file_stream::FileStream;
/// use tokio::fs::File;
/// use tokio_util::io::ReaderStream;
///
/// async fn file_stream() -> Result<Response, (StatusCode, String)> {
/// let file = File::open("test.txt")
/// .await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}")))?;
///
/// let stream = ReaderStream::new(file);
/// let file_stream_resp = FileStream::new(stream).file_name("test.txt");
///
/// Ok(file_stream_resp.into_response())
/// }
///
/// let app = Router::new().route("/file-stream", get(file_stream));
/// # let _: Router = app;
/// ```
#[must_use]
#[derive(Debug)]
pub struct FileStream<S> {
/// stream.
pub stream: S,
/// The file name of the file.
pub file_name: Option<String>,
/// The size of the file.
pub content_size: Option<u64>,
}
impl<S> FileStream<S>
where
S: TryStream + Send + 'static,
S::Ok: Into<Bytes>,
S::Error: Into<BoxError>,
{
/// Create a new [`FileStream`]
pub fn new(stream: S) -> Self {
Self {
stream,
file_name: None,
content_size: None,
}
}
/// Create a [`FileStream`] from a file path.
///
/// # Examples
///
/// ```
/// use axum::{
/// http::StatusCode,
/// response::IntoResponse,
/// Router,
/// routing::get
/// };
/// use axum_extra::response::file_stream::FileStream;
/// use tokio::fs::File;
/// use tokio_util::io::ReaderStream;
///
/// async fn file_stream() -> impl IntoResponse {
/// FileStream::<ReaderStream<File>>::from_path("test.txt")
/// .await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}")))
/// }
///
/// let app = Router::new().route("/file-stream", get(file_stream));
/// # let _: Router = app;
/// ```
pub async fn from_path(path: impl AsRef<Path>) -> io::Result<FileStream<ReaderStream<File>>> {
let file = File::open(&path).await?;
let mut content_size = None;
let mut file_name = None;
if let Ok(metadata) = file.metadata().await {
content_size = Some(metadata.len());
}
if let Some(file_name_os) = path.as_ref().file_name() {
if let Some(file_name_str) = file_name_os.to_str() {
file_name = Some(file_name_str.to_owned());
}
}
Ok(FileStream {
stream: ReaderStream::new(file),
file_name,
content_size,
})
}
/// Set the file name of the [`FileStream`].
///
/// This adds the attachment `Content-Disposition` header with the given `file_name`.
pub fn file_name(mut self, file_name: impl Into<String>) -> Self {
self.file_name = Some(file_name.into());
self
}
/// Set the size of the file.
pub fn content_size(mut self, len: u64) -> Self {
self.content_size = Some(len);
self
}
/// Return a range response.
///
/// range: (start, end, total_size)
///
/// # Examples
///
/// ```
/// use axum::{
/// http::StatusCode,
/// response::IntoResponse,
/// routing::get,
/// Router,
/// };
/// use axum_extra::response::file_stream::FileStream;
/// use tokio::fs::File;
/// use tokio::io::AsyncSeekExt;
/// use tokio_util::io::ReaderStream;
///
/// async fn range_response() -> Result<impl IntoResponse, (StatusCode, String)> {
/// let mut file = File::open("test.txt")
/// .await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}")))?;
/// let mut file_size = file
/// .metadata()
/// .await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("Get file size: {e}")))?
/// .len();
///
/// file.seek(std::io::SeekFrom::Start(10))
/// .await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("File seek error: {e}")))?;
/// let stream = ReaderStream::new(file);
///
/// Ok(FileStream::new(stream).into_range_response(10, file_size - 1, file_size))
/// }
///
/// let app = Router::new().route("/file-stream", get(range_response));
/// # let _: Router = app;
/// ```
pub fn into_range_response(self, start: u64, end: u64, total_size: u64) -> Response {
let mut resp = Response::builder().header(header::CONTENT_TYPE, "application/octet-stream");
resp = resp.status(StatusCode::PARTIAL_CONTENT);
resp = resp.header(
header::CONTENT_RANGE,
format!("bytes {start}-{end}/{total_size}"),
);
resp.body(body::Body::from_stream(self.stream))
.unwrap_or_else(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("build FileStream response error: {e}"),
)
.into_response()
})
}
/// Attempts to return RANGE requests directly from the file path.
///
/// # Arguments
///
/// * `file_path` - The path of the file to be streamed
/// * `start` - The start position of the range
/// * `end` - The end position of the range
///
/// # Note
///
/// * If `end` is 0, then it is used as `file_size - 1`
/// * If `start` > `file_size` or `start` > `end`, then `Range Not Satisfiable` is returned
///
/// # Examples
///
/// ```
/// use axum::{
/// http::StatusCode,
/// response::IntoResponse,
/// Router,
/// routing::get
/// };
/// use std::path::Path;
/// use axum_extra::response::file_stream::FileStream;
/// use tokio::fs::File;
/// use tokio_util::io::ReaderStream;
/// use tokio::io::AsyncSeekExt;
///
/// async fn range_stream() -> impl IntoResponse {
/// let range_start = 0;
/// let range_end = 1024;
///
/// FileStream::<ReaderStream<File>>::try_range_response("CHANGELOG.md", range_start, range_end).await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}")))
/// }
///
/// let app = Router::new().route("/file-stream", get(range_stream));
/// # let _: Router = app;
/// ```
pub async fn try_range_response(
file_path: impl AsRef<Path>,
start: u64,
mut end: u64,
) -> io::Result<Response> {
let mut file = File::open(file_path).await?;
let metadata = file.metadata().await?;
let total_size = metadata.len();
if end == 0 {
end = total_size - 1;
}
if start > total_size {
return Ok((StatusCode::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable").into_response());
}
if start > end {
return Ok((StatusCode::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable").into_response());
}
if end >= total_size {
return Ok((StatusCode::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable").into_response());
}
file.seek(std::io::SeekFrom::Start(start)).await?;
let stream = ReaderStream::new(file.take(end - start + 1));
Ok(FileStream::new(stream).into_range_response(start, end, total_size))
}
}
impl<S> IntoResponse for FileStream<S>
where
S: TryStream + Send + 'static,
S::Ok: Into<Bytes>,
S::Error: Into<BoxError>,
{
fn into_response(self) -> Response {
let mut resp = Response::builder().header(header::CONTENT_TYPE, "application/octet-stream");
if let Some(file_name) = self.file_name {
resp = resp.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{file_name}\""),
);
}
if let Some(content_size) = self.content_size {
resp = resp.header(header::CONTENT_LENGTH, content_size);
}
resp.body(body::Body::from_stream(self.stream))
.unwrap_or_else(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("build FileStream responsec error: {e}"),
)
.into_response()
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{extract::Request, routing::get, Router};
use body::Body;
use http::HeaderMap;
use http_body_util::BodyExt;
use std::io::Cursor;
use tokio_util::io::ReaderStream;
use tower::ServiceExt;
#[tokio::test]
async fn response() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route(
"/file",
get(|| async {
// Simulating a file stream
let file_content = b"Hello, this is the simulated file content!".to_vec();
let reader = Cursor::new(file_content);
// Response file stream
// Content size and file name are not attached by default
let stream = ReaderStream::new(reader);
FileStream::new(stream).into_response()
}),
);
// Simulating a GET request
let response = app
.oneshot(Request::builder().uri("/file").body(Body::empty())?)
.await?;
// Validate Response Status Code
assert_eq!(response.status(), StatusCode::OK);
// Validate Response Headers
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/octet-stream"
);
// Validate Response Body
let body: &[u8] = &response.into_body().collect().await?.to_bytes();
assert_eq!(
std::str::from_utf8(body)?,
"Hello, this is the simulated file content!"
);
Ok(())
}
#[tokio::test]
async fn response_not_set_filename() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route(
"/file",
get(|| async {
// Simulating a file stream
let file_content = b"Hello, this is the simulated file content!".to_vec();
let size = file_content.len() as u64;
let reader = Cursor::new(file_content);
// Response file stream
let stream = ReaderStream::new(reader);
FileStream::new(stream).content_size(size).into_response()
}),
);
// Simulating a GET request
let response = app
.oneshot(Request::builder().uri("/file").body(Body::empty())?)
.await?;
// Validate Response Status Code
assert_eq!(response.status(), StatusCode::OK);
// Validate Response Headers
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/octet-stream"
);
assert_eq!(response.headers().get("content-length").unwrap(), "42");
// Validate Response Body
let body: &[u8] = &response.into_body().collect().await?.to_bytes();
assert_eq!(
std::str::from_utf8(body)?,
"Hello, this is the simulated file content!"
);
Ok(())
}
#[tokio::test]
async fn response_not_set_content_size() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route(
"/file",
get(|| async {
// Simulating a file stream
let file_content = b"Hello, this is the simulated file content!".to_vec();
let reader = Cursor::new(file_content);
// Response file stream
let stream = ReaderStream::new(reader);
FileStream::new(stream).file_name("test").into_response()
}),
);
// Simulating a GET request
let response = app
.oneshot(Request::builder().uri("/file").body(Body::empty())?)
.await?;
// Validate Response Status Code
assert_eq!(response.status(), StatusCode::OK);
// Validate Response Headers
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/octet-stream"
);
assert_eq!(
response.headers().get("content-disposition").unwrap(),
"attachment; filename=\"test\""
);
// Validate Response Body
let body: &[u8] = &response.into_body().collect().await?.to_bytes();
assert_eq!(
std::str::from_utf8(body)?,
"Hello, this is the simulated file content!"
);
Ok(())
}
#[tokio::test]
async fn response_with_content_size_and_filename() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route(
"/file",
get(|| async {
// Simulating a file stream
let file_content = b"Hello, this is the simulated file content!".to_vec();
let size = file_content.len() as u64;
let reader = Cursor::new(file_content);
// Response file stream
let stream = ReaderStream::new(reader);
FileStream::new(stream)
.file_name("test")
.content_size(size)
.into_response()
}),
);
// Simulating a GET request
let response = app
.oneshot(Request::builder().uri("/file").body(Body::empty())?)
.await?;
// Validate Response Status Code
assert_eq!(response.status(), StatusCode::OK);
// Validate Response Headers
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/octet-stream"
);
assert_eq!(
response.headers().get("content-disposition").unwrap(),
"attachment; filename=\"test\""
);
assert_eq!(response.headers().get("content-length").unwrap(), "42");
// Validate Response Body
let body: &[u8] = &response.into_body().collect().await?.to_bytes();
assert_eq!(
std::str::from_utf8(body)?,
"Hello, this is the simulated file content!"
);
Ok(())
}
#[tokio::test]
async fn response_from_path() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route(
"/from_path",
get(move || async move {
FileStream::<ReaderStream<File>>::from_path(Path::new("CHANGELOG.md"))
.await
.unwrap()
.into_response()
}),
);
// Simulating a GET request
let response = app
.oneshot(
Request::builder()
.uri("/from_path")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
// Validate Response Status Code
assert_eq!(response.status(), StatusCode::OK);
// Validate Response Headers
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/octet-stream"
);
assert_eq!(
response.headers().get("content-disposition").unwrap(),
"attachment; filename=\"CHANGELOG.md\""
);
let file = File::open("CHANGELOG.md").await.unwrap();
// get file size
let content_length = file.metadata().await.unwrap().len();
assert_eq!(
response
.headers()
.get("content-length")
.unwrap()
.to_str()
.unwrap(),
content_length.to_string()
);
Ok(())
}
#[tokio::test]
async fn response_range_file() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route("/range_response", get(range_stream));
// Simulating a GET request
let response = app
.oneshot(
Request::builder()
.uri("/range_response")
.header(header::RANGE, "bytes=20-1000")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
// Validate Response Status Code
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
// Validate Response Headers
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/octet-stream"
);
let file = File::open("CHANGELOG.md").await.unwrap();
// get file size
let content_length = file.metadata().await.unwrap().len();
assert_eq!(
response
.headers()
.get("content-range")
.unwrap()
.to_str()
.unwrap(),
format!("bytes 20-1000/{content_length}")
);
Ok(())
}
async fn range_stream(headers: HeaderMap) -> Response {
let range_header = headers
.get(header::RANGE)
.and_then(|value| value.to_str().ok());
let (start, end) = if let Some(range) = range_header {
if let Some(range) = parse_range_header(range) {
range
} else {
return (StatusCode::RANGE_NOT_SATISFIABLE, "Invalid Range").into_response();
}
} else {
(0, 0) // default range end = 0, if end = 0 end == file size - 1
};
FileStream::<ReaderStream<File>>::try_range_response(Path::new("CHANGELOG.md"), start, end)
.await
.unwrap()
}
fn parse_range_header(range: &str) -> Option<(u64, u64)> {
let range = range.strip_prefix("bytes=")?;
let mut parts = range.split('-');
let start = parts.next()?.parse::<u64>().ok()?;
let end = parts
.next()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
if start > end {
return None;
}
Some((start, end))
}
}
+113
View File
@@ -0,0 +1,113 @@
//! Additional types for generating responses.
#[cfg(feature = "erased-json")]
mod erased_json;
#[cfg(feature = "attachment")]
mod attachment;
#[cfg(feature = "multipart")]
pub mod multiple;
#[cfg(feature = "error-response")]
mod error_response;
#[cfg(feature = "file-stream")]
/// Module for handling file streams.
pub mod file_stream;
#[cfg(feature = "file-stream")]
pub use file_stream::FileStream;
#[cfg(feature = "error-response")]
pub use error_response::InternalServerError;
#[cfg(feature = "erased-json")]
pub use erased_json::ErasedJson;
/// _not_ public API
#[cfg(feature = "erased-json")]
#[doc(hidden)]
pub use erased_json::private as __private_erased_json;
#[cfg(feature = "json-lines")]
#[doc(no_inline)]
pub use crate::json_lines::JsonLines;
#[cfg(feature = "attachment")]
pub use attachment::Attachment;
macro_rules! mime_response {
(
$(#[$m:meta])*
$ident:ident,
$mime:ident,
) => {
mime_response! {
$(#[$m])*
$ident,
mime::$mime.as_ref(),
}
};
(
$(#[$m:meta])*
$ident:ident,
$mime:expr,
) => {
$(#[$m])*
#[derive(Clone, Copy, Debug)]
#[must_use]
pub struct $ident<T>(pub T);
impl<T> axum::response::IntoResponse for $ident<T>
where
T: axum::response::IntoResponse,
{
fn into_response(self) -> axum::response::Response {
(
[(
http::header::CONTENT_TYPE,
http::HeaderValue::from_static($mime),
)],
self.0,
)
.into_response()
}
}
impl<T> From<T> for $ident<T> {
fn from(inner: T) -> Self {
Self(inner)
}
}
};
}
mime_response! {
/// A JavaScript response.
///
/// Will automatically get `Content-Type: application/javascript; charset=utf-8`.
JavaScript,
APPLICATION_JAVASCRIPT_UTF_8,
}
mime_response! {
/// A CSS response.
///
/// Will automatically get `Content-Type: text/css; charset=utf-8`.
Css,
TEXT_CSS_UTF_8,
}
mime_response! {
/// A WASM response.
///
/// Will automatically get `Content-Type: application/wasm`.
Wasm,
"application/wasm",
}
#[cfg(feature = "typed-header")]
#[doc(no_inline)]
pub use crate::typed_header::TypedHeader;
+297
View File
@@ -0,0 +1,297 @@
//! Generate forms to use in responses.
use axum::response::{IntoResponse, Response};
use fastrand;
use http::{header, HeaderMap, StatusCode};
use mime::Mime;
/// Create multipart forms to be used in API responses.
///
/// This struct implements [`IntoResponse`], and so it can be returned from a handler.
#[must_use]
#[derive(Debug)]
pub struct MultipartForm {
parts: Vec<Part>,
}
impl MultipartForm {
/// Initialize a new multipart form with the provided vector of parts.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// let parts: Vec<Part> = vec![Part::text("foo".to_string(), "abc"), Part::text("bar".to_string(), "def")];
/// let form = MultipartForm::with_parts(parts);
/// ```
pub fn with_parts(parts: Vec<Part>) -> Self {
MultipartForm { parts }
}
}
impl IntoResponse for MultipartForm {
fn into_response(self) -> Response {
// see RFC5758 for details
let boundary = generate_boundary();
let mut headers = HeaderMap::new();
let mime_type: Mime = match format!("multipart/form-data; boundary={boundary}").parse() {
Ok(m) => m,
// Realistically this should never happen unless the boundary generation code
// is modified, and that will be caught by unit tests
Err(_) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid multipart boundary generated",
)
.into_response()
}
};
// The use of unwrap is safe here because mime types are inherently string representable
headers.insert(header::CONTENT_TYPE, mime_type.to_string().parse().unwrap());
let mut serialized_form: Vec<u8> = Vec::new();
for part in self.parts {
// for each part, the boundary is preceded by two dashes
serialized_form.extend_from_slice(format!("--{boundary}\r\n").as_bytes());
serialized_form.extend_from_slice(&part.serialize());
}
serialized_form.extend_from_slice(format!("--{boundary}--").as_bytes());
(headers, serialized_form).into_response()
}
}
// Valid settings for that header are: "base64", "quoted-printable", "8bit", "7bit", and "binary".
/// A single part of a multipart form as defined by
/// <https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4>
/// and RFC5758.
#[derive(Debug)]
pub struct Part {
// Every part is expected to contain:
// - a [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
// header, where `Content-Disposition` is set to `form-data`, with a parameter of `name` that is set to
// the name of the field in the form. In the below example, the name of the field is `user`:
// ```
// Content-Disposition: form-data; name="user"
// ```
// If the field contains a file, then the `filename` parameter may be set to the name of the file.
// Handling for non-ascii field names is not done here, support for non-ascii characters may be encoded using
// methodology described in RFC 2047.
// - (optionally) a `Content-Type` header, which if not set, defaults to `text/plain`.
// If the field contains a file, then the file should be identified with that file's MIME type (eg: `image/gif`).
// If the `MIME` type is not known or specified, then the MIME type should be set to `application/octet-stream`.
/// The name of the part in question
name: String,
/// If the part should be treated as a file, the filename that should be attached that part
filename: Option<String>,
/// The `Content-Type` header. While not strictly required, it is always set here
mime_type: Mime,
/// The content/body of the part
contents: Vec<u8>,
}
impl Part {
/// Create a new part with `Content-Type` of `text/plain` with the supplied name and contents.
///
/// This form will not have a defined file name.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// // create a form with a single part that has a field with a name of "foo",
/// // and a value of "abc"
/// let parts: Vec<Part> = vec![Part::text("foo".to_string(), "abc")];
/// let form = MultipartForm::from_iter(parts);
/// ```
#[must_use]
pub fn text(name: String, contents: &str) -> Self {
Self {
name,
filename: None,
mime_type: mime::TEXT_PLAIN_UTF_8,
contents: contents.as_bytes().to_vec(),
}
}
/// Create a new part containing a generic file, with a `Content-Type` of `application/octet-stream`
/// using the provided file name, field name, and contents.
///
/// If the MIME type of the file is known, consider using `Part::raw_part`.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// // create a form with a single part that has a field with a name of "foo",
/// // with a file name of "foo.txt", and with the specified contents
/// let parts: Vec<Part> = vec![Part::file("foo", "foo.txt", vec![0x68, 0x68, 0x20, 0x6d, 0x6f, 0x6d])];
/// let form = MultipartForm::from_iter(parts);
/// ```
#[must_use]
pub fn file(field_name: &str, file_name: &str, contents: Vec<u8>) -> Self {
Self {
name: field_name.to_owned(),
filename: Some(file_name.to_owned()),
// If the `MIME` type is not known or specified, then the MIME type should be set to `application/octet-stream`.
// See RFC2388 section 3 for specifics.
mime_type: mime::APPLICATION_OCTET_STREAM,
contents,
}
}
/// Create a new part with more fine-grained control over the semantics of that part.
///
/// The caller is assumed to have set a valid MIME type.
///
/// This function will return an error if the provided MIME type is not valid.
///
/// # Examples
///
/// ```rust
/// use axum_extra::response::multiple::{MultipartForm, Part};
///
/// // create a form with a single part that has a field with a name of "part_name",
/// // with a MIME type of "application/json", and the supplied contents.
/// let parts: Vec<Part> = vec![Part::raw_part("part_name", "application/json", vec![0x68, 0x68, 0x20, 0x6d, 0x6f, 0x6d], None).expect("MIME type must be valid")];
/// let form = MultipartForm::from_iter(parts);
/// ```
pub fn raw_part(
name: &str,
mime_type: &str,
contents: Vec<u8>,
filename: Option<&str>,
) -> Result<Self, &'static str> {
let mime_type = mime_type.parse().map_err(|_| "Invalid MIME type")?;
Ok(Self {
name: name.to_owned(),
filename: filename.map(|f| f.to_owned()),
mime_type,
contents,
})
}
/// Serialize this part into a chunk that can be easily inserted into a larger form
pub(super) fn serialize(&self) -> Vec<u8> {
// A part is serialized in this general format:
// // the filename is optional
// Content-Disposition: form-data; name="FIELD_NAME"; filename="FILENAME"\r\n
// // the mime type (not strictly required by the spec, but always sent here)
// Content-Type: mime/type\r\n
// // a blank line, then the contents of the file start
// \r\n
// CONTENTS\r\n
// Format what we can as a string, then handle the rest at a byte level
let mut serialized_part = format!("Content-Disposition: form-data; name=\"{}\"", self.name);
// specify a filename if one was set
if let Some(filename) = &self.filename {
serialized_part += &format!("; filename=\"{filename}\"");
}
serialized_part += "\r\n";
// specify the MIME type
serialized_part += &format!("Content-Type: {}\r\n", self.mime_type);
serialized_part += "\r\n";
let mut part_bytes = serialized_part.as_bytes().to_vec();
part_bytes.extend_from_slice(&self.contents);
part_bytes.extend_from_slice(b"\r\n");
part_bytes
}
}
impl FromIterator<Part> for MultipartForm {
fn from_iter<T: IntoIterator<Item = Part>>(iter: T) -> Self {
Self {
parts: iter.into_iter().collect(),
}
}
}
/// A boundary is defined as a user defined (arbitrary) value that does not occur in any of the data.
///
/// Because the specification does not clearly define a methodology for generating boundaries, this implementation
/// follow's Reqwest's, and generates a boundary in the format of `XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX` where `XXXXXXXX`
/// is a hexadecimal representation of a pseudo randomly generated u64.
fn generate_boundary() -> String {
let a = fastrand::u64(0..u64::MAX);
let b = fastrand::u64(0..u64::MAX);
let c = fastrand::u64(0..u64::MAX);
let d = fastrand::u64(0..u64::MAX);
format!("{a:016x}-{b:016x}-{c:016x}-{d:016x}")
}
#[cfg(test)]
mod tests {
use super::{generate_boundary, MultipartForm, Part};
use axum::{body::Body, http};
use axum::{routing::get, Router};
use http::{Request, Response};
use http_body_util::BodyExt;
use mime::Mime;
use tower::ServiceExt;
#[tokio::test]
async fn process_form() -> Result<(), Box<dyn std::error::Error>> {
// create a boilerplate handle that returns a form
async fn handle() -> MultipartForm {
let parts: Vec<Part> = vec![
Part::text("part1".to_owned(), "basictext"),
Part::file(
"part2",
"file.txt",
vec![0x68, 0x69, 0x20, 0x6d, 0x6f, 0x6d],
),
Part::raw_part("part3", "text/plain", b"rawpart".to_vec(), None).unwrap(),
];
MultipartForm::from_iter(parts)
}
// make a request to that handle
let app = Router::new().route("/", get(handle));
let response: Response<_> = app
.oneshot(Request::builder().uri("/").body(Body::empty())?)
.await?;
// content_type header
let ct_header = response.headers().get("content-type").unwrap().to_str()?;
let boundary = ct_header.split("boundary=").nth(1).unwrap().to_owned();
let body: &[u8] = &response.into_body().collect().await?.to_bytes();
assert_eq!(
std::str::from_utf8(body)?,
format!(
"--{boundary}\r\n\
Content-Disposition: form-data; name=\"part1\"\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
basictext\r\n\
--{boundary}\r\n\
Content-Disposition: form-data; name=\"part2\"; filename=\"file.txt\"\r\n\
Content-Type: application/octet-stream\r\n\
\r\n\
hi mom\r\n\
--{boundary}\r\n\
Content-Disposition: form-data; name=\"part3\"\r\n\
Content-Type: text/plain\r\n\
\r\n\
rawpart\r\n\
--{boundary}--",
)
);
Ok(())
}
#[test]
fn valid_boundary_generation() {
for _ in 0..256 {
let boundary = generate_boundary();
let mime_type: Result<Mime, _> =
format!("multipart/form-data; boundary={boundary}").parse();
assert!(
mime_type.is_ok(),
"The generated boundary was unable to be parsed into a valid mime type."
);
}
}
}
+508
View File
@@ -0,0 +1,508 @@
//! Additional types for defining routes.
use axum::{
extract::{OriginalUri, Request},
response::{IntoResponse, Redirect, Response},
routing::{any, MethodRouter},
Router,
};
use http::{uri::PathAndQuery, StatusCode, Uri};
use std::{borrow::Cow, convert::Infallible};
use tower_service::Service;
mod resource;
#[cfg(feature = "typed-routing")]
mod typed;
pub use self::resource::Resource;
#[cfg(feature = "typed-routing")]
pub use self::typed::WithQueryParams;
#[cfg(feature = "typed-routing")]
pub use axum_macros::TypedPath;
#[cfg(feature = "typed-routing")]
pub use self::typed::{SecondElementIs, TypedPath};
// Validates a path at compile time, used with the vpath macro.
#[rustversion::since(1.80)]
#[doc(hidden)]
#[must_use]
pub const fn __private_validate_static_path(path: &'static str) -> &'static str {
if path.is_empty() {
panic!("Paths must start with a `/`. Use \"/\" for root routes")
}
if path.as_bytes()[0] != b'/' {
panic!("Paths must start with /");
}
path
}
/// This macro aborts compilation if the path is invalid.
///
/// This example will fail to compile:
///
/// ```compile_fail
/// use axum::routing::{Router, get};
/// use axum_extra::vpath;
///
/// let router = axum::Router::<()>::new()
/// .route(vpath!("invalid_path"), get(root))
/// .to_owned();
///
/// async fn root() {}
/// ```
///
/// This one will compile without problems:
///
/// ```no_run
/// use axum::routing::{Router, get};
/// use axum_extra::vpath;
///
/// let router = axum::Router::<()>::new()
/// .route(vpath!("/valid_path"), get(root))
/// .to_owned();
///
/// async fn root() {}
/// ```
///
/// This macro is available only on rust versions 1.80 and above.
#[rustversion::since(1.80)]
#[macro_export]
macro_rules! vpath {
($e:expr) => {
const { $crate::routing::__private_validate_static_path($e) }
};
}
/// Extension trait that adds additional methods to [`Router`].
#[allow(clippy::return_self_not_must_use)]
pub trait RouterExt<S>: sealed::Sealed {
/// Add a typed `GET` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_get<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `DELETE` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_delete<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `HEAD` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_head<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `OPTIONS` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_options<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `PATCH` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_patch<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `POST` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_post<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `PUT` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_put<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `TRACE` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_trace<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add a typed `CONNECT` route to the router.
///
/// The path will be inferred from the first argument to the handler function which must
/// implement [`TypedPath`].
///
/// See [`TypedPath`] for more details and examples.
#[cfg(feature = "typed-routing")]
fn typed_connect<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath;
/// Add another route to the router with an additional "trailing slash redirect" route.
///
/// If you add a route _without_ a trailing slash, such as `/foo`, this method will also add a
/// route for `/foo/` that redirects to `/foo`.
///
/// If you add a route _with_ a trailing slash, such as `/bar/`, this method will also add a
/// route for `/bar` that redirects to `/bar/`.
///
/// This is similar to what axum 0.5.x did by default, except this explicitly adds another
/// route, so trying to add a `/foo/` route after calling `.route_with_tsr("/foo", /* ... */)`
/// will result in a panic due to route overlap.
///
/// # Example
///
/// ```
/// use axum::{Router, routing::get};
/// use axum_extra::routing::RouterExt;
///
/// let app = Router::new()
/// // `/foo/` will redirect to `/foo`
/// .route_with_tsr("/foo", get(|| async {}))
/// // `/bar` will redirect to `/bar/`
/// .route_with_tsr("/bar/", get(|| async {}));
/// # let _: Router = app;
/// ```
fn route_with_tsr(self, path: &str, method_router: MethodRouter<S>) -> Self
where
Self: Sized;
/// Add another route to the router with an additional "trailing slash redirect" route.
///
/// This works like [`RouterExt::route_with_tsr`] but accepts any [`Service`].
fn route_service_with_tsr<T>(self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
Self: Sized;
}
impl<S> RouterExt<S> for Router<S>
where
S: Clone + Send + Sync + 'static,
{
#[cfg(feature = "typed-routing")]
fn typed_get<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::get(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_delete<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::delete(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_head<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::head(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_options<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::options(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_patch<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::patch(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_post<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::post(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_put<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::put(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_trace<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::trace(handler))
}
#[cfg(feature = "typed-routing")]
fn typed_connect<H, T, P>(self, handler: H) -> Self
where
H: axum::handler::Handler<T, S>,
T: SecondElementIs<P> + 'static,
P: TypedPath,
{
self.route(P::PATH, axum::routing::connect(handler))
}
#[track_caller]
fn route_with_tsr(mut self, path: &str, method_router: MethodRouter<S>) -> Self
where
Self: Sized,
{
validate_tsr_path(path);
self = self.route(path, method_router);
add_tsr_redirect_route(self, path)
}
#[track_caller]
fn route_service_with_tsr<T>(mut self, path: &str, service: T) -> Self
where
T: Service<Request, Error = Infallible> + Clone + Send + Sync + 'static,
T::Response: IntoResponse,
T::Future: Send + 'static,
Self: Sized,
{
validate_tsr_path(path);
self = self.route_service(path, service);
add_tsr_redirect_route(self, path)
}
}
#[track_caller]
fn validate_tsr_path(path: &str) {
if path == "/" {
panic!("Cannot add a trailing slash redirect route for `/`")
}
}
fn add_tsr_redirect_route<S>(router: Router<S>, path: &str) -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
async fn redirect_handler(OriginalUri(uri): OriginalUri) -> Response {
let new_uri = map_path(uri, |path| {
path.strip_suffix('/')
.map(Cow::Borrowed)
.unwrap_or_else(|| Cow::Owned(format!("{path}/")))
});
if let Some(new_uri) = new_uri {
Redirect::permanent(&new_uri.to_string()).into_response()
} else {
StatusCode::BAD_REQUEST.into_response()
}
}
if let Some(path_without_trailing_slash) = path.strip_suffix('/') {
router.route(path_without_trailing_slash, any(redirect_handler))
} else {
router.route(&format!("{path}/"), any(redirect_handler))
}
}
/// Map the path of a `Uri`.
///
/// Returns `None` if the `Uri` cannot be put back together with the new path.
fn map_path<F>(original_uri: Uri, f: F) -> Option<Uri>
where
F: FnOnce(&str) -> Cow<'_, str>,
{
let mut parts = original_uri.into_parts();
let path_and_query = parts.path_and_query.as_ref()?;
let new_path = f(path_and_query.path());
let new_path_and_query = if let Some(query) = &path_and_query.query() {
format!("{new_path}?{query}").parse::<PathAndQuery>().ok()?
} else {
new_path.parse::<PathAndQuery>().ok()?
};
parts.path_and_query = Some(new_path_and_query);
Uri::from_parts(parts).ok()
}
mod sealed {
pub trait Sealed {}
impl<S> Sealed for axum::Router<S> {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{extract::Path, routing::get};
#[tokio::test]
async fn test_tsr() {
let app = Router::new()
.route_with_tsr("/foo", get(|| async {}))
.route_with_tsr("/bar/", get(|| async {}));
let client = TestClient::new(app);
let res = client.get("/foo").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("/foo/").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/foo");
let res = client.get("/bar/").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("/bar").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/bar/");
}
#[tokio::test]
async fn tsr_with_params() {
let app = Router::new()
.route_with_tsr(
"/a/{a}",
get(|Path(param): Path<String>| async move { param }),
)
.route_with_tsr(
"/b/{b}/",
get(|Path(param): Path<String>| async move { param }),
);
let client = TestClient::new(app);
let res = client.get("/a/foo").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo");
let res = client.get("/a/foo/").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/a/foo");
let res = client.get("/b/foo/").await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text().await, "foo");
let res = client.get("/b/foo").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/b/foo/");
}
#[tokio::test]
async fn tsr_maintains_query_params() {
let app = Router::new().route_with_tsr("/foo", get(|| async {}));
let client = TestClient::new(app);
let res = client.get("/foo/?a=a").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/foo?a=a");
}
#[tokio::test]
async fn tsr_works_in_nested_router() {
let app = Router::new().nest(
"/neko",
Router::new().route_with_tsr("/nyan/", get(|| async {})),
);
let client = TestClient::new(app);
let res = client.get("/neko/nyan/").await;
assert_eq!(res.status(), StatusCode::OK);
let res = client.get("/neko/nyan").await;
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
assert_eq!(res.headers()["location"], "/neko/nyan/");
}
#[test]
#[should_panic = "Cannot add a trailing slash redirect route for `/`"]
fn tsr_at_root() {
let _: Router = Router::new().route_with_tsr("/", get(|| async move {}));
}
}
+231
View File
@@ -0,0 +1,231 @@
use axum::{
handler::Handler,
routing::{delete, get, on, post, MethodFilter, MethodRouter},
Router,
};
/// A resource which defines a set of conventional CRUD routes.
///
/// # Example
///
/// ```rust
/// use axum::{Router, routing::get, extract::Path};
/// use axum_extra::routing::{RouterExt, Resource};
///
/// let users = Resource::named("users")
/// // Define a route for `GET /users`
/// .index(|| async {})
/// // `POST /users`
/// .create(|| async {})
/// // `GET /users/new`
/// .new(|| async {})
/// // `GET /users/{users_id}`
/// .show(|Path(user_id): Path<u64>| async {})
/// // `GET /users/{users_id}/edit`
/// .edit(|Path(user_id): Path<u64>| async {})
/// // `PUT or PATCH /users/{users_id}`
/// .update(|Path(user_id): Path<u64>| async {})
/// // `DELETE /users/{users_id}`
/// .destroy(|Path(user_id): Path<u64>| async {});
///
/// let app = Router::new().merge(users);
/// # let _: Router = app;
/// ```
#[derive(Debug)]
#[must_use]
pub struct Resource<S = ()> {
pub(crate) name: String,
pub(crate) router: Router<S>,
}
impl<S> Resource<S>
where
S: Clone + Send + Sync + 'static,
{
/// Create a `Resource` with the given name.
///
/// All routes will be nested at `/{resource_name}`.
pub fn named(resource_name: &str) -> Self {
Self {
name: resource_name.to_owned(),
router: Router::new(),
}
}
/// Add a handler at `GET /{resource_name}`.
pub fn index<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = self.index_create_path();
self.route(&path, get(handler))
}
/// Add a handler at `POST /{resource_name}`.
pub fn create<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = self.index_create_path();
self.route(&path, post(handler))
}
/// Add a handler at `GET /{resource_name}/new`.
pub fn new<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = format!("/{}/new", self.name);
self.route(&path, get(handler))
}
/// Add a handler at `GET /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `GET /post/{post_id}`.
pub fn show<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = self.show_update_destroy_path();
self.route(&path, get(handler))
}
/// Add a handler at `GET /<resource_name>/{<resource_name>_id}/edit`.
///
/// For example when the resources are posts: `GET /post/{post_id}/edit`.
pub fn edit<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = format!("/{0}/{{{0}_id}}/edit", self.name);
self.route(&path, get(handler))
}
/// Add a handler at `PUT or PATCH /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `PUT /post/{post_id}`.
pub fn update<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = self.show_update_destroy_path();
self.route(
&path,
on(MethodFilter::PUT.or(MethodFilter::PATCH), handler),
)
}
/// Add a handler at `DELETE /<resource_name>/{<resource_name>_id}`.
///
/// For example when the resources are posts: `DELETE /post/{post_id}`.
pub fn destroy<H, T>(self, handler: H) -> Self
where
H: Handler<T, S>,
T: 'static,
{
let path = self.show_update_destroy_path();
self.route(&path, delete(handler))
}
fn index_create_path(&self) -> String {
format!("/{}", self.name)
}
fn show_update_destroy_path(&self) -> String {
format!("/{0}/{{{0}_id}}", self.name)
}
fn route(mut self, path: &str, method_router: MethodRouter<S>) -> Self {
self.router = self.router.route(path, method_router);
self
}
}
impl<S> From<Resource<S>> for Router<S> {
fn from(resource: Resource<S>) -> Self {
resource.router
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
use axum::{body::Body, extract::Path, http::Method};
use http::Request;
use http_body_util::BodyExt;
use tower::ServiceExt;
#[tokio::test]
async fn works() {
let users = Resource::named("users")
.index(|| async { "users#index" })
.create(|| async { "users#create" })
.new(|| async { "users#new" })
.show(|Path(id): Path<u64>| async move { format!("users#show id={id}") })
.edit(|Path(id): Path<u64>| async move { format!("users#edit id={id}") })
.update(|Path(id): Path<u64>| async move { format!("users#update id={id}") })
.destroy(|Path(id): Path<u64>| async move { format!("users#destroy id={id}") });
let app = Router::new().merge(users);
assert_eq!(call_route(&app, Method::GET, "/users").await, "users#index");
assert_eq!(
call_route(&app, Method::POST, "/users").await,
"users#create"
);
assert_eq!(
call_route(&app, Method::GET, "/users/new").await,
"users#new"
);
assert_eq!(
call_route(&app, Method::GET, "/users/1").await,
"users#show id=1"
);
assert_eq!(
call_route(&app, Method::GET, "/users/1/edit").await,
"users#edit id=1"
);
assert_eq!(
call_route(&app, Method::PATCH, "/users/1").await,
"users#update id=1"
);
assert_eq!(
call_route(&app, Method::PUT, "/users/1").await,
"users#update id=1"
);
assert_eq!(
call_route(&app, Method::DELETE, "/users/1").await,
"users#destroy id=1"
);
}
async fn call_route(app: &Router, method: Method, uri: &str) -> String {
let res = app
.clone()
.oneshot(
Request::builder()
.method(method)
.uri(uri)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
let bytes = res.collect().await.unwrap().to_bytes();
String::from_utf8(bytes.to_vec()).unwrap()
}
}
+464
View File
@@ -0,0 +1,464 @@
use std::{any::type_name, fmt};
use super::sealed::Sealed;
use http::Uri;
use serde_core::Serialize;
/// A type safe path.
///
/// This is used to statically connect a path to its corresponding handler using
/// [`RouterExt::typed_get`], [`RouterExt::typed_post`], etc.
///
/// # Example
///
/// ```rust
/// use serde::Deserialize;
/// use axum::{Router, extract::Json};
/// use axum_extra::routing::{
/// TypedPath,
/// RouterExt, // for `Router::typed_*`
/// };
///
/// // A type safe route with `/users/{id}` as its associated path.
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: u32,
/// }
///
/// // A regular handler function that takes `UsersMember` as the first argument
/// // and thus creates a typed connection between this handler and the `/users/{id}` path.
/// //
/// // The `TypedPath` must be the first argument to the function.
/// async fn users_show(
/// UsersMember { id }: UsersMember,
/// ) {
/// // ...
/// }
///
/// let app = Router::new()
/// // Add our typed route to the router.
/// //
/// // The path will be inferred to `/users/{id}` since `users_show`'s
/// // first argument is `UsersMember` which implements `TypedPath`
/// .typed_get(users_show)
/// .typed_post(users_create)
/// .typed_delete(users_destroy);
///
/// #[derive(TypedPath)]
/// #[typed_path("/users")]
/// struct UsersCollection;
///
/// #[derive(Deserialize)]
/// struct UsersCreatePayload { /* ... */ }
///
/// async fn users_create(
/// _: UsersCollection,
/// // Our handlers can accept other extractors.
/// Json(payload): Json<UsersCreatePayload>,
/// ) {
/// // ...
/// }
///
/// async fn users_destroy(_: UsersCollection) { /* ... */ }
///
/// #
/// # let app: Router = app;
/// ```
///
/// # Using `#[derive(TypedPath)]`
///
/// While `TypedPath` can be implemented manually, it's _highly_ recommended to derive it:
///
/// ```
/// use serde::Deserialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: u32,
/// }
/// ```
///
/// The macro expands to:
///
/// - A `TypedPath` implementation.
/// - A [`FromRequest`] implementation compatible with [`RouterExt::typed_get`],
/// [`RouterExt::typed_post`], etc. This implementation uses [`Path`] and thus your struct must
/// also implement [`serde::Deserialize`], unless it's a unit struct.
/// - A [`Display`] implementation that interpolates the captures. This can be used to, among other
/// things, create links to known paths and have them verified statically. Note that the
/// [`Display`] implementation for each field must return something that's compatible with its
/// [`Deserialize`] implementation.
///
/// Additionally the macro will verify the captures in the path matches the fields of the struct.
/// For example this fails to compile since the struct doesn't have a `team_id` field:
///
/// ```compile_fail
/// use serde::Deserialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/{id}/teams/{team_id}")]
/// struct UsersMember {
/// id: u32,
/// }
/// ```
///
/// Unit and tuple structs are also supported:
///
/// ```
/// use serde::Deserialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath)]
/// #[typed_path("/users")]
/// struct UsersCollection;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/{id}")]
/// struct UsersMember(u32);
/// ```
///
/// ## Percent encoding
///
/// The generated [`Display`] implementation will automatically percent-encode the arguments:
///
/// ```
/// use serde::Deserialize;
/// use axum_extra::routing::TypedPath;
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/{id}")]
/// struct UsersMember {
/// id: String,
/// }
///
/// assert_eq!(
/// UsersMember {
/// id: "foo bar".to_string(),
/// }.to_string(),
/// "/users/foo%20bar",
/// );
/// ```
///
/// ## Customizing the rejection
///
/// By default the rejection used in the [`FromRequest`] implementation will be [`PathRejection`].
///
/// That can be customized using `#[typed_path("...", rejection(YourType))]`:
///
/// ```
/// use serde::Deserialize;
/// use axum_extra::routing::TypedPath;
/// use axum::{
/// response::{IntoResponse, Response},
/// extract::rejection::PathRejection,
/// };
///
/// #[derive(TypedPath, Deserialize)]
/// #[typed_path("/users/{id}", rejection(UsersMemberRejection))]
/// struct UsersMember {
/// id: String,
/// }
///
/// struct UsersMemberRejection;
///
/// // Your rejection type must implement `From<PathRejection>`.
/// //
/// // Here you can grab whatever details from the inner rejection
/// // that you need.
/// impl From<PathRejection> for UsersMemberRejection {
/// fn from(rejection: PathRejection) -> Self {
/// # UsersMemberRejection
/// // ...
/// }
/// }
///
/// // Your rejection must implement `IntoResponse`, like all rejections.
/// impl IntoResponse for UsersMemberRejection {
/// fn into_response(self) -> Response {
/// # ().into_response()
/// // ...
/// }
/// }
/// ```
///
/// The `From<PathRejection>` requirement only applies if your typed path is a struct with named
/// fields or a tuple struct. For unit structs your rejection type must implement `Default`:
///
/// ```
/// use axum_extra::routing::TypedPath;
/// use axum::response::{IntoResponse, Response};
///
/// #[derive(TypedPath)]
/// #[typed_path("/users", rejection(UsersCollectionRejection))]
/// struct UsersCollection;
///
/// #[derive(Default)]
/// struct UsersCollectionRejection;
///
/// impl IntoResponse for UsersCollectionRejection {
/// fn into_response(self) -> Response {
/// # ().into_response()
/// // ...
/// }
/// }
/// ```
///
/// [`FromRequest`]: axum::extract::FromRequest
/// [`RouterExt::typed_get`]: super::RouterExt::typed_get
/// [`RouterExt::typed_post`]: super::RouterExt::typed_post
/// [`Path`]: axum::extract::Path
/// [`Display`]: std::fmt::Display
/// [`Deserialize`]: serde::Deserialize
/// [`PathRejection`]: axum::extract::rejection::PathRejection
pub trait TypedPath: std::fmt::Display {
/// The path with optional captures such as `/users/{id}`.
const PATH: &'static str;
/// Convert the path into a `Uri`.
///
/// # Panics
///
/// The default implementation parses the required [`Display`] implementation. If that fails it
/// will panic.
///
/// Using `#[derive(TypedPath)]` will never result in a panic since it percent-encodes
/// arguments.
///
/// [`Display`]: std::fmt::Display
fn to_uri(&self) -> Uri {
self.to_string().parse().unwrap()
}
/// Add query parameters to a path.
///
/// # Example
///
/// ```
/// use axum_extra::routing::TypedPath;
/// use serde::Serialize;
///
/// #[derive(TypedPath)]
/// #[typed_path("/users")]
/// struct Users;
///
/// #[derive(Serialize)]
/// struct Pagination {
/// page: u32,
/// per_page: u32,
/// }
///
/// let path = Users.with_query_params(Pagination {
/// page: 1,
/// per_page: 10,
/// });
///
/// assert_eq!(path.to_uri(), "/users?&page=1&per_page=10");
/// ```
///
/// # Panics
///
/// If `params` doesn't support being serialized as query params [`WithQueryParams`]'s [`Display`]
/// implementation will panic, and thus [`WithQueryParams::to_uri`] will also panic.
///
/// [`WithQueryParams::to_uri`]: TypedPath::to_uri
/// [`Display`]: std::fmt::Display
fn with_query_params<T>(self, params: T) -> WithQueryParams<Self, T>
where
T: Serialize,
Self: Sized,
{
WithQueryParams { path: self, params }
}
}
/// A [`TypedPath`] with query params.
///
/// See [`TypedPath::with_query_params`] for more details.
#[derive(Debug, Clone, Copy)]
pub struct WithQueryParams<P, T> {
path: P,
params: T,
}
impl<P, T> fmt::Display for WithQueryParams<P, T>
where
P: TypedPath,
T: Serialize,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = self.path.to_string();
if !out.contains('?') {
out.push('?');
}
let mut urlencoder = form_urlencoded::Serializer::new(&mut out);
self.params
.serialize(serde_html_form::ser::Serializer::new(&mut urlencoder))
.unwrap_or_else(|err| {
panic!(
"failed to URL encode value of type `{}`: {err}",
type_name::<T>(),
)
});
f.write_str(&out)?;
Ok(())
}
}
impl<P, T> TypedPath for WithQueryParams<P, T>
where
P: TypedPath,
T: Serialize,
{
const PATH: &'static str = P::PATH;
}
/// Utility trait used with [`RouterExt`] to ensure the second element of a tuple type is a
/// given type.
///
/// If you see it in type errors it's most likely because the second argument to your handler doesn't
/// implement [`TypedPath`].
///
/// You normally shouldn't have to use this trait directly.
///
/// It is sealed such that it cannot be implemented outside this crate.
///
/// [`RouterExt`]: super::RouterExt
pub trait SecondElementIs<P>: Sealed {}
macro_rules! impl_second_element_is {
( $($ty:ident),* $(,)? ) => {
impl<M, P, $($ty,)*> SecondElementIs<P> for (M, P, $($ty,)*)
where
P: TypedPath
{}
impl<M, P, $($ty,)*> Sealed for (M, P, $($ty,)*)
where
P: TypedPath
{}
impl<M, P, $($ty,)*> SecondElementIs<P> for (M, Option<P>, $($ty,)*)
where
P: TypedPath
{}
impl<M, P, $($ty,)*> Sealed for (M, Option<P>, $($ty,)*)
where
P: TypedPath
{}
impl<M, P, E, $($ty,)*> SecondElementIs<P> for (M, Result<P, E>, $($ty,)*)
where
P: TypedPath
{}
impl<M, P, E, $($ty,)*> Sealed for (M, Result<P, E>, $($ty,)*)
where
P: TypedPath
{}
};
}
impl_second_element_is!();
impl_second_element_is!(T1);
impl_second_element_is!(T1, T2);
impl_second_element_is!(T1, T2, T3);
impl_second_element_is!(T1, T2, T3, T4);
impl_second_element_is!(T1, T2, T3, T4, T5);
impl_second_element_is!(T1, T2, T3, T4, T5, T6);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15);
impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16);
#[cfg(test)]
mod tests {
use crate::{
extract::WithRejection,
routing::{RouterExt, TypedPath},
};
use axum::{
extract::rejection::PathRejection,
response::{IntoResponse, Response},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(TypedPath, Deserialize)]
#[typed_path("/users/{id}")]
struct UsersShow {
id: i32,
}
#[derive(Serialize)]
struct Params {
foo: &'static str,
bar: i32,
baz: bool,
}
#[test]
fn with_params() {
let path = UsersShow { id: 1 }.with_query_params(Params {
foo: "foo",
bar: 123,
baz: true,
});
let uri = path.to_uri();
// according to [the spec] starting the params with `?&` is allowed specifically:
//
// > If bytes is the empty byte sequence, then continue.
//
// [the spec]: https://url.spec.whatwg.org/#urlencoded-parsing
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true");
}
#[test]
fn with_params_called_multiple_times() {
let path = UsersShow { id: 1 }
.with_query_params(Params {
foo: "foo",
bar: 123,
baz: true,
})
.with_query_params([("qux", 1337)]);
let uri = path.to_uri();
assert_eq!(uri, "/users/1?&foo=foo&bar=123&baz=true&qux=1337");
}
#[allow(dead_code)] // just needs to compile
fn supports_with_rejection() {
async fn handler(_: WithRejection<UsersShow, MyRejection>) {}
struct MyRejection {}
impl IntoResponse for MyRejection {
fn into_response(self) -> Response {
unimplemented!()
}
}
impl From<PathRejection> for MyRejection {
fn from(_: PathRejection) -> Self {
unimplemented!()
}
}
let _: Router = Router::new().typed_get(handler);
}
}
+253
View File
@@ -0,0 +1,253 @@
//! Extractor and response for typed headers.
use axum::{
extract::{FromRequestParts, OptionalFromRequestParts},
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
};
use headers::{Header, HeaderMapExt};
use http::{request::Parts, StatusCode};
use std::convert::Infallible;
/// Extractor and response that works with typed header values from [`headers`].
///
/// # As extractor
///
/// In general, it's recommended to extract only the needed headers via `TypedHeader` rather than
/// removing all headers with the `HeaderMap` extractor.
///
/// ```rust,no_run
/// use axum::{
/// routing::get,
/// Router,
/// };
/// use headers::UserAgent;
/// use axum_extra::TypedHeader;
///
/// async fn users_teams_show(
/// TypedHeader(user_agent): TypedHeader<UserAgent>,
/// ) {
/// // ...
/// }
///
/// let app = Router::new().route("/users/{user_id}/team/{team_id}", get(users_teams_show));
/// # let _: Router = app;
/// ```
///
/// # As response
///
/// ```rust
/// use axum::{
/// response::IntoResponse,
/// };
/// use headers::ContentType;
/// use axum_extra::TypedHeader;
///
/// async fn handler() -> (TypedHeader<ContentType>, &'static str) {
/// (
/// TypedHeader(ContentType::text_utf8()),
/// "Hello, World!",
/// )
/// }
/// ```
#[cfg(feature = "typed-header")]
#[derive(Debug, Clone, Copy)]
#[must_use]
pub struct TypedHeader<T>(pub T);
impl<T, S> FromRequestParts<S> for TypedHeader<T>
where
T: Header,
S: Send + Sync,
{
type Rejection = TypedHeaderRejection;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let mut values = parts.headers.get_all(T::name()).iter();
let is_missing = values.size_hint() == (0, Some(0));
T::decode(&mut values)
.map(Self)
.map_err(|err| TypedHeaderRejection {
name: T::name(),
reason: if is_missing {
// Report a more precise rejection for the missing header case.
TypedHeaderRejectionReason::Missing
} else {
TypedHeaderRejectionReason::Error(err)
},
})
}
}
impl<T, S> OptionalFromRequestParts<S> for TypedHeader<T>
where
T: Header,
S: Send + Sync,
{
type Rejection = TypedHeaderRejection;
async fn from_request_parts(
parts: &mut Parts,
_state: &S,
) -> Result<Option<Self>, Self::Rejection> {
let mut values = parts.headers.get_all(T::name()).iter();
let is_missing = values.size_hint() == (0, Some(0));
match T::decode(&mut values) {
Ok(res) => Ok(Some(Self(res))),
Err(_) if is_missing => Ok(None),
Err(err) => Err(TypedHeaderRejection {
name: T::name(),
reason: TypedHeaderRejectionReason::Error(err),
}),
}
}
}
axum_core::__impl_deref!(TypedHeader);
impl<T> IntoResponseParts for TypedHeader<T>
where
T: Header,
{
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.headers_mut().typed_insert(self.0);
Ok(res)
}
}
impl<T> IntoResponse for TypedHeader<T>
where
T: Header,
{
fn into_response(self) -> Response {
let mut res = ().into_response();
res.headers_mut().typed_insert(self.0);
res
}
}
/// Rejection used for [`TypedHeader`].
#[cfg(feature = "typed-header")]
#[derive(Debug)]
pub struct TypedHeaderRejection {
name: &'static http::header::HeaderName,
reason: TypedHeaderRejectionReason,
}
impl TypedHeaderRejection {
/// Name of the header that caused the rejection
#[must_use]
pub fn name(&self) -> &http::header::HeaderName {
self.name
}
/// Reason why the header extraction has failed
#[must_use]
pub fn reason(&self) -> &TypedHeaderRejectionReason {
&self.reason
}
/// Returns `true` if the typed header rejection reason is [`Missing`].
///
/// [`Missing`]: TypedHeaderRejectionReason::Missing
#[must_use]
pub fn is_missing(&self) -> bool {
self.reason.is_missing()
}
}
/// Additional information regarding a [`TypedHeaderRejection`]
#[cfg(feature = "typed-header")]
#[derive(Debug)]
#[non_exhaustive]
pub enum TypedHeaderRejectionReason {
/// The header was missing from the HTTP request
Missing,
/// An error occurred when parsing the header from the HTTP request
Error(headers::Error),
}
impl TypedHeaderRejectionReason {
/// Returns `true` if the typed header rejection reason is [`Missing`].
///
/// [`Missing`]: TypedHeaderRejectionReason::Missing
#[must_use]
pub fn is_missing(&self) -> bool {
matches!(self, Self::Missing)
}
}
impl IntoResponse for TypedHeaderRejection {
fn into_response(self) -> Response {
let status = StatusCode::BAD_REQUEST;
let body = self.to_string();
axum_core::__log_rejection!(rejection_type = Self, body_text = body, status = status,);
(status, body).into_response()
}
}
impl std::fmt::Display for TypedHeaderRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.reason {
TypedHeaderRejectionReason::Missing => {
write!(f, "Header of type `{}` was missing", self.name)
}
TypedHeaderRejectionReason::Error(err) => {
write!(f, "{err} ({})", self.name)
}
}
}
}
impl std::error::Error for TypedHeaderRejection {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.reason {
TypedHeaderRejectionReason::Error(err) => Some(err),
TypedHeaderRejectionReason::Missing => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::get, Router};
#[tokio::test]
async fn typed_header() {
async fn handle(
TypedHeader(user_agent): TypedHeader<headers::UserAgent>,
TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> impl IntoResponse {
let user_agent = user_agent.as_str();
let cookies = cookies.iter().collect::<Vec<_>>();
format!("User-Agent={user_agent:?}, Cookie={cookies:?}")
}
let app = Router::new().route("/", get(handle));
let client = TestClient::new(app);
let res = client
.get("/")
.header("user-agent", "foobar")
.header("cookie", "a=1; b=2")
.header("cookie", "c=3")
.await;
let body = res.text().await;
assert_eq!(
body,
r#"User-Agent="foobar", Cookie=[("a", "1"), ("b", "2"), ("c", "3")]"#
);
let res = client.get("/").header("user-agent", "foobar").await;
let body = res.text().await;
assert_eq!(body, r#"User-Agent="foobar", Cookie=[]"#);
let res = client.get("/").header("cookie", "a=1").await;
let body = res.text().await;
assert_eq!(body, "Header of type `user-agent` was missing");
}
}
+1
View File
@@ -0,0 +1 @@
<h1>Hello, World!</h1>
@@ -0,0 +1 @@
<strong>Hello, World!</strong>
+1
View File
@@ -0,0 +1 @@
console.log('hi')