Compare commits

...

174 Commits

Author SHA1 Message Date
Canmi
0dc7a055a9
fix: empty_enum renamed to empty_enums (#3642) 2026-02-04 07:34:35 +01:00
Jonas Platte
f8e0e6d025
Update Cargo.lock and examples/Cargo.lock (#3643) 2026-02-03 23:38:36 +01:00
Joey de Waal
7961711fc7
Accept owned locations in Redirect constructors (#3635) 2026-01-30 11:34:46 +00:00
Antonio Souza
b1c5ed99c5
Fix InternalServerError docs to match current behavior (#3640)
Signed-off-by: Antonio Souza <arfs.antonio@gmail.com>
2026-01-27 09:50:35 +00:00
Canmi
5dc504035b
Add sigterm crate to ecosystem.md (#3638) 2026-01-26 05:43:54 -05:00
Dominique Martinet
2df78b7072
axum-extra: Remove unused extract::rejection module (#3636) 2026-01-22 08:40:01 +01:00
Jonas Platte
576968b406
Fix IntoResponse for tuples overriding error response codes (#3603)
Co-authored-by: David Pedersen <david.pdrsn@gmail.com>
Co-authored-by: Yann Simon <yann.simon@commercetools.com>
2026-01-18 20:20:18 +01:00
tottoto
81e727faf6
ci: Remove test-docs job (#3634) 2026-01-14 00:19:21 +09:00
tottoto
3034f2cb16
chore: Remove unused package metadata (#3630) 2026-01-14 00:18:47 +09:00
tottoto
a34d06b3e9
axum: Update to serde_json 1.0.29 (#3632) 2026-01-13 00:36:03 +09:00
tottoto
c3fc7bb1df
ci: Remove unnecessary package handling (#3628) 2026-01-12 08:20:13 +09:00
tottoto
f829daf094
ci: Improve test-msrv job cache (#3627) 2026-01-11 19:36:33 +09:00
Jonas Platte
4c09ea7d80
Rewrite sec-websocket-protocol handling (#3620) 2026-01-10 12:06:45 +00:00
tottoto
309dc56a73
ci: Make examples checked with cargo-sort (#3624) 2026-01-10 17:27:04 +09:00
Jonas Platte
204c5adf5f
Upgrade serde_html_form to 0.4.0 (#3619) 2026-01-08 01:04:59 +01:00
Lethe Lee
02f1dd1731
Add customizable WebSocket subprotocol selection (#3597) 2026-01-07 17:42:55 +00:00
tottoto
023d8b7e8e
ci: Refactor ci style (#3613) 2026-01-07 18:43:44 +09:00
Jonas Platte
05de165de2
Stop calling axum a 'framework' (#3616) 2026-01-06 11:04:40 +01:00
Consoli
4077966491
docs: Document that vpath and json macros are feature-gated (#3615) 2026-01-05 17:45:45 +01:00
Jonas Platte
183ace306a
Add method filtering to route_with_tsr (#3586) 2026-01-04 09:46:33 +01:00
tottoto
051628c163
ci: Make examples cached and checked with rustfmt (#3609) 2026-01-03 14:24:17 +01:00
tottoto
4028d0e7cf
ci: Update to cargo-check-external-types 0.4.0 (#3608) 2025-12-30 07:03:23 +09:00
tottoto
057cf4a3bf
examples: Reuse reqwest client in oauth example (#3607) 2025-12-30 07:02:41 +09:00
Jonas Platte
0101c2a240
Use Result for validate_nest_path (#3606) 2025-12-28 19:30:54 +01:00
Jonas Platte
f3a95d786a
Fix return types of serve futures (#3601) 2025-12-28 09:25:50 +01:00
tottoto
370c6df40a
examples: Update to axum-server 0.8 (#3605) 2025-12-28 08:39:32 +09:00
Jonas Platte
f72bb26ff3
ci: Require up-to-date workspace in more places (#3604) 2025-12-27 20:38:51 +00:00
Jonas Platte
3b9a4193ea
Add recommended settings for VSCode (#3602) 2025-12-27 19:47:00 +01:00
Jonas Platte
e3b32f48b5
Remove deprecated extractors from axum-extra (#3599) 2025-12-27 15:43:01 +00:00
Jonas Platte
b1cd1c17cb
Merge branch 'v0.8.x' 2025-12-27 15:34:46 +01:00
Jonas Platte
d9f79f5616
Release axum-extra v0.12.5 2025-12-27 15:33:05 +01:00
Jan
6b0089190c
fix(json-lines): Respect default body limit (#3591) 2025-12-27 15:31:50 +01:00
Jonas Platte
cc9f151f3c
Merge branch 'v0.8.x' 2025-12-27 10:48:19 +01:00
Jonas Platte
4e2bc8c92a
Release axum-extra v0.12.4 2025-12-27 10:35:59 +01:00
Jonas Platte
f72c298ee8
Improve error messages with #[diagnostic::do_not_recommend] (#3588) 2025-12-27 10:22:51 +01:00
Jonas Platte
e710a97a5a
Release axum-core v0.5.6 2025-12-26 14:55:07 +01:00
Jonas Platte
aba8046921
Deprecate Host and Scheme extractors 2025-12-26 14:48:47 +01:00
Jonas Platte
adf2e6c6bf
Remove CI job using ancient nightly
It has been updated on main, backporting is too much effort.
2025-12-26 14:40:56 +01:00
Jonas Platte
8eaf49e317
Remove cargo-sort CI job
It isn't very important and fixing it here will result in more merge conflicts.
2025-12-26 14:39:07 +01:00
Jonas Platte
5155b9bed7
Remove cargo-public-api-crates CI job
It's broken and has been replaced on main.
2025-12-26 14:38:28 +01:00
Jonas Platte
b6ffaee099
Exclude broken example from workspace 2025-12-26 14:37:53 +01:00
Jonas Platte
cae6bc3709
axum: Use serde_html_form for Query and Form (#3594) 2025-12-26 13:03:42 +01:00
tottoto
061666a111
examples: Update to askama 0.15 (#3593) 2025-12-23 08:11:48 +09:00
tottoto
82af1277a4
examples: Update to metrics-exporter-prometheus 0.18 (#3590) 2025-12-23 06:06:15 +09:00
Jan
e1ed2f189c
fix(json-lines): Respect default body limit (#3591) 2025-12-22 20:24:59 +01:00
Jonas Platte
fd57d871aa
Improve error messages with #[diagnostic::do_not_recommend] (#3588) 2025-12-20 18:40:34 +01:00
Jonas Platte
e2d49c5eee
Merge branch 'v0.8.x' 2025-12-20 14:15:01 +01:00
Jonas Platte
d07863f97d
Release axum v0.8.8 and axum-extra v0.12.3 2025-12-20 14:14:20 +01:00
tottoto
287c674b65
axum-extra: Make typed-routing feature enable routing feature (#3514) 2025-12-20 14:07:09 +01:00
Brad Dunbar
f5804aa6a1
SecondElementIs: Correct a small inconsistency (#3559) 2025-12-20 14:04:13 +01:00
Asger Hautop Drewsen
f51f3ba436
axum-extra: Add trailing newline to pretty JSON response (#3526) 2025-12-20 14:03:58 +01:00
Alice Ryhl
816407a816
Fix integer underflow in try_range_response for empty files (#3566) 2025-12-20 14:03:46 +01:00
Mohamed Macow
78656ebb4a
docs: Clarify route_layer does not apply middleware to the fallback handler (#3567) 2025-12-20 14:03:26 +01:00
Joshua Mo
4b28e4421d
chore: Remove shuttle from docs (#3585) 2025-12-19 10:38:39 -05:00
tottoto
9795e3be51
chore: Update headers to 0.4.1 in lock file (#3579) 2025-12-07 12:46:23 +09:00
tottoto
2059c12868
examples: Update to redis 1 (#3528) 2025-12-06 23:49:47 +09:00
tottoto
642e4dcb3c
ci: Switch cargo-public-api-crates to cargo-check-external-types (#3576) 2025-12-05 22:03:30 +09:00
Yann Simon
ca24460fac
move imports under the feature (#3578) 2025-12-04 13:47:30 +01:00
Brad Dunbar
5c4c1658a7
SecondElementIs: Correct a small inconsistency (#3559) 2025-12-03 00:24:27 +01:00
David Mládek
b9e35ec780
Split examples & check minimum versions (#3370)
Currently, both cargo deny and our MSRV checks use Cargo.lock file which has unified features and versions across both the axum crates and all the examples. This can hide some issues as usually when someone adds an example, they might use cargo add which will silently update the dependency for the whole repository. Some compilation errors (like axum requiring bytes@1.0 while it uses features from bytes@1.7) will then be hidden.

I don't think most users would ever need to use the minimal versions anyway so this is not as severe, but someone might run into compilation errors.
2025-12-02 22:24:37 +01:00
tottoto
aeff16e91a
ci: Update to actions/checkout v6 (#3572) 2025-11-26 06:25:55 +09:00
Asger Hautop Drewsen
e668598cef
axum-extra: Add trailing newline to pretty JSON response (#3526) 2025-11-24 21:34:46 +01:00
Mohamed Macow
9bd839e5e9
refac(axum-extra): improve test invariants for protobuf.rs extractor (#3569) 2025-11-23 20:56:22 +01:00
Alice Ryhl
601d775da8
Fix integer underflow in try_range_response for empty files (#3566) 2025-11-22 20:09:42 +01:00
Mohamed Macow
7fd17ceba5
docs: Clarify route_layer does not apply middleware to the fallback handler (#3567) 2025-11-21 23:17:24 +01:00
Ivan Tham
26367b9f1e
axum: use Vec for PathRouter (#3509) 2025-11-21 17:09:18 +01:00
Niclas Klugmann
509016003e
add axum-conditional-requests to ECOSYSTEM.md (#3496) 2025-11-21 15:07:25 +01:00
Jonas Platte
b1ef45469b
Merge branch 'v0.8.x' into jplatte/v0.8.7 2025-11-14 20:46:08 +01:00
Jonas Platte
68320009fb
Merge branch 'v0.8.x' into jplatte/extra-v0.12.1 2025-11-14 20:02:27 +01:00
Jonas Platte
c10934c79c
Relax implicit Send / Sync bounds (#3555) 2025-11-14 10:48:07 +01:00
Andrii Mishkovskyi
830a114172
Fix typo in extractors guide (#3554) 2025-11-13 09:47:58 +01:00
Lyra Naeseth
3ef6c5d183
Make it easier to visually scan for default features (#3550) 2025-11-10 10:41:04 +01:00
Yann Simon
8954d7922a
Update and fix mongo example (#3549) 2025-11-04 11:57:40 +01:00
Brad Dunbar
efc1f57b15
Use extensions directly in from_request_parts (#3542) 2025-10-30 08:40:00 -04:00
Italo Maia
c4890b844b
Fixes wording typo (#3540) 2025-10-26 23:35:01 +01:00
Lewin Probst, M.Sc.
cec605c8eb
Added axum-gate to ECOSYSTEM.md (#3538) 2025-10-25 17:46:37 +02:00
tottoto
7bc6f52892
ci: Use taiki-e/install-action to setup cargo-hack (#3535) 2025-10-23 09:18:25 +02:00
Paul Mattern
37a08a17d4
Fix misplaced comment in error handling example (#3529) 2025-10-11 22:16:47 +02:00
Paul Mattern
8389ab10b0
examples: refactor error logging in error-handling example (#3527) 2025-10-11 17:41:12 +02:00
tottoto
dcbcf5c5fd
axum-extra: Make typed-routing feature enable routing feature (#3514) 2025-10-11 11:22:51 +02:00
xumaple
76cb48ebe0
Upgrade axum-extra to prost v0.14 (#3517)
Co-authored-by: Maple Xu <mapxu@microsoft.com>
2025-10-09 07:03:51 +02:00
tottoto
4160bbe6de
examples: Update to thiserror 2 (#3522) 2025-10-08 23:19:41 +09:00
tottoto
f9c0e7069a
examples: Update to jsonwebtoken 10 (#3520) 2025-10-07 13:19:27 -04:00
tottoto
29be91240d
examples: Use diesel HasQuery trait (#3519) 2025-10-07 00:13:36 +09:00
tottoto
a847c60937
examples: Update to diesel-async 0.7 (#3518) 2025-10-06 23:58:01 +09:00
tottoto
ac4ba7b63a
Fix rolled back cargo-deny config (#3516) 2025-10-06 21:43:17 +09:00
Pavel Borzenkov
86e4442888
axum: Bump 'bytes' dependency to 1.7 (#3513) 2025-10-01 11:39:23 +02:00
David Mládek
d8cd5cec4b Merge remote-tracking branch 'origin/v0.8.x' into mladedav/v0.8.6 2025-09-30 14:29:20 +02:00
David Mládek
6cea3eadee
Remove usage of the doc_auto_cfg feature (#3505) 2025-09-30 11:53:30 +02:00
Jonas Platte
b5fa1428e9
Merge branch 'v0.8.x' into jplatte/axum-core-v0.5.4 2025-09-28 22:13:27 +02:00
Jonas Platte
858146d1f1
Remove unused rustversion dependency of axum-core (#3502) 2025-09-28 21:46:54 +02:00
Jonas Platte
f791ec3879
Merge branch 'v0.8.x' into jplatte/v0.8.5 2025-09-28 20:46:41 +02:00
Jonas Platte
72e820f6fa
Bump nightly for axum-macros (#3500) 2025-09-28 20:31:17 +02:00
Sheroz
8d055800d2
Add axum-rest-api-sample to ECOSYSTEM.md (#3494) 2025-09-26 23:43:52 +02:00
tottoto
bf9b43cabe
Update to tokio-tungstenite 0.28 (#3497) 2025-09-25 22:09:35 +09:00
Søren Løvborg
9ed1ad69d2
axum: add ListenerExt::limit_connections (#3489) 2025-09-23 23:25:36 +02:00
tottoto
50f0082970
axum: Make futures-sink optional dependency (#3491) 2025-09-22 23:58:30 +09:00
David Mládek
cc11b5ad7d
axum-extra: gate rejection test behind feature (#3493) 2025-09-22 14:18:46 +02:00
tottoto
817aae3d1e
axum-extra: Add link definition for pull request to changelog (#3492) 2025-09-22 09:30:03 +02:00
tottoto
4d8a6f6f0f
axum-extra: Make rustversion and serde_core optional dependency (#3487) 2025-09-21 06:32:56 +09:00
tottoto
f09f50c50a
axum-extra: Remove unused tower dependency (#3486) 2025-09-20 20:57:04 +00:00
tottoto
d184798937
axum-extra: Make axum optional dependency (#3485) 2025-09-20 22:50:56 +02:00
Mattia Penati
40e9d7b22a
Add tower-otel to ECOSYSTEM.md (#3482) 2025-09-17 11:52:34 +02:00
Fredrik Park
600b16d431
Spelling misstake in Using closure capture (#3481) 2025-09-16 22:33:39 +02:00
Loic Hausammann
e43030d051
websocket: add a wrapper around is_terminated (#3443)
Co-authored-by: Loic <loic@daedalean.ai>
Co-authored-by: loikki <851651-loikki@users.noreply.gitlab.com>
2025-09-16 15:54:44 +00:00
othelot
ab593bbab7
routing: omit the Allow header for non-405 method not allowed fallbacks (#3465) 2025-09-16 08:29:01 +00:00
Søren Løvborg
1073468163
axum::serve: Enable Hyper request header timeout (#3478)
It's possible for an HTTP request to get stuck (due to network issues or
malicious clients) halfway through the request line or header, in which
case no amount of timeouts configured by an application using axum/tower
will help, since Hyper hasn't yet handed off the request to the service.

This has two consequences:

- Wasted memory: Up to ~1 MB per connection in the worst case, compared
  to the ~2.5 kB used by a regular idle connection.

- A `with_graceful_shutdown` signal will cause the server to stop
  accepting new requests, but then hang instead of actually stopping.

This changes axum::serve to configure a Timer for HTTP/1 connections,
which activates Hyper's default timeout (currently 30 seconds).

The timeout applies only to the request line and request header, not the
request body or subsequent response (where axum applications can instead
simply configure a timeout using `tower_http::timeout::TimeoutLayer`).

The timeout does not currently apply to newly opened connections, even
though Hyper's `header_read_timeout` SHOULD apply here too since 1.4.0;
discussion on tokio-rs#2741 suggests a possible TokioIo issue. However, the
graceful shutdown still works as expected for such connections.

The timer is only enabled for HTTP/1 here since similar functionality
does not currently appear to exist for HTTP/2 connections in Hyper.

This is a breaking change for any axum::serve users who currently rely
on being able to begin sending a HTTP/1 request, and then take more than
30 seconds to finish the request line and headers, or rely on keeping
connections idle for 30+ seconds between requests.
2025-09-16 10:19:20 +02:00
David Mládek
5c090dcb3e axum-extra: make option_layer guarantee that the output body is axum::body::Body 2025-09-14 19:50:04 +02:00
David Mládek
853d7e707a axum: add ResponseAxumBodyLayer for mapping response body to axum::body::Body 2025-09-14 19:50:04 +02:00
Jonas Platte
4ab8df5e42
Switch serde dependency to serde_core (#3477) 2025-09-14 08:49:07 +02:00
K-tecchan
94af9c3262
axum: correct typo in append_allow_header (#3476) 2025-09-13 14:27:43 +00:00
tottoto
d5e505619f
Update to cargo-deny api version 2 (#3475) 2025-09-13 14:48:25 +02:00
tottoto
e8ab6029d1
examples: Update to askama 0.14 (#3474) 2025-09-13 13:19:19 +02:00
tottoto
8d0497f793
examples: Update to brotli 8 (#3473) 2025-09-13 10:29:21 +02:00
tottoto
1b844c9fc6
examples: Update to metrics 0.24 (#3472) 2025-09-13 10:06:19 +02:00
tottoto
83d19e7b4f
examples: Update to redis 0.32 (#3471) 2025-09-13 09:06:00 +02:00
tottoto
038f0966a8
examples: Update to diesel-async 0.6 (#3470) 2025-09-12 17:03:33 +02:00
tottoto
14f15252ee
Remove resolved cargo-deny skip-tree config of tracing-subscriber (#3468) 2025-09-12 13:23:09 +00:00
tottoto
490cde6048
examples: Update to axum-server 0.7 (#3467) 2025-09-12 15:18:33 +02:00
tottoto
a0748e5a74
examples: Update to tower-http 0.6 (#3466) 2025-09-12 13:44:24 +02:00
Niclas Klugmann
25acc8d131
Clarify that AddExtension is not the actual Layer (#3463) 2025-09-12 09:21:19 +02:00
tottoto
e4550d23b1
examples: Update to oauth2 5 (#3462) 2025-09-11 15:34:41 +02:00
Joel Uckelman
5d5ba01be9
Add axum_extra::extract::Query::try_from_uri (#3460)
`axum::extract::Query` has a try_from_uri, which is useful for testing. This adds the same function to `axum_extra::extract::Query`.
2025-09-08 10:18:31 +02:00
Kenny Lau
64ae48347e
Fix typo in file_stream error message (#3459) 2025-09-07 12:49:22 +02:00
Canmi
3fc8e4ae0a
Add axum-governor to community maintained axum ecosystem (#3456) 2025-09-05 10:10:56 +02:00
tottoto
8f707ca9fd
ci: Refactor dependencies-are-sorted job (#3445) 2025-09-05 00:13:45 +02:00
Antoine Vandecreme
86868de80e
Reject JSON bodies with trailing chars (#3453) 2025-09-02 19:34:17 +00:00
Antoine Vandecreme
49c8facad3
Bump tracing-subscriber (#3454) 2025-09-02 13:01:31 -04:00
baerwang
ee1519c82f
Add openapi-rs to ECOSYSTEM.md (#3414) 2025-08-30 13:29:14 +00:00
tottoto
94db5273a5
ci: Update to actions/checkout v5 (#3450) 2025-08-28 15:41:09 +02:00
Poliorcetics
9ec85d6970
fix(axum-extra): don't require S generic param when using FileStream::from_path() (#3437) 2025-08-17 07:14:07 +02:00
Patrik Lundin
4b0590b30e
examples/prometheus-metrics: run upkeep task (#3430)
Co-authored-by: David Mládek <david.mladek.cz@gmail.com>
2025-08-14 12:42:01 +00:00
Lukas Bergdoll
a57935c039
Generalize Connected implementation to all Listeners for SocketAddr (#3331)
Co-authored-by: David Mládek <david.mladek.cz@gmail.com>
2025-08-14 13:01:04 +02:00
reivilibre
76268ae52b
Document type parameter T on Handler (#3435)
Signed-off-by: Olivier 'reivilibre <git.contact@librepush.net>
2025-08-14 11:10:52 +02:00
Glen De Cauwsemaecker
dfc15bce9d
Support custom (binary) data to be written into SSE Event (#3425) 2025-08-13 23:32:08 +02:00
Glen De Cauwsemaecker
d66cabd5e9
Fix ci failures (#3432) 2025-08-13 10:53:53 +02:00
Theodore Bjernhed
7f5cea0e6b
Update and centralize the LICENSE file to the repo root (#3401) 2025-08-12 23:31:12 +02:00
Jan Runge
2977bd4adf
Extractors doc: correct linked example section (#3349) 2025-08-01 21:47:55 +00:00
Jonas Platte
ff031867df
Update minimum Rust version to 1.78 (#3412) 2025-07-20 07:47:56 +00:00
Yann Simon
3bd445d9ee
remove unused dep in tls examples (#3411) 2025-07-17 12:51:12 +00:00
chenlunfu
d9609d754f
Add qiluo-admin to ECOSYSTEM.md (#3409) 2025-07-16 13:37:44 -04:00
Kyle Nweeia
0832de443c
Use via extractor's rejection instead of axum::response::Response (#3261) 2025-07-16 09:50:59 +00:00
Theodore Bjernhed
6bc0717b06
Improve value and reference passing efficiency (#3406) 2025-07-11 05:38:45 +02:00
Glen De Cauwsemaecker
420d7b6aaa
Update ECOSYSTEM.md: Add datastar with axum support (#3383) 2025-07-09 19:16:12 +00:00
Theodore Bjernhed
1b157a461e
Enable and fix control flow related clippy lints 2025-07-09 18:48:28 +00:00
Glen De Cauwsemaecker
617594cd4a
sse: Add space between duration and : (#3403) 2025-07-09 12:20:05 +02:00
Theodore Bjernhed
fb64e72de9
Add #[must_use] to types and methods (#3395)
Co-authored-by: Theodore Bjernhed <fosseder@danwin1210.de>
2025-07-05 19:07:24 +02:00
tottoto
d7104313cb
Update tokio-tungstenite to 0.27 (#3398) 2025-07-05 16:47:42 +02:00
Theodore Bjernhed
0f255c3c4c
style: use Self to avoid unnecessary repetition (#3396) 2025-07-04 21:10:01 +00:00
Theodore Bjernhed
b8d9a3e764
style: Reorder macro-generated items to fix lints (#3392) 2025-07-01 23:01:54 +02:00
zeon
7dc70bf0d3
New redirect inspection (#3377) 2025-06-27 19:04:46 +00:00
lee suk-jae
6d78e84398
Add Axum Clean Demo to project showcase (#3371) 2025-06-08 11:35:53 +02:00
David Mládek
fc14f5c728
axum: add router nesting fallback changelog (#3339) 2025-06-03 21:16:36 +00:00
Sabrina Jewson
8202c66a4d
Add DefaultBodyLimit::apply (#3368) 2025-06-03 14:21:22 +02:00
fritzchr
7eabf7e645
docs: Update changelog (#3364) 2025-05-28 18:59:34 +02:00
Jake McGinty
9ef53e5a48
Implement OptionalFromRequest for Multipart (#3220) 2025-05-28 16:40:03 +02:00
Daniel
769e4066b1
Implement the OptionalFromRequestParts trait for the Host extractor (#3177) 2025-05-25 21:13:01 +02:00
Paolo Barbolini
869ba86e51
Reduce dependency on futures-util (#3358) 2025-05-25 09:39:35 +02:00
Jonas Platte
d0aff24c85
Reorder some TOML sections (#3360) 2025-05-25 08:46:05 +02:00
Rik Huijzer
dd8d4a47cb
Add fx to ECOSYSTEM.md (#3351) 2025-05-13 09:51:05 +00:00
Jonas Platte
d72a0f962a
Make clippy happy (#3350) 2025-05-13 09:58:45 +02:00
Steven Black
07f29f6c5c
Add "Run with" comment to example-low-level-openssl (#3347) 2025-05-09 08:23:08 +02:00
Yann Simon
7d1dbeb3af
Update validator to 0.20.0 (#3345) 2025-05-06 15:44:03 +02:00
daalfox
df55d83f29
Further refactor json_content_type (#3340) 2025-05-06 13:51:10 +02:00
dependabot[bot]
36f2630e4d
Bump hickory-proto from 0.24.2 to 0.24.4 (#3344)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 13:34:54 +02:00
dependabot[bot]
14e50c0dcb
Bump ring from 0.17.8 to 0.17.14 (#3342)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 13:28:22 +02:00
dependabot[bot]
f00b14bac6
Bump openssl from 0.10.68 to 0.10.72 (#3341)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 11:42:51 +02:00
David Mládek
0821a9dc09
axum: Allow body types other than axum::body::Body in serve (#3205) 2025-05-02 08:54:39 +02:00
Jonas Platte
cd1453f084
Fix compilation (#3338) 2025-05-01 19:06:43 +02:00
Nano
6ad76dd9a4
Make SSE less dependent on tokio (#3154) 2025-05-01 10:54:29 +02:00
David Mládek
bf7c5fc5f3 axum/routing: Merge fallbacks with the rest of the router 2025-05-01 10:53:27 +02:00
David Mládek
64563fb94c Update axum-extra/CHANGELOG.md
Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
2025-05-01 10:35:11 +02:00
David Mládek
a9f3172e88 axum-extra: Remove unused feature 2025-05-01 10:35:11 +02:00
Jonas Platte
ef3a7ccf1c Bring back the breaking changes warning, once again 2025-05-01 10:34:40 +02:00
196 changed files with 8812 additions and 6757 deletions

View File

@ -2,7 +2,7 @@ name: CI
env:
CARGO_TERM_COLOR: always
MSRV: '1.78'
MSRV: '1.80'
on:
push:
@ -14,33 +14,37 @@ on:
jobs:
check:
runs-on: ubuntu-24.04
strategy:
matrix:
workspace: [".", examples]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@beta
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: rustfmt
run: cargo fmt --all --check
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@beta
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ matrix.workspace }}
- name: Check
run: cargo clippy --locked --manifest-path ${{ matrix.workspace }}/Cargo.toml --workspace --all-targets --all-features -- -D warnings
- name: rustfmt
run: cargo fmt --all --check --manifest-path ${{ matrix.workspace }}/Cargo.toml
check-docs:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo doc -p axum-core
run: cargo doc --package axum-core --all-features --no-deps
- name: cargo doc -p axum
run: cargo doc --package axum --all-features --no-deps
- name: cargo doc -p axum-extra
run: cargo doc --package axum-extra --all-features --no-deps
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: cargo doc -p axum-core
run: cargo doc --package axum-core --all-features --no-deps
- name: cargo doc -p axum
run: cargo doc --package axum --all-features --no-deps
- name: cargo doc -p axum-extra
run: cargo doc --package axum-extra --all-features --no-deps
env:
RUSTDOCFLAGS: "-D rustdoc::all -A rustdoc::private-doc-tests"
@ -50,33 +54,31 @@ jobs:
# Fail the build if there are any warnings
RUSTFLAGS: "-D warnings"
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-hack
run: |
curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin
- name: cargo hack check
run: cargo hack check --each-feature --no-dev-deps --all
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: taiki-e/install-action@cargo-hack
- name: cargo hack check
run: cargo hack check --each-feature --no-dev-deps --all
cargo-public-api-crates:
external-types:
runs-on: ubuntu-24.04
strategy:
matrix:
crate: [axum, axum-core, axum-extra, axum-macros]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-public-api-crates
run: |
cargo install --git https://github.com/jplatte/cargo-public-api-crates
- name: cargo public-api-crates check
run: cargo public-api-crates --manifest-path ${{ matrix.crate }}/Cargo.toml check
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2025-10-18
- name: Install cargo-check-external-types
uses: taiki-e/cache-cargo-install-action@v2
with:
tool: cargo-check-external-types@0.4.0
- uses: taiki-e/install-action@cargo-hack
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- run: cargo hack --no-private --exclude axum-macros check-external-types --all-features
test-versions:
needs: check
@ -84,35 +86,37 @@ jobs:
strategy:
matrix:
rust: [stable, beta]
workspace: [".", examples]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests
run: cargo test --workspace --all-features --all-targets
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: ${{ matrix.workspace }}
- name: Run tests
run: cargo test --locked --workspace --all-features --all-targets --manifest-path ${{ matrix.workspace }}/Cargo.toml
# some examples don't support our MSRV so we only test axum itself on our MSRV
test-nightly:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Get rust-toolchain version
id: rust-toolchain
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.rust-toolchain.outputs.version }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run nightly tests
working-directory: axum-macros
run: cargo test
- uses: actions/checkout@v6
- name: Get rust-toolchain version
id: rust-toolchain
run: echo "version=$(cat axum-macros/rust-toolchain)" >> $GITHUB_OUTPUT
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ steps.rust-toolchain.outputs.version }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run nightly tests
working-directory: axum-macros
run: cargo test
# some examples don't support our MSRV (such as async-graphql)
# so we only test axum itself on our MSRV
@ -120,51 +124,25 @@ jobs:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.MSRV }}
- name: "install Rust nightly"
uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Select minimal version
run: cargo +nightly update -Z minimal-versions
- name: Fix up Cargo.lock
run: cargo +nightly update -p crc32fast --precise 1.1.1
- name: Run tests
run: >
cargo +${{ env.MSRV }}
test
-p axum
-p axum-extra
-p axum-core
--all-features
--locked
# the compiler errors are different on our MSRV which makes
# the trybuild tests in axum-macros fail, so just run the doc
# tests
- name: Run axum-macros doc tests
run: >
cargo +${{ env.MSRV }}
test
-p axum-macros
--doc
--all-features
--locked
test-docs:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run doc tests
run: cargo test --all-features --doc
- uses: actions/checkout@v6
- name: "install Rust nightly"
uses: dtolnay/rust-toolchain@nightly
- name: Select minimal version
run: cargo +nightly update -Z minimal-versions
- name: Fix up Cargo.lock - crc32fast
run: cargo +nightly update -p crc32fast --precise 1.1.1
- name: Fix up Cargo.lock - version_check
run : cargo +nightly update -p version_check@0.1.0 --precise 0.1.4
- name: Fix up Cargo.lock - lazy_static
run: cargo +nightly update -p lazy_static --precise 1.1.0
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.MSRV }}
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests
run: cargo +${{ env.MSRV }} test --locked --all-features
deny-check:
name: cargo-deny check
@ -176,7 +154,7 @@ jobs:
- advisories
- bans licenses sources
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check ${{ matrix.checks }}
@ -186,71 +164,56 @@ jobs:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
target: armv5te-unknown-linux-musleabi
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check
env:
# Clang has native cross-compilation support
CC: clang
run: >
cargo
check
--all-targets
--all-features
-p axum
-p axum-core
-p axum-extra
-p axum-macros
--target armv5te-unknown-linux-musleabi
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
target: armv5te-unknown-linux-musleabi
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check
env:
# Clang has native cross-compilation support
CC: clang
run: cargo check --all-targets --all-features --target armv5te-unknown-linux-musleabi
wasm32-unknown-unknown:
needs: check
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check
run: >
cargo
check
--manifest-path ./examples/simple-router-wasm/Cargo.toml
--target wasm32-unknown-unknown
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
target: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
workspaces: examples
- name: Check
run: >
cargo
check
--manifest-path ./examples/simple-router-wasm/Cargo.toml
--target wasm32-unknown-unknown
dependencies-are-sorted:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@beta
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-sort
run: |
cargo install cargo-sort
# Work around cargo-sort not honoring workspace.exclude
- name: Remove non-crate folder
run: rm -rf examples/async-graphql
- name: Check dependency tables
run: |
cargo sort --workspace --grouped --check
- uses: actions/checkout@v6
- name: Install cargo-sort
uses: taiki-e/install-action@v2
with:
tool: cargo-sort@2.0.2
- name: Check dependency tables
run: cargo-sort --workspace --grouped --check
- name: Check examples dependency tables
run: cargo-sort --workspace --grouped --check
working-directory: examples
typos:
name: Spell Check with Typos
runs-on: ubuntu-24.04
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Check the spelling of the files in our repo
uses: crate-ci/typos@v1.29.4

4974
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,9 @@
[workspace]
members = ["axum", "axum-*", "examples/*"]
# Only check / build main crates by default (check all with `--workspace`)
default-members = ["axum", "axum-*"]
# Example has been deleted, but README.md remains
exclude = ["examples/async-graphql"]
members = ["axum", "axum-*"]
resolver = "2"
[workspace.package]
rust-version = "1.78"
rust-version = "1.80"
[workspace.lints.rust]
unsafe_code = "forbid"
@ -22,31 +18,42 @@ type_complexity = "allow"
await_holding_lock = "warn"
dbg_macro = "warn"
empty_enum = "warn"
empty_enums = "warn"
enum_glob_use = "warn"
equatable_if_let = "warn"
exit = "warn"
filter_map_next = "warn"
fn_params_excessive_bools = "warn"
if_let_mutex = "warn"
implicit_clone = "warn"
imprecise_flops = "warn"
inefficient_to_string = "warn"
linkedlist = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_let_else = "warn"
match_same_arms = "warn"
match_wildcard_for_single_variants = "warn"
mem_forget = "warn"
must_use_candidate = "warn"
needless_borrow = "warn"
needless_continue = "warn"
needless_pass_by_ref_mut = "warn"
needless_pass_by_value = "warn"
option_option = "warn"
redundant_clone = "warn"
ref_option = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_self_not_must_use = "warn"
single_match_else = "warn"
str_to_string = "warn"
suboptimal_flops = "warn"
todo = "warn"
trivially_copy_pass_by_ref = "warn"
uninlined_format_args = "warn"
unnested_or_patterns = "warn"
unused_self = "warn"
use_self = "warn"
verbose_file_reads = "warn"
# configuration for https://github.com/crate-ci/typos

View File

@ -24,13 +24,15 @@ If your project isn't listed here and you would like it to be, please feel free
- [axum-template](https://github.com/Altair-Bueno/axum-template): Layers, extractors and template engine wrappers for axum based Web MVC applications
- [axum-template](https://github.com/janos-r/axum-template): GraphQL and REST API, SurrealDb, JWT auth, direct error handling, request logs
- [axum-guard-logic](https://github.com/sjud/axum_guard_logic): Use AND/OR logic to extract types and check their values against `Service` inputs.
- [axum-casbin-auth](https://github.com/casbin-rs/axum-casbin-auth): Casbin access control middleware for axum framework
- [axum-casbin-auth](https://github.com/casbin-rs/axum-casbin-auth): Casbin access control middleware for axum
- [aide](https://docs.rs/aide): Code-first Open API documentation generator with [axum integration](https://docs.rs/aide/latest/aide/axum/index.html).
- [axum-typed-routing](https://docs.rs/axum-typed-routing/latest/axum_typed_routing/): Statically typed routing macros with OpenAPI generation using aide.
- [axum-jsonschema](https://docs.rs/axum-jsonschema/): A `Json<T>` extractor that does JSON schema validation of requests.
- [axum-login](https://docs.rs/axum-login): Session-based user authentication for axum.
- [axum-gate](https://docs.rs/axum-gate): JWT-based authentication and role-based authorization for axum (Cookie and Bearer, for monolithic and distributed applications).
- [axum-csrf-sync-pattern](https://crates.io/crates/axum-csrf-sync-pattern): A middleware implementing CSRF STP for AJAX backends and API endpoints.
- [axum-otel-metrics](https://github.com/ttys3/axum-otel-metrics/): A axum OpenTelemetry Metrics middleware with prometheus exporter supported.
- [tower-otel](https://github.com/mattiapenati/tower-otel): OpenTelemetry layer for HTTP/gRPC services with optional axum integration.
- [jwt-authorizer](https://crates.io/crates/jwt-authorizer): JWT authorization layer for axum (oidc discovery, validation options, claims extraction, etc.)
- [axum-typed-multipart](https://crates.io/crates/axum_typed_multipart): Type safe wrapper for `axum::extract::Multipart`.
- [tower-governor](https://crates.io/crates/tower_governor): A Tower service and layer that provides a rate-limiting backend by [governor](https://crates.io/crates/governor)
@ -41,7 +43,6 @@ If your project isn't listed here and you would like it to be, please feel free
- [axum-prometheus](https://github.com/ptrskay3/axum-prometheus): A middleware library to collect HTTP metrics for axum applications, compatible with all [metrics.rs](https://metrics.rs) exporters.
- [axum-valid](https://github.com/gengteng/axum-valid): Extractors for data validation using validator, garde, and validify.
- [tower-sessions](https://github.com/maxcountryman/tower-sessions): Sessions as a `tower` and `axum` middleware.
- [shuttle](https://github.com/shuttle-hq/shuttle): Build & ship backends without writing any infrastructure files. Now with axum support.
- [socketioxide](https://github.com/totodore/socketioxide): An easy to use socket.io server implementation working as a `tower` layer/service.
- [axum-serde](https://github.com/gengteng/axum-serde): Provides multiple serde-based extractors / responses, also offers a macro to easily customize serde-based extractors / responses.
- [loco.rs](https://github.com/loco-rs/loco): A full stack Web and API productivity framework similar to Rails, based on axum.
@ -57,6 +58,10 @@ If your project isn't listed here and you would like it to be, please feel free
- [baxe](https://github.com/zyphelabs/baxe): Simple macro for defining backend errors once and automatically generate standardized JSON error responses, saving time and reducing complexity
- [axum-html-minifier](https://crates.io/crates/axum_html_minifier): This middleware minify the html body content of a axum response.
- [static-serve](https://crates.io/crates/static-serve): A helper macro for compressing and embedding static assets in an axum webserver.
- [datastar](https://crates.io/crates/datastar): Rust implementation of the Datastar SDK specification with Axum support
- [axum-governor](https://crates.io/crates/axum-governor): An independent Axum middleware for rate limiting, powered by [lazy-limit](https://github.com/canmi21/lazy-limit) (not related to tower-governor).
- [axum-conditional-requests](https://crates.io/crates/axum-conditional-requests): A library for handling client-side caching HTTP headers
- [sigterm](https://github.com/canmi21/sigterm): Signal-aware async control and cancellation primitives for Tokio.
## Project showcase
@ -98,6 +103,11 @@ If your project isn't listed here and you would like it to be, please feel free
- [sero](https://github.com/clowzed/sero): Host static sites with custom subdomains as surge.sh does. But with full control and cool new features. (axum, sea-orm, postgresql)
- [Hatsu](https://github.com/importantimport/hatsu): 🩵 Self-hosted & Fully-automated ActivityPub Bridge for Static Sites.
- [Mini RPS](https://github.com/marcodpt/minirps): Mini reverse proxy server, HTTPS, CORS, static file hosting and template engine (minijinja).
- [fx](https://github.com/rikhuijzer/fx): A (micro)blogging server that you can self-host.
- [clean_axum_demo](https://github.com/sukjaelee/clean_axum_demo): A modern, clean-architecture Rust API server template built with Axum and SQLx. It incorporates domain-driven design, repository patterns, JWT authentication, file uploads, Swagger documentation, OpenTelemetry.
- [qiluo-admin](https://github.com/chelunfu/qiluo_admin) | Axum + SeaORM + JWT + Scheduled + Tasks + SnowId + Redis + Memory + VUE3 | DB: MySQL, Postgres, SQLite
- [openapi-rs](https://github.com/baerwang/openapi-rs/tree/main/examples/axum) | This project adds a middleware layer to axum using openapi-rs, enabling automatic request validation and processing based on OpenAPI 3.1 specifications. It helps ensure that the server behavior strictly follows the OpenAPI contract.
- [axum-rest-api-example](https://github.com/sheroz/axum-rest-api-sample): REST API Web service in Rust using axum, JSON Web Tokens (JWT), SQLx, PostgreSQL, Redis, Docker, structured error handling, and end-to-end API tests.
[Realworld]: https://github.com/gothinkster/realworld
[SQLx]: https://github.com/launchbadge/sqlx
@ -106,14 +116,14 @@ If your project isn't listed here and you would like it to be, please feel free
- [Rust on Nails](https://rust-on-nails.com/): A full stack architecture for Rust web applications
- [axum-tutorial] ([website][axum-tutorial-website]): axum tutorial for beginners
- [demo-rust-axum]: Demo of Rust and axum web framework
- [demo-rust-axum]: Demo of Rust and axum
- [Introduction to axum (talk)]: Talk about axum from the Copenhagen Rust Meetup
- [Getting Started with Axum]: axum tutorial, GET, POST endpoints and serving files
- [Using Rust, Axum, PostgreSQL, and Tokio to build a Blog]
- [Introduction to axum]: YouTube playlist
- [Rust Axum Full Course]: YouTube video
- [Deploying Axum projects with Shuttle]
- [API Development with Rust](https://rust-api.dev/docs/front-matter/preface/): REST APIs based on axum
- [axum-rest-api-postgres-redis-jwt-docker]: Getting started with REST API Web Services in Rust using Axum, PostgreSQL, Redis, and JWT
[axum-tutorial]: https://github.com/programatik29/axum-tutorial
[axum-tutorial-website]: https://programatik29.github.io/axum-tutorial/
@ -123,6 +133,5 @@ If your project isn't listed here and you would like it to be, please feel free
[Using Rust, Axum, PostgreSQL, and Tokio to build a Blog]: https://spacedimp.com/blog/using-rust-axum-postgresql-and-tokio-to-build-a-blog/
[Introduction to axum]: https://www.youtube.com/playlist?list=PLrmY5pVcnuE-_CP7XZ_44HN-mDrLQV4nS
[Rust Axum Full Course]: https://www.youtube.com/watch?v=XZtlD_m59sM
[Deploying Axum projects with Shuttle]: https://docs.shuttle.rs/examples/axum
[axum-rest-api-postgres-redis-jwt-docker]: https://sheroz.com/pages/blog/rust-axum-rest-api-postgres-redis-jwt-docker.html
[Building a SaaS with Rust & Next.js](https://joshmo.bearblog.dev/lets-build-a-saas-with-rust/) A tutorial for combining Next.js with Rust via axum to make a SaaS.

27
LICENSE Normal file
View File

@ -0,0 +1,27 @@
MIT License
Copyright (c) 20192025 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.

View File

@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# 0.5.6
Improve error messages with `#[diagnostic::do_not_recommend]`.
# 0.5.5
Released without changes to fix docs.rs build.

View File

@ -4,12 +4,29 @@ description = "Core types and traits for axum"
edition = "2021"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
keywords = ["http", "web", "routing"]
license = "MIT"
name = "axum-core"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.5.5" # remember to bump the version that axum and axum-extra depend on
version = "0.5.6" # remember to bump the version that axum and axum-extra depend on
[package.metadata.cargo_check_external_types]
allowed_external_types = [
# not 1.0
"futures_core::*",
"tower_layer::*",
# >=1.0
"bytes::*",
"http::*",
"http_body::*",
]
[package.metadata.cargo-machete]
ignored = ["tower-http"] # See __private_docs feature
[package.metadata.docs.rs]
all-features = true
[features]
tracing = ["dep:tracing"]
@ -38,26 +55,9 @@ axum = { path = "../axum", features = ["__private"] }
axum-extra = { path = "../axum-extra", features = ["typed-header"] }
axum-macros = { path = "../axum-macros", features = ["__private"] }
hyper = "1.0.0"
serde = { version = "1.0.200", features = ["derive"] }
tokio = { version = "1.25.0", features = ["macros"] }
tower-http = { version = "0.6.0", features = ["limit"] }
[lints]
workspace = true
[package.metadata.cargo-public-api-crates]
allowed = [
# not 1.0
"futures_core",
"tower_layer",
# >=1.0
"bytes",
"http",
"http_body",
]
[package.metadata.cargo-machete]
ignored = ["tower-http"] # See __private_docs feature
[package.metadata.docs.rs]
all-features = true

View File

@ -1,7 +0,0 @@
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.

1
axum-core/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

View File

@ -35,6 +35,9 @@ pub trait OptionalFromRequest<S, M = private::ViaRequest>: Sized {
) -> impl Future<Output = Result<Option<Self>, Self::Rejection>> + Send;
}
// Compiler hint just says that there is an impl for Option<T>, not mentioning
// the bounds, which is not very helpful.
#[diagnostic::do_not_recommend]
impl<S, T> FromRequestParts<S> for Option<T>
where
T: OptionalFromRequestParts<S>,
@ -42,6 +45,7 @@ where
{
type Rejection = T::Rejection;
#[allow(clippy::use_self)]
fn from_request_parts(
parts: &mut Parts,
state: &S,
@ -50,6 +54,7 @@ where
}
}
#[diagnostic::do_not_recommend]
impl<S, T> FromRequest<S> for Option<T>
where
T: OptionalFromRequest<S>,
@ -57,6 +62,7 @@ where
{
type Rejection = T::Rejection;
#[allow(clippy::use_self)]
async fn from_request(req: Request, state: &S) -> Result<Option<T>, Self::Rejection> {
T::from_request(req, state).await
}

View File

@ -65,6 +65,7 @@ where
}
}
#[diagnostic::do_not_recommend] // pretty niche impl
impl<S> FromRequest<S> for BytesMut
where
S: Send + Sync,
@ -73,6 +74,7 @@ where
async fn from_request(req: Request, _: &S) -> Result<Self, Self::Rejection> {
let mut body = req.into_limited_body();
#[allow(clippy::use_self)]
let mut bytes = BytesMut::new();
body_to_bytes_mut(&mut body, &mut bytes).await?;
Ok(bytes)
@ -128,12 +130,14 @@ where
}
})?;
#[allow(clippy::use_self)]
let string = String::from_utf8(bytes.into()).map_err(InvalidUtf8::from_err)?;
Ok(string)
}
}
#[diagnostic::do_not_recommend] // pretty niche impl
impl<S> FromRequestParts<S> for Parts
where
S: Send + Sync,
@ -145,6 +149,7 @@ where
}
}
#[diagnostic::do_not_recommend] // pretty niche impl
impl<S> FromRequestParts<S> for Extensions
where
S: Send + Sync,

View File

@ -3,6 +3,7 @@ use crate::response::{IntoResponse, Response};
use http::request::Parts;
use std::{convert::Infallible, future::Future};
#[diagnostic::do_not_recommend]
impl<S> FromRequestParts<S> for ()
where
S: Send + Sync,
@ -18,6 +19,7 @@ macro_rules! impl_from_request {
(
[$($ty:ident),*], $last:ident
) => {
#[diagnostic::do_not_recommend]
#[allow(non_snake_case, unused_mut, unused_variables)]
impl<S, $($ty,)* $last> FromRequestParts<S> for ($($ty,)* $last,)
where
@ -43,6 +45,7 @@ macro_rules! impl_from_request {
// This impl must not be generic over M, otherwise it would conflict with the blanket
// implementation of `FromRequest<S, Mut>` for `T: FromRequestParts<S>`.
#[diagnostic::do_not_recommend]
#[allow(non_snake_case, unused_mut, unused_variables)]
impl<S, $($ty,)* $last> FromRequest<S> for ($($ty,)* $last,)
where

View File

@ -1,4 +1,4 @@
use super::{IntoResponseParts, Response, ResponseParts};
use super::{ForceStatusCode, IntoResponseFailed, IntoResponseParts, Response, ResponseParts};
use crate::{body::Body, BoxError};
use bytes::{buf::Chain, Buf, Bytes, BytesMut};
use http::{
@ -329,7 +329,9 @@ where
{
fn into_response(self) -> Response {
let mut res = self.1.into_response();
*res.status_mut() = self.0;
if res.extensions().get::<IntoResponseFailed>().is_none() {
*res.status_mut() = self.0;
}
res
}
}
@ -405,18 +407,16 @@ macro_rules! impl_into_response {
let ($($ty),*, res) = self;
let res = res.into_response();
let parts = ResponseParts { res };
$(
let parts = match $ty.into_response_parts(parts) {
if res.extensions().get::<IntoResponseFailed>().is_none() {
let parts = ResponseParts { res };
let parts = match ($($ty,)*).into_response_parts(parts) {
Ok(parts) => parts,
Err(err) => {
return err.into_response();
}
Err(err) => return err.into_response(),
};
)*
parts.res
parts.res
} else {
res
}
}
}
@ -430,16 +430,40 @@ macro_rules! impl_into_response {
let (status, $($ty),*, res) = self;
let res = res.into_response();
let parts = ResponseParts { res };
$(
let parts = match $ty.into_response_parts(parts) {
if res.extensions().get::<IntoResponseFailed>().is_none() {
let parts = ResponseParts { res };
let mut parts = match ($($ty,)*).into_response_parts(parts) {
Ok(parts) => parts,
Err(err) => {
return err.into_response();
}
Err(err) => return err.into_response(),
};
)*
// Don't call `(status, parts.res).into_response()` since that checks for
// `IntoResponseFailed` and skips setting the status. We've already done that
// check here so overriding the status is required if returning
// `(IntoResponseFailed, StatusCode::INTERNAL_SERVER_ERROR)`
*parts.res.status_mut() = status;
parts.res
} else {
res
}
}
}
#[allow(non_snake_case)]
impl<R, $($ty,)*> IntoResponse for (ForceStatusCode, $($ty),*, R)
where
$( $ty: IntoResponseParts, )*
R: IntoResponse,
{
fn into_response(self) -> Response {
let (status, $($ty),*, res) = self;
let res = res.into_response();
let parts = ResponseParts { res };
let parts = match ($($ty,)*).into_response_parts(parts) {
Ok(parts) => parts,
Err(err) => return err.into_response(),
};
(status, parts.res).into_response()
}
@ -455,17 +479,22 @@ macro_rules! impl_into_response {
let (outer_parts, $($ty),*, res) = self;
let res = res.into_response();
let parts = ResponseParts { res };
$(
let parts = match $ty.into_response_parts(parts) {
if res.extensions().get::<IntoResponseFailed>().is_none() {
let parts = ResponseParts { res };
let mut parts = match ($($ty,)*).into_response_parts(parts) {
Ok(parts) => parts,
Err(err) => {
return err.into_response();
}
Err(err) => return err.into_response(),
};
)*
(outer_parts, parts.res).into_response()
// Don't call `(outer_parts, parts.res).into_response()` for the same reason we
// don't call `(status, parts.res).into_response()` in the above impl.
*parts.res.status_mut() = outer_parts.status;
parts.res.headers_mut().extend(outer_parts.headers);
parts.res.extensions_mut().extend(outer_parts.extensions);
parts.res
} else {
res
}
}
}

View File

@ -241,7 +241,9 @@ macro_rules! impl_into_response_parts {
let res = match $ty.into_response_parts(res) {
Ok(res) => res,
Err(err) => {
return Err(err.into_response());
let mut err_res = err.into_response();
err_res.extensions_mut().insert(super::IntoResponseFailed);
return Err(err_res);
}
};
)*
@ -270,3 +272,19 @@ impl IntoResponseParts for () {
Ok(res)
}
}
#[cfg(test)]
mod tests {
use http::StatusCode;
use crate::response::IntoResponse;
#[test]
fn failed_into_response_parts() {
let response = (StatusCode::CREATED, [("\n", "\n")]).into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let response = (StatusCode::CREATED, [("\n", "\n")], ()).into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
}

View File

@ -4,6 +4,10 @@
//!
//! [`axum::response`]: https://docs.rs/axum/0.8/axum/response/index.html
use std::convert::Infallible;
use http::StatusCode;
use crate::body::Body;
mod append_headers;
@ -128,3 +132,87 @@ where
Self(value.into_response())
}
}
/// Response part that stops status code overrides.
///
/// This type should be used by types implementing [`IntoResponseParts`] or
/// [`IntoResponse`] when they fail to produce the response usually expected of
/// them and return some sort of error response instead.
///
/// It is checked used by the tuple impls of [`IntoResponse`] that have a
/// [`StatusCode`] as their first element to ignore that status code.
/// Consider the following example:
///
/// ```no_run
/// # use axum::Json;
/// # use http::StatusCode;
/// # #[derive(serde::Serialize)]
/// # struct CreatedResponse { }
/// fn my_handler(/* ... */) -> (StatusCode, Json<CreatedResponse>) {
/// // This response type's serialization may fail
/// let response = CreatedResponse { /* ... */ };
/// (StatusCode::CREATED, Json(response))
/// }
/// ```
///
/// When `response` serialization succeeds, the server responds with a status
/// code of 201 Created (overwriting `Json`s default status code of 200 OK),
/// and the expected JSON payload.
///
/// When `response` serialization fails hoewever, `impl IntoResponse for Json`
/// return a response with status code 500 Internal Server Error, and
/// `IntoResponseFailed` as a response extension, and the 201 Created override
/// is ignored.
///
/// This is a behavior introduced with axum 0.9.\
/// To force a status code override even when an inner [`IntoResponseParts`] /
/// [`IntoResponse`] failed, use [`ForceStatusCode`].
#[derive(Copy, Clone, Debug)]
pub struct IntoResponseFailed;
impl IntoResponseParts for IntoResponseFailed {
type Error = Infallible;
fn into_response_parts(self, mut res: ResponseParts) -> Result<ResponseParts, Self::Error> {
res.extensions_mut().insert(self);
Ok(res)
}
}
/// Not sure it makes sense to return `IntoResponseFailed` as the whole response. You should
/// probably at least combine it with a status code.
///
/// ```compile_fail
/// fn foo()
/// where
/// axum_core::response::IntoResponseFailed: axum_core::response::IntoResponse,
/// {}
/// ```
#[allow(dead_code)]
fn into_response_failed_doesnt_impl_into_response() {}
/// Set the status code regardless of whether [`IntoResponseFailed`] is used or not.
///
/// See the docs for [`IntoResponseFailed`] for more details.
#[derive(Debug, Copy, Clone, Default)]
pub struct ForceStatusCode(pub StatusCode);
impl IntoResponse for ForceStatusCode {
fn into_response(self) -> Response {
let mut res = ().into_response();
*res.status_mut() = self.0;
res
}
}
impl<R> IntoResponse for (ForceStatusCode, R)
where
R: IntoResponse,
{
fn into_response(self) -> Response {
let (ForceStatusCode(status), res) = self;
let mut res = res.into_response();
*res.status_mut() = status;
res
}
}

View File

@ -5,6 +5,51 @@ 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].
# Unreleased
- **breaking:** Remove the deprecated `Host`, `Scheme` and `OptionalPath`
extractors ([#3599])
- Also remove `HostRejection` which only had `FailedToResolveHost`
previously used in the `Host` extractor ([#3636])
- **breaking:** Change `routing::RouterExt::route_with_tsr` to only redirect
the HTTP methods that the supplied `MethodRouter` handles. This allows the
following pattern which lead to a panic before because the two
`route_with_tsr` calls would both attempt to register a method-independent
redirect ([#3586]):
```rust
Router::new()
.route_with_tsr("/path", get(/* handler */))
.route_with_tsr("/path", post(/* handler */))
```
[#3599]: https://github.com/tokio-rs/axum/pull/3599
[#3586]: https://github.com/tokio-rs/axum/pull/3586
# 0.12.5
- **fixed:** `JsonLines` now correctly respects the default body limit ([#3591])
[#3591]: https://github.com/tokio-rs/axum/pull/3591
# 0.12.4
- **changed:** Deprecated the `Host` and `Scheme` extractors ([#3595])
- Please comment in [#3442] if you depend on one of them
[#3595]: https://github.com/tokio-rs/axum/pull/3595
[#3442]: https://github.com/tokio-rs/axum/issues/3442
# 0.12.3
- **changed:** Make the `typed-routing` feature enable the `routing` feature ([#3514])
- **changed:** Add trailing newline to `ErasedJson::pretty` response bodies ([#3526])
- **fixed:** Fix integer underflow in `FileStream::try_range_response` for empty files ([#3566])
[#3514]: https://github.com/tokio-rs/axum/pull/3514
[#3526]: https://github.com/tokio-rs/axum/pull/3526
[#3566]: https://github.com/tokio-rs/axum/pull/3566
# 0.12.2
- Make it easier to visually scan for default features ([#3550])

View File

@ -4,12 +4,35 @@ description = "Extra utilities for axum"
edition = "2021"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
keywords = ["http", "web", "routing"]
license = "MIT"
name = "axum-extra"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.12.2"
version = "0.12.5"
[package.metadata.docs.rs]
all-features = true
[package.metadata.cargo_check_external_types]
allowed_external_types = [
"axum::*",
"axum_core::*",
"axum_macros::*",
"bytes::*",
"cookie::*",
"futures_core::*",
"futures_util::*",
"headers",
"headers_core::*",
"http::*",
"http_body::*",
"prost::*",
"serde_core::*",
"tokio::*",
"tower_layer::*",
"tower_service::*",
]
[features]
default = ["tracing"]
@ -50,10 +73,8 @@ json-lines = [
]
middleware = ["dep:axum"]
multipart = ["dep:multer", "dep:fastrand"]
optional-path = ["dep:axum", "dep:serde_core"]
protobuf = ["dep:prost"]
routing = ["axum/original-uri", "dep:rustversion"]
scheme = []
query = [
"dep:form_urlencoded",
"dep:serde_core",
@ -63,6 +84,7 @@ query = [
tracing = ["axum-core/tracing", "axum/tracing", "dep:tracing"]
typed-header = ["dep:headers"]
typed-routing = [
"routing",
"dep:axum-macros",
"dep:percent-encoding",
"dep:serde_core",
@ -73,10 +95,10 @@ with-rejection = ["dep:axum"]
# Enabled by docs.rs because it uses all-features
# Enables upstream things linked to in docs
__private_docs = ["axum/json", "dep:serde", "dep:tower"]
__private_docs = ["axum/json", "axum/query", "dep:serde", "dep:tower"]
[dependencies]
axum-core = { path = "../axum-core", version = "0.5.2" }
axum-core = { path = "../axum-core", version = "0.5.6" }
bytes = "1.1.0"
futures-core = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
@ -89,7 +111,7 @@ tower-layer = "0.3"
tower-service = "0.3"
# optional dependencies
axum = { path = "../axum", version = "0.8.7", default-features = false, optional = true }
axum = { path = "../axum", version = "0.8.8", default-features = false, optional = true }
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 }
@ -100,6 +122,11 @@ percent-encoding = { version = "2.1", optional = true }
prost = { version = "0.14", optional = true }
rustversion = { version = "1.0.9", optional = true }
serde_core = { version = "1.0.221", optional = true }
# DO NOT update. axum itself uses serde_html_form 0.3.x which has slightly
# different behavior in some edge cases. This feature here is kept (deprecated)
# to let people transition to that easier (fewer breaking changes to deal with
# at once if they keep using axum_extra's `Query` / `Form` initially when going
# to axum 0.9).
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 }
@ -120,13 +147,11 @@ 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"
tempfile = "3.23.0"
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"
tracing-subscriber = "0.3.20"
[lints]
workspace = true
[package.metadata.docs.rs]
all-features = true

View File

@ -1,7 +0,0 @@
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.

1
axum-extra/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

View File

@ -283,8 +283,8 @@ where
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)),
Self::E1(layer) => Either::E1(layer.layer(inner)),
Self::E2(layer) => Either::E2(layer.layer(inner)),
}
}
}
@ -300,15 +300,15 @@ where
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),
Self::E1(inner) => inner.poll_ready(cx),
Self::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)),
Self::E1(inner) => futures_util::future::Either::Left(inner.call(req)),
Self::E2(inner) => futures_util::future::Either::Right(inner.call(req)),
}
}
}

View File

@ -206,7 +206,7 @@ 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());
set_cookies(&self.jar, res.headers_mut());
Ok(res)
}
}
@ -217,7 +217,7 @@ impl IntoResponse for CookieJar {
}
}
fn set_cookies(jar: cookie::CookieJar, headers: &mut HeaderMap) {
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);
@ -321,13 +321,13 @@ mod tests {
}
impl FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Key {
fn from_ref(state: &AppState) -> Self {
state.key.clone()
}
}
impl FromRef<AppState> for CustomKey {
fn from_ref(state: &AppState) -> CustomKey {
fn from_ref(state: &AppState) -> Self {
state.custom_key.clone()
}
}

View File

@ -137,7 +137,7 @@ where
key,
_marker: _,
} = PrivateCookieJar::from_headers(&parts.headers, key);
Ok(PrivateCookieJar {
Ok(Self {
jar,
key,
_marker: PhantomData,
@ -274,7 +274,7 @@ 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());
set_cookies(&self.jar, res.headers_mut());
Ok(res)
}
}

View File

@ -154,7 +154,7 @@ where
key,
_marker: _,
} = SignedCookieJar::from_headers(&parts.headers, key);
Ok(SignedCookieJar {
Ok(Self {
jar,
key,
_marker: PhantomData,
@ -292,7 +292,7 @@ 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());
set_cookies(&self.jar, res.headers_mut());
Ok(res)
}
}

View File

@ -1,3 +1,5 @@
#![allow(deprecated)]
use axum::extract::rejection::RawFormRejection;
use axum::{
extract::{FromRequest, RawForm, Request},
@ -12,31 +14,16 @@ use serde_core::de::DeserializeOwned;
///
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Differences from `axum::extract::Form`
/// # Deprecated
///
/// 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.
/// This extractor used to use a different deserializer under-the-hood but that
/// is no longer the case. Now it only uses an older version of the same
/// deserializer, purely for ease of transition to the latest version.
/// Before switching to `axum::extract::Form`, it is recommended to read the
/// [changelog for `serde_html_form v0.3.0`][changelog].
///
/// # 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
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
#[deprecated = "see documentation"]
#[derive(Debug, Clone, Copy, Default)]
#[cfg(feature = "form")]
pub struct Form<T>(pub T);
@ -90,6 +77,7 @@ composite_rejection! {
/// Rejection used for [`Form`].
///
/// Contains one variant for each way the [`Form`] extractor can fail.
#[deprecated = "because Form is deprecated"]
pub enum FormRejection {
RawFormRejection,
FailedToDeserializeForm,

View File

@ -1,246 +0,0 @@
use super::rejection::{FailedToResolveHost, HostRejection};
use axum_core::{
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
}
}

View File

@ -183,21 +183,15 @@ composite_rejection! {
}
fn json_content_type(headers: &HeaderMap) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
content_type
} else {
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
return false;
};
let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
let Ok(content_type) = content_type.to_str() else {
return false;
};
let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
mime
} else {
let Ok(mime) = content_type.parse::<mime::Mime>() else {
return false;
};

View File

@ -1,11 +1,5 @@
//! Additional extractors.
mod host;
pub mod rejection;
#[cfg(feature = "optional-path")]
mod optional_path;
#[cfg(feature = "cached")]
mod cached;
@ -27,15 +21,6 @@ mod query;
#[cfg(feature = "multipart")]
pub mod multipart;
#[cfg(feature = "scheme")]
mod scheme;
#[allow(deprecated)]
#[cfg(feature = "optional-path")]
pub use self::optional_path::OptionalPath;
pub use self::host::Host;
#[cfg(feature = "cached")]
pub use self::cached::Cached;
@ -52,20 +37,18 @@ pub use self::cookie::PrivateCookieJar;
pub use self::cookie::SignedCookieJar;
#[cfg(feature = "form")]
#[allow(deprecated)]
pub use self::form::{Form, FormRejection};
#[cfg(feature = "query")]
pub use self::query::OptionalQuery;
#[cfg(feature = "query")]
#[allow(deprecated)]
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,

View File

@ -1,98 +0,0 @@
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`"
);
}
}

View File

@ -1,3 +1,5 @@
#![allow(deprecated)]
use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use axum_core::extract::FromRequestParts;
@ -8,72 +10,16 @@ use serde_core::de::DeserializeOwned;
///
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Differences from `axum::extract::Query`
/// # Deprecated
///
/// 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.
/// This extractor used to use a different deserializer under-the-hood but that
/// is no longer the case. Now it only uses an older version of the same
/// deserializer, purely for ease of transition to the latest version.
/// Before switching to `axum::extract::Form`, it is recommended to read the
/// [changelog for `serde_html_form v0.3.0`][changelog].
///
/// # 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;
/// ```
/// [changelog]: https://github.com/jplatte/serde_html_form/blob/main/CHANGELOG.md#030
#[deprecated = "see documentation"]
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T);
@ -91,7 +37,7 @@ where
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))
Ok(Self(value))
}
}
@ -140,6 +86,7 @@ composite_rejection! {
/// Rejection used for [`Query`].
///
/// Contains one variant for each way the [`Query`] extractor can fail.
#[deprecated = "because Query is deprecated"]
pub enum QueryRejection {
FailedToDeserializeQueryString,
}
@ -147,7 +94,7 @@ composite_rejection! {
/// Extractor that deserializes query strings into `None` if no query parameters are present.
///
/// Otherwise behaviour is identical to [`Query`].
/// Otherwise behaviour is identical to [`Query`][axum::extract::Query].
/// `T` is expected to implement [`serde::Deserialize`].
///
/// # Example
@ -179,11 +126,6 @@ composite_rejection! {
///
/// 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>);
@ -201,9 +143,9 @@ where
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)))
Ok(Self(Some(value)))
} else {
Ok(OptionalQuery(None))
Ok(Self(None))
}
}
}

View File

@ -1,23 +0,0 @@
//! 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,
}
}

View File

@ -1,146 +0,0 @@
//! Extractor that parses the scheme of a request.
//! See [`Scheme`] for more details.
use axum_core::{__define_rejection as define_rejection, extract::FromRequestParts};
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
}
}

View File

@ -119,7 +119,7 @@ where
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let extractor = E::from_request(req, state).await?;
Ok(WithRejection(extractor, PhantomData))
Ok(Self(extractor, PhantomData))
}
}
@ -133,7 +133,7 @@ where
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))
Ok(Self(extractor, PhantomData))
}
}
@ -188,7 +188,7 @@ mod tests {
impl From<()> for TestRejection {
fn from(_: ()) -> Self {
TestRejection
Self
}
}

View File

@ -7,7 +7,8 @@ use axum::{
handler::Handler,
response::{IntoResponse, Response},
};
use futures_util::future::{BoxFuture, FutureExt, Map};
use futures_core::future::BoxFuture;
use futures_util::future::{FutureExt, Map};
use std::{future::Future, marker::PhantomData};
mod or;

View File

@ -5,7 +5,8 @@ use axum::{
handler::Handler,
response::{IntoResponse, Response},
};
use futures_util::future::{BoxFuture, Either as EitherFuture, FutureExt, Map};
use futures_core::future::BoxFuture;
use futures_util::future::{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

View File

@ -4,10 +4,11 @@ use axum_core::{
body::Body,
extract::{FromRequest, Request},
response::{IntoResponse, Response},
BoxError,
BoxError, RequestExt,
};
use bytes::{BufMut, BytesMut};
use futures_util::stream::{BoxStream, Stream, TryStream, TryStreamExt};
use futures_core::{stream::BoxStream, Stream, TryStream};
use futures_util::stream::TryStreamExt;
use pin_project_lite::pin_project;
use serde_core::{de::DeserializeOwned, Serialize};
use std::{
@ -44,7 +45,7 @@ pin_project! {
/// ```rust
/// use axum::{BoxError, response::{IntoResponse, Response}};
/// use axum_extra::json_lines::JsonLines;
/// use futures_util::stream::Stream;
/// use futures_core::stream::Stream;
///
/// fn stream_of_values() -> impl Stream<Item = Result<serde_json::Value, BoxError>> {
/// # futures_util::stream::empty()
@ -108,7 +109,7 @@ where
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 body = req.into_limited_body();
let stream = body.into_data_stream();
let stream = stream.map_err(io::Error::other);
let read = StreamReader::new(stream);

View File

@ -18,15 +18,14 @@
//! `cookie-key-expansion` | Enables the [`Key::derive_from`](crate::extract::cookie::Key::derive_from) method |
//! `erased-json` | Enables the [`ErasedJson`](crate::response::ErasedJson) response |
//! `error-response` | Enables the [`InternalServerError`](crate::response::InternalServerError) response |
//! `form` | Enables the [`Form`](crate::extract::Form) extractor |
//! `form` (deprecated) | Enables the [`Form`](crate::extract::Form) extractor |
//! `handler` | Enables the [handler] utilities |
//! `json-deserializer` | Enables the [`JsonDeserializer`](crate::extract::JsonDeserializer) extractor |
//! `json-lines` | Enables the [`JsonLines`](crate::extract::JsonLines) extractor and response |
//! `middleware` | Enables the [middleware] utilities |
//! `multipart` | Enables the [`Multipart`](crate::extract::Multipart) extractor |
//! `optional-path` | Enables the [`OptionalPath`](crate::extract::OptionalPath) extractor |
//! `protobuf` | Enables the [`Protobuf`](crate::protobuf::Protobuf) extractor and response |
//! `query` | Enables the [`Query`](crate::extract::Query) extractor |
//! `query` (deprecated) | Enables the [`Query`](crate::extract::Query) extractor |
//! `routing` | Enables the [routing] utilities |
//! `tracing` | Log rejections from built-in extractors | <span role="img" aria-label="Default feature">✔</span>
//! `typed-routing` | Enables the [`TypedPath`](crate::routing::TypedPath) routing utilities and the `routing` feature. |
@ -88,8 +87,5 @@ pub mod __private {
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;

View File

@ -4,7 +4,7 @@ use axum_core::__composite_rejection as composite_rejection;
use axum_core::__define_rejection as define_rejection;
use axum_core::{
extract::{rejection::BytesRejection, FromRequest, Request},
response::{IntoResponse, Response},
response::{IntoResponse, IntoResponseFailed, Response},
RequestExt,
};
use bytes::BytesMut;
@ -109,7 +109,7 @@ where
.aggregate();
match T::decode(&mut buf) {
Ok(value) => Ok(Protobuf(value)),
Ok(value) => Ok(Self(value)),
Err(err) => Err(ProtobufDecodeError::from_err(err).into()),
}
}
@ -131,7 +131,12 @@ where
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(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
IntoResponseFailed,
err.to_string(),
)
.into_response(),
}
}
}
@ -161,6 +166,8 @@ mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{routing::post, Router};
use http::header::CONTENT_TYPE;
use http::StatusCode;
#[tokio::test]
async fn decode_body() {
@ -172,7 +179,7 @@ mod tests {
let app = Router::new().route(
"/",
post(|input: Protobuf<Input>| async move { input.foo.to_owned() }),
post(|Protobuf(input): Protobuf<Input>| async move { input.foo }),
);
let input = Input {
@ -182,9 +189,11 @@ mod tests {
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).await;
let status = res.status();
let body = res.text().await;
assert_eq!(body, "bar");
assert_eq!(status, StatusCode::OK);
assert_eq!(body, input.foo);
}
#[tokio::test]
@ -211,6 +220,7 @@ mod tests {
let res = client.post("/").body(input.encode_to_vec()).await;
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
assert!(res.text().await.starts_with("Failed to decode the body"));
}
#[tokio::test]
@ -228,10 +238,8 @@ mod tests {
}
#[axum::debug_handler]
async fn handler(input: Protobuf<Input>) -> Protobuf<Output> {
let output = Output {
result: input.foo.to_owned(),
};
async fn handler(Protobuf(input): Protobuf<Input>) -> Protobuf<Output> {
let output = Output { result: input.foo };
Protobuf(output)
}
@ -245,8 +253,13 @@ mod tests {
let client = TestClient::new(app);
let res = client.post("/").body(input.encode_to_vec()).await;
let content_type_header_value = res
.headers()
.get(CONTENT_TYPE)
.expect("missing expected header");
assert_eq!(
res.headers()["content-type"],
content_type_header_value,
mime::APPLICATION_OCTET_STREAM.as_ref()
);
@ -254,6 +267,6 @@ mod tests {
let output = Output::decode(body).unwrap();
assert_eq!(output.result, "bar");
assert_eq!(output.result, input.foo);
}
}

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use axum_core::response::{IntoResponse, Response};
use axum_core::response::{IntoResponse, IntoResponseFailed, Response};
use bytes::{BufMut, Bytes, BytesMut};
use http::{header, HeaderValue, StatusCode};
use serde_core::Serialize;
@ -57,7 +57,10 @@ impl ErasedJson {
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()),
Ok(()) => {
bytes.put_u8(b'\n');
Ok(bytes.freeze())
}
Err(e) => Err(Arc::new(e)),
};
Self(result)
@ -75,7 +78,12 @@ impl IntoResponse for ErasedJson {
bytes,
)
.into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
IntoResponseFailed,
err.to_string(),
)
.into_response(),
}
}
}
@ -127,6 +135,7 @@ impl IntoResponse for ErasedJson {
/// ```
/// let response = axum_extra::json!(["trailing",]);
/// ```
#[cfg_attr(docsrs, doc(cfg(feature = "erased-json")))]
#[macro_export]
macro_rules! json {
($($t:tt)*) => {

View File

@ -6,9 +6,9 @@ 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.
/// the `IntoResponse` trait itself. Error details are logged using [`tracing::error!`]
/// and a generic `500 Internal Server Error` response is returned to the client without
/// exposing error details.
///
/// ```rust
/// use axum_extra::response::InternalServerError;

View File

@ -4,7 +4,7 @@ use axum_core::{
BoxError,
};
use bytes::Bytes;
use futures_util::TryStream;
use futures_core::TryStream;
use http::{header, StatusCode};
use std::{io, path::Path};
use tokio::{
@ -191,6 +191,10 @@ where
let metadata = file.metadata().await?;
let total_size = metadata.len();
if total_size == 0 {
return Ok((StatusCode::RANGE_NOT_SATISFIABLE, "Range Not Satisfiable").into_response());
}
if end == 0 {
end = total_size - 1;
}
@ -284,7 +288,7 @@ where
.unwrap_or_else(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("build FileStream responsec error: {e}"),
format!("build FileStream response error: {e}"),
)
.into_response()
})
@ -596,4 +600,52 @@ mod tests {
}
Some((start, end))
}
#[tokio::test]
async fn response_range_empty_file() -> Result<(), Box<dyn std::error::Error>> {
let file = tempfile::NamedTempFile::new()?;
file.as_file().set_len(0)?;
let path = file.path().to_owned();
let app = Router::new().route(
"/range_empty",
get(move |headers: HeaderMap| {
let path = path.clone();
async move {
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)
};
FileStream::<ReaderStream<File>>::try_range_response(path, start, end)
.await
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response())
}
}),
);
let response = app
.oneshot(
Request::builder()
.uri("/range_empty")
.header(header::RANGE, "bytes=0-")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
Ok(())
}
}

View File

@ -26,7 +26,7 @@ impl MultipartForm {
/// let form = MultipartForm::with_parts(parts);
/// ```
pub fn with_parts(parts: Vec<Part>) -> Self {
MultipartForm { parts }
Self { parts }
}
}

View File

@ -3,7 +3,7 @@
use axum::{
extract::{OriginalUri, Request},
response::{IntoResponse, Redirect, Response},
routing::{any, MethodRouter},
routing::{any, on, MethodFilter, MethodRouter},
Router,
};
use http::{uri::PathAndQuery, StatusCode, Uri};
@ -68,6 +68,7 @@ pub const fn __private_validate_static_path(path: &'static str) -> &'static str
/// ```
///
/// This macro is available only on rust versions 1.80 and above.
#[cfg_attr(docsrs, doc(cfg(feature = "routing")))]
#[rustversion::since(1.80)]
#[macro_export]
macro_rules! vpath {
@ -336,8 +337,9 @@ where
Self: Sized,
{
validate_tsr_path(path);
let method_filter = method_router.method_filter();
self = self.route(path, method_router);
add_tsr_redirect_route(self, path)
add_tsr_redirect_route(self, path, method_filter)
}
#[track_caller]
@ -350,7 +352,7 @@ where
{
validate_tsr_path(path);
self = self.route_service(path, service);
add_tsr_redirect_route(self, path)
add_tsr_redirect_route(self, path, None)
}
}
@ -361,7 +363,11 @@ fn validate_tsr_path(path: &str) {
}
}
fn add_tsr_redirect_route<S>(router: Router<S>, path: &str) -> Router<S>
fn add_tsr_redirect_route<S>(
router: Router<S>,
path: &str,
method_filter: Option<MethodFilter>,
) -> Router<S>
where
S: Clone + Send + Sync + 'static,
{
@ -373,17 +379,27 @@ where
});
if let Some(new_uri) = new_uri {
Redirect::permanent(&new_uri.to_string()).into_response()
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))
let _slot;
let redirect_path = if let Some(without_slash) = path.strip_suffix('/') {
without_slash
} else {
router.route(&format!("{path}/"), any(redirect_handler))
}
// FIXME: Can return `&format!(...)` directly when MSRV is updated
_slot = format!("{path}/");
&_slot
};
let method_router = match method_filter {
Some(f) => on(f, redirect_handler),
None => any(redirect_handler),
};
router.route(redirect_path, method_router)
}
/// Map the path of a `Uri`.
@ -417,7 +433,10 @@ mod sealed {
mod tests {
use super::*;
use crate::test_helpers::*;
use axum::{extract::Path, routing::get};
use axum::{
extract::Path,
routing::{get, post},
};
#[tokio::test]
async fn test_tsr() {
@ -500,6 +519,13 @@ mod tests {
assert_eq!(res.headers()["location"], "/neko/nyan/");
}
#[test]
fn tsr_independent_route_registration() {
let _: Router = Router::new()
.route_with_tsr("/x", get(|| async {}))
.route_with_tsr("/x", post(|| async {}));
}
#[test]
#[should_panic = "Cannot add a trailing slash redirect route for `/`"]
fn tsr_at_root() {

View File

@ -320,7 +320,7 @@ where
/// 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
/// If you see it in type errors it's most likely because the first argument to your handler doesn't
/// implement [`TypedPath`].
///
/// You normally shouldn't have to use this trait directly.
@ -384,12 +384,7 @@ impl_second_element_is!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13,
#[cfg(test)]
mod tests {
use crate::routing::{RouterExt, TypedPath};
use axum::{
extract::rejection::PathRejection,
response::{IntoResponse, Response},
Router,
};
use crate::routing::TypedPath;
use serde::{Deserialize, Serialize};
#[derive(TypedPath, Deserialize)]
@ -441,6 +436,12 @@ mod tests {
#[cfg(feature = "with-rejection")]
#[allow(dead_code)] // just needs to compile
fn supports_with_rejection() {
use crate::routing::RouterExt;
use axum::{
extract::rejection::PathRejection,
response::{IntoResponse, Response},
Router,
};
async fn handler(_: crate::extract::WithRejection<UsersShow, MyRejection>) {}
struct MyRejection {}

View File

@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
- **breaking:** `#[from_request(via(Extractor))]` now uses the extractor's
rejection type instead of `axum::response::Response` ([#3261])
[#3261]: https://github.com/tokio-rs/axum/pull/3261
# 0.5.0
*No changes since alpha.1*

View File

@ -11,13 +11,16 @@ readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
version = "0.5.0" # remember to also bump the version that axum and axum-extra depends on
[features]
default = []
__private = ["syn/visit-mut"]
[package.metadata.docs.rs]
all-features = true
[lib]
proc-macro = true
[features]
default = []
__private = ["syn/visit-mut"]
[dependencies]
proc-macro2 = "1.0"
quote = "1.0"
@ -40,9 +43,3 @@ trybuild = "1.0.63"
[lints]
workspace = true
[package.metadata.cargo-public-api-crates]
allowed = []
[package.metadata.docs.rs]
all-features = true

View File

@ -1,7 +0,0 @@
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.

1
axum-macros/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

View File

@ -1 +1 @@
nightly-2024-06-22
nightly-2025-09-28

View File

@ -8,16 +8,16 @@ use proc_macro2::{Ident, Span, TokenStream};
use quote::{format_ident, quote, quote_spanned};
use syn::{parse::Parse, spanned::Spanned, FnArg, ItemFn, ReturnType, Token, Type};
pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenStream {
pub(crate) fn expand(attr: Attrs, item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
let Attrs { state_ty } = attr;
let mut state_ty = state_ty.map(second);
let check_extractor_count = check_extractor_count(&item_fn, kind);
let check_path_extractor = check_path_extractor(&item_fn, kind);
let check_output_tuples = check_output_tuples(&item_fn);
let check_extractor_count = check_extractor_count(item_fn, kind);
let check_path_extractor = check_path_extractor(item_fn, kind);
let check_output_tuples = check_output_tuples(item_fn);
let check_output_impls_into_response = if check_output_tuples.is_empty() {
check_output_impls_into_response(&item_fn)
check_output_impls_into_response(item_fn)
} else {
check_output_tuples
};
@ -28,7 +28,7 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenS
let mut err = None;
if state_ty.is_none() {
let state_types_from_args = state_types_from_args(&item_fn);
let state_types_from_args = state_types_from_args(item_fn);
#[allow(clippy::comparison_chain)]
if state_types_from_args.len() == 1 {
@ -50,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenS
err.unwrap_or_else(|| {
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
let check_future_send = check_future_send(&item_fn, kind);
let check_future_send = check_future_send(item_fn, kind);
if let Some(check_input_order) = check_input_order(&item_fn, kind) {
if let Some(check_input_order) = check_input_order(item_fn, kind) {
quote! {
#check_input_order
#check_future_send
}
} else {
let check_inputs_impls_from_request =
check_inputs_impls_from_request(&item_fn, state_ty, kind);
check_inputs_impls_from_request(item_fn, &state_ty, kind);
quote! {
#check_inputs_impls_from_request
@ -76,7 +76,7 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn, kind: FunctionKind) -> TokenS
};
let middleware_takes_next_as_last_arg =
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(item_fn));
quote! {
#item_fn
@ -97,17 +97,17 @@ pub(crate) enum FunctionKind {
impl fmt::Display for FunctionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FunctionKind::Handler => f.write_str("handler"),
FunctionKind::Middleware => f.write_str("middleware"),
Self::Handler => f.write_str("handler"),
Self::Middleware => f.write_str("middleware"),
}
}
}
impl FunctionKind {
fn name_uppercase_plural(&self) -> &'static str {
fn name_uppercase_plural(self) -> &'static str {
match self {
FunctionKind::Handler => "Handlers",
FunctionKind::Middleware => "Middleware",
Self::Handler => "Handlers",
Self::Middleware => "Middleware",
}
}
}
@ -222,7 +222,7 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
fn check_inputs_impls_from_request(
item_fn: &ItemFn,
state_ty: Type,
state_ty: &Type,
kind: FunctionKind,
) -> TokenStream {
let takes_self = item_fn.sig.inputs.first().is_some_and(|arg| match arg {

View File

@ -19,10 +19,10 @@ pub(crate) enum Trait {
}
impl Trait {
fn via_marker_type(&self) -> Option<Type> {
fn via_marker_type(self) -> Option<Type> {
match self {
Trait::FromRequest => Some(parse_quote!(M)),
Trait::FromRequestParts => None,
Self::FromRequest => Some(parse_quote!(M)),
Self::FromRequestParts => None,
}
}
}
@ -30,8 +30,8 @@ impl Trait {
impl fmt::Display for Trait {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Trait::FromRequest => f.write_str("FromRequest"),
Trait::FromRequestParts => f.write_str("FromRequestParts"),
Self::FromRequest => f.write_str("FromRequest"),
Self::FromRequestParts => f.write_str("FromRequestParts"),
}
}
}
@ -50,9 +50,9 @@ impl State {
/// ```
fn impl_generics(&self) -> impl Iterator<Item = Type> {
match self {
State::Default(inner) => Some(inner.clone()),
State::Custom(_) => None,
State::CannotInfer => Some(parse_quote!(S)),
Self::Default(inner) => Some(inner.clone()),
Self::Custom(_) => None,
Self::CannotInfer => Some(parse_quote!(S)),
}
.into_iter()
}
@ -63,18 +63,18 @@ impl State {
/// ```
fn trait_generics(&self) -> impl Iterator<Item = Type> {
match self {
State::Default(inner) | State::Custom(inner) => iter::once(inner.clone()),
State::CannotInfer => iter::once(parse_quote!(S)),
Self::Default(inner) | Self::Custom(inner) => iter::once(inner.clone()),
Self::CannotInfer => iter::once(parse_quote!(S)),
}
}
fn bounds(&self) -> TokenStream {
match self {
State::Custom(_) => quote! {},
State::Default(inner) => quote! {
Self::Custom(_) => quote! {},
Self::Default(inner) => quote! {
#inner: ::std::marker::Send + ::std::marker::Sync,
},
State::CannotInfer => quote! {
Self::CannotInfer => quote! {
S: ::std::marker::Send + ::std::marker::Sync,
},
}
@ -84,8 +84,8 @@ impl State {
impl ToTokens for State {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
State::Custom(inner) | State::Default(inner) => inner.to_tokens(tokens),
State::CannotInfer => quote! { S }.to_tokens(tokens),
Self::Custom(inner) | Self::Default(inner) => inner.to_tokens(tokens),
Self::CannotInfer => quote! { S }.to_tokens(tokens),
}
}
}
@ -111,43 +111,42 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
state,
} = parse_attrs("from_request", &attrs)?;
let state = match state {
Some((_, state)) => State::Custom(state),
None => {
let mut inferred_state_types: HashSet<_> =
infer_state_type_from_field_types(&fields)
.chain(infer_state_type_from_field_attributes(&fields))
.collect();
let state = if let Some((_, state)) = state {
State::Custom(state)
} else {
let mut inferred_state_types: HashSet<_> =
infer_state_type_from_field_types(&fields)
.chain(infer_state_type_from_field_attributes(&fields))
.collect();
if let Some((_, via)) = &via {
inferred_state_types.extend(state_from_via(&ident, via));
}
if let Some((_, via)) = &via {
inferred_state_types.extend(state_from_via(&ident, via));
}
match inferred_state_types.len() {
0 => State::Default(syn::parse_quote!(S)),
1 => State::Custom(inferred_state_types.iter().next().unwrap().to_owned()),
_ => State::CannotInfer,
}
match inferred_state_types.len() {
0 => State::Default(syn::parse_quote!(S)),
1 => State::Custom(inferred_state_types.iter().next().unwrap().to_owned()),
_ => State::CannotInfer,
}
};
let trait_impl = match (via.map(second), rejection.map(second)) {
(Some(via), rejection) => impl_struct_by_extracting_all_at_once(
ident,
&ident,
fields,
via,
rejection,
generic_ident,
&via,
rejection.as_ref(),
generic_ident.as_ref(),
&state,
tr,
)?,
(None, rejection) => {
error_on_generic_ident(generic_ident, tr)?;
impl_struct_by_extracting_each_field(ident, fields, rejection, &state, tr)?
impl_struct_by_extracting_each_field(&ident, &fields, rejection, &state, tr)?
}
};
if let State::CannotInfer = state {
if matches!(state, State::CannotInfer) {
let attr_name = match tr {
Trait::FromRequest => "from_request",
Trait::FromRequestParts => "from_request_parts",
@ -207,11 +206,11 @@ pub(crate) fn expand(item: syn::Item, tr: Trait) -> syn::Result<TokenStream> {
match (via.map(second), rejection) {
(Some(via), rejection) => impl_enum_by_extracting_all_at_once(
ident,
&ident,
variants,
via,
rejection.map(second),
state,
&via,
rejection.map(second).as_ref(),
&state,
tr,
),
(None, Some((rejection_kw, _))) => Err(syn::Error::new_spanned(
@ -329,29 +328,28 @@ fn error_on_generic_ident(generic_ident: Option<Ident>, tr: Trait) -> syn::Resul
}
fn impl_struct_by_extracting_each_field(
ident: syn::Ident,
fields: syn::Fields,
ident: &syn::Ident,
fields: &syn::Fields,
rejection: Option<syn::Path>,
state: &State,
tr: Trait,
) -> syn::Result<TokenStream> {
let trait_fn_body = match state {
State::CannotInfer => quote! {
let trait_fn_body = if matches!(state, State::CannotInfer) {
quote! {
::std::unimplemented!()
},
_ => {
let extract_fields = extract_fields(&fields, &rejection, tr)?;
quote! {
::std::result::Result::Ok(Self {
#(#extract_fields)*
})
}
}
} else {
let extract_fields = extract_fields(fields, rejection.as_ref(), tr)?;
quote! {
::std::result::Result::Ok(Self {
#(#extract_fields)*
})
}
};
let rejection_ident = if let Some(rejection) = rejection {
quote!(#rejection)
} else if has_no_fields(&fields) {
} else if has_no_fields(fields) {
quote!(::std::convert::Infallible)
} else {
quote!(::axum::response::Response)
@ -413,23 +411,22 @@ fn has_no_fields(fields: &syn::Fields) -> bool {
fn extract_fields(
fields: &syn::Fields,
rejection: &Option<syn::Path>,
rejection: Option<&syn::Path>,
tr: Trait,
) -> syn::Result<Vec<TokenStream>> {
fn member(field: &syn::Field, index: usize) -> TokenStream {
match &field.ident {
Some(ident) => quote! { #ident },
_ => {
let member = syn::Member::Unnamed(syn::Index {
index: index as u32,
span: field.span(),
});
quote! { #member }
}
if let Some(ident) = &field.ident {
quote! { #ident }
} else {
let member = syn::Member::Unnamed(syn::Index {
index: index as u32,
span: field.span(),
});
quote! { #member }
}
}
fn into_inner(via: &Option<(attr::kw::via, syn::Path)>, ty_span: Span) -> TokenStream {
fn into_inner(via: Option<&(attr::kw::via, syn::Path)>, ty_span: Span) -> TokenStream {
if let Some((_, path)) = via {
let span = path.span();
quote_spanned! {span=>
@ -443,7 +440,7 @@ fn extract_fields(
}
fn into_outer(
via: &Option<(attr::kw::via, syn::Path)>,
via: Option<&(attr::kw::via, syn::Path)>,
ty_span: Span,
field_ty: &Type,
) -> TokenStream {
@ -475,10 +472,10 @@ fn extract_fields(
let member = member(field, index);
let ty_span = field.ty.span();
let into_inner = into_inner(&via, ty_span);
let into_inner = into_inner(via.as_ref(), ty_span);
if peel_option(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
let field_ty = into_outer(via.as_ref(), ty_span, peel_option(&field.ty).unwrap());
let tokens = match tr {
Trait::FromRequest => {
quote_spanned! {ty_span=>
@ -513,7 +510,7 @@ fn extract_fields(
};
Ok(tokens)
} else if peel_result_ok(&field.ty).is_some() {
let field_ty = into_outer(&via,ty_span, peel_result_ok(&field.ty).unwrap());
let field_ty = into_outer(via.as_ref(), ty_span, peel_result_ok(&field.ty).unwrap());
let tokens = match tr {
Trait::FromRequest => {
quote_spanned! {ty_span=>
@ -546,7 +543,7 @@ fn extract_fields(
};
Ok(tokens)
} else {
let field_ty = into_outer(&via,ty_span,&field.ty);
let field_ty = into_outer(via.as_ref(), ty_span, &field.ty);
let map_err = if let Some(rejection) = rejection {
quote! { <#rejection as ::std::convert::From<_>>::from }
} else {
@ -596,10 +593,10 @@ fn extract_fields(
let member = member(field, fields.len() - 1);
let ty_span = field.ty.span();
let into_inner = into_inner(&via, ty_span);
let into_inner = into_inner(via.as_ref(), ty_span);
let item = if peel_option(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_option(&field.ty).unwrap());
let field_ty = into_outer(via.as_ref(), ty_span, peel_option(&field.ty).unwrap());
quote_spanned! {ty_span=>
#member: {
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
@ -609,7 +606,7 @@ fn extract_fields(
},
}
} else if peel_result_ok(&field.ty).is_some() {
let field_ty = into_outer(&via, ty_span, peel_result_ok(&field.ty).unwrap());
let field_ty = into_outer(via.as_ref(), ty_span, peel_result_ok(&field.ty).unwrap());
quote_spanned! {ty_span=>
#member: {
<#field_ty as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
@ -618,7 +615,7 @@ fn extract_fields(
},
}
} else {
let field_ty = into_outer(&via, ty_span, &field.ty);
let field_ty = into_outer(via.as_ref(), ty_span, &field.ty);
let map_err = if let Some(rejection) = rejection {
quote! { <#rejection as ::std::convert::From<_>>::from }
} else {
@ -642,9 +639,7 @@ fn extract_fields(
}
fn peel_option(ty: &syn::Type) -> Option<&syn::Type> {
let type_path = if let syn::Type::Path(type_path) = ty {
type_path
} else {
let syn::Type::Path(type_path) = ty else {
return None;
};
@ -673,9 +668,7 @@ fn peel_option(ty: &syn::Type) -> Option<&syn::Type> {
}
fn peel_result_ok(ty: &syn::Type) -> Option<&syn::Type> {
let type_path = if let syn::Type::Path(type_path) = ty {
type_path
} else {
let syn::Type::Path(type_path) = ty else {
return None;
};
@ -704,11 +697,11 @@ fn peel_result_ok(ty: &syn::Type) -> Option<&syn::Type> {
}
fn impl_struct_by_extracting_all_at_once(
ident: syn::Ident,
ident: &syn::Ident,
fields: syn::Fields,
via_path: syn::Path,
rejection: Option<syn::Path>,
generic_ident: Option<Ident>,
via_path: &syn::Path,
rejection: Option<&syn::Path>,
generic_ident: Option<&Ident>,
state: &State,
tr: Trait,
) -> syn::Result<TokenStream> {
@ -732,18 +725,6 @@ fn impl_struct_by_extracting_all_at_once(
let path_span = via_path.span();
let (associated_rejection_type, map_err) = if let Some(rejection) = &rejection {
let rejection = quote! { #rejection };
let map_err = quote! { ::std::convert::From::from };
(rejection, map_err)
} else {
let rejection = quote! {
::axum::response::Response
};
let map_err = quote! { ::axum::response::IntoResponse::into_response };
(rejection, map_err)
};
// for something like
//
// ```
@ -757,7 +738,7 @@ fn impl_struct_by_extracting_all_at_once(
// - `State`, not other extractors
//
// honestly not sure why but the tests all pass
let via_marker_type = if path_ident_is_state(&via_path) {
let via_marker_type = if path_ident_is_state(via_path) {
tr.via_marker_type()
} else {
None
@ -812,6 +793,19 @@ fn impl_struct_by_extracting_all_at_once(
quote! { Self }
};
let associated_rejection_type = if let Some(rejection) = &rejection {
quote! { #rejection }
} else {
match tr {
Trait::FromRequest => quote! {
<#via_path<#via_type_generics> as ::axum::extract::FromRequest<#trait_generics>>::Rejection
},
Trait::FromRequestParts => quote! {
<#via_path<#via_type_generics> as ::axum::extract::FromRequestParts<#trait_generics>>::Rejection
},
}
};
let value_to_self = if generic_ident.is_some() {
quote! {
#ident(value)
@ -841,7 +835,7 @@ fn impl_struct_by_extracting_all_at_once(
<#via_path<#via_type_generics> as ::axum::extract::FromRequest<_, _>>::from_request(req, state)
.await
.map(|#via_path(value)| #value_to_self)
.map_err(#map_err)
.map_err(::std::convert::From::from)
}
}
}
@ -864,7 +858,7 @@ fn impl_struct_by_extracting_all_at_once(
<#via_path<#via_type_generics> as ::axum::extract::FromRequestParts<_>>::from_request_parts(parts, state)
.await
.map(|#via_path(value)| #value_to_self)
.map_err(#map_err)
.map_err(::std::convert::From::from)
}
}
}
@ -875,11 +869,11 @@ fn impl_struct_by_extracting_all_at_once(
}
fn impl_enum_by_extracting_all_at_once(
ident: syn::Ident,
ident: &syn::Ident,
variants: Punctuated<syn::Variant, Token![,]>,
path: syn::Path,
rejection: Option<syn::Path>,
state: State,
path: &syn::Path,
rejection: Option<&syn::Path>,
state: &State,
tr: Trait,
) -> syn::Result<TokenStream> {
for variant in variants {

View File

@ -438,7 +438,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
/// let app = Router::new().route("/", get(handler));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// axum::serve(listener, app).await;
/// }
///
/// fn handler() -> &'static str {
@ -458,7 +458,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
/// # let app = Router::new().route("/", get(handler));
/// #
/// # let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// # axum::serve(listener, app).await.unwrap();
/// # axum::serve(listener, app).await;
/// # }
/// #
/// #[debug_handler]
@ -485,7 +485,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
/// let app = Router::new().route("/", get(handler));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// axum::serve(listener, app).await;
/// }
///
/// #[debug_handler]
@ -579,7 +579,7 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[cfg(debug_assertions)]
return expand_attr_with(_attr, input, |attrs, item_fn| {
debug_handler::expand(attrs, item_fn, FunctionKind::Handler)
debug_handler::expand(attrs, &item_fn, FunctionKind::Handler)
});
}
@ -607,7 +607,7 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
/// .layer(middleware::from_fn(my_middleware));
///
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app).await.unwrap();
/// axum::serve(listener, app).await;
/// }
///
/// // if this wasn't a valid middleware function #[debug_middleware] would
@ -635,7 +635,7 @@ pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream {
#[cfg(debug_assertions)]
return expand_attr_with(_attr, input, |attrs, item_fn| {
debug_handler::expand(attrs, item_fn, FunctionKind::Middleware)
debug_handler::expand(attrs, &item_fn, FunctionKind::Middleware)
});
}
@ -662,7 +662,7 @@ pub fn __private_axum_test(_attr: TokenStream, input: TokenStream) -> TokenStrea
/// [`axum_extra::routing::TypedPath`]: https://docs.rs/axum-extra/latest/axum_extra/routing/trait.TypedPath.html
#[proc_macro_derive(TypedPath, attributes(typed_path))]
pub fn derive_typed_path(input: TokenStream) -> TokenStream {
expand_with(input, typed_path::expand)
expand_with(input, |item_struct| typed_path::expand(&item_struct))
}
/// Derive an implementation of [`FromRef`] for each field in a struct.

View File

@ -4,14 +4,14 @@ use syn::{parse::Parse, ItemStruct, LitStr, Token};
use crate::attr_parsing::{combine_attribute, parse_parenthesized_attribute, second, Combine};
pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
pub(crate) fn expand(item_struct: &ItemStruct) -> syn::Result<TokenStream> {
let ItemStruct {
attrs,
ident,
generics,
fields,
..
} = &item_struct;
} = item_struct;
if !generics.params.is_empty() || generics.where_clause.is_some() {
return Err(syn::Error::new_spanned(
@ -34,13 +34,18 @@ pub(crate) fn expand(item_struct: ItemStruct) -> syn::Result<TokenStream> {
match fields {
syn::Fields::Named(_) => {
let segments = parse_path(&path)?;
Ok(expand_named_fields(ident, path, &segments, rejection))
Ok(expand_named_fields(
ident,
&path,
&segments,
rejection.as_ref(),
))
}
syn::Fields::Unnamed(fields) => {
let segments = parse_path(&path)?;
expand_unnamed_fields(fields, ident, path, &segments, rejection)
expand_unnamed_fields(fields, ident, &path, &segments, rejection.as_ref())
}
syn::Fields::Unit => expand_unit_fields(ident, path, rejection),
syn::Fields::Unit => expand_unit_fields(ident, &path, rejection.as_ref()),
}
}
@ -95,9 +100,9 @@ impl Combine for Attrs {
fn expand_named_fields(
ident: &syn::Ident,
path: LitStr,
path: &LitStr,
segments: &[Segment],
rejection: Option<syn::Path>,
rejection: Option<&syn::Path>,
) -> TokenStream {
let format_str = format_str_from_path(segments);
let captures = captures_from_path(segments);
@ -129,8 +134,8 @@ fn expand_named_fields(
}
};
let rejection_assoc_type = rejection_assoc_type(&rejection);
let map_err_rejection = map_err_rejection(&rejection);
let rejection_assoc_type = rejection_assoc_type(rejection);
let map_err_rejection = map_err_rejection(rejection);
let from_request_impl = quote! {
#[automatically_derived]
@ -162,9 +167,9 @@ fn expand_named_fields(
fn expand_unnamed_fields(
fields: &syn::FieldsUnnamed,
ident: &syn::Ident,
path: LitStr,
path: &LitStr,
segments: &[Segment],
rejection: Option<syn::Path>,
rejection: Option<&syn::Path>,
) -> syn::Result<TokenStream> {
let num_captures = segments
.iter()
@ -233,8 +238,8 @@ fn expand_unnamed_fields(
}
};
let rejection_assoc_type = rejection_assoc_type(&rejection);
let map_err_rejection = map_err_rejection(&rejection);
let rejection_assoc_type = rejection_assoc_type(rejection);
let map_err_rejection = map_err_rejection(rejection);
let from_request_impl = quote! {
#[automatically_derived]
@ -273,10 +278,10 @@ fn simple_pluralize(count: usize, word: &str) -> String {
fn expand_unit_fields(
ident: &syn::Ident,
path: LitStr,
rejection: Option<syn::Path>,
path: &LitStr,
rejection: Option<&syn::Path>,
) -> syn::Result<TokenStream> {
for segment in parse_path(&path)? {
for segment in parse_path(path)? {
match segment {
Segment::Capture(_, span) => {
return Err(syn::Error::new(
@ -409,14 +414,14 @@ fn path_rejection() -> TokenStream {
}
}
fn rejection_assoc_type(rejection: &Option<syn::Path>) -> TokenStream {
fn rejection_assoc_type(rejection: Option<&syn::Path>) -> TokenStream {
match rejection {
Some(rejection) => quote! { #rejection },
None => path_rejection(),
}
}
fn map_err_rejection(rejection: &Option<syn::Path>) -> TokenStream {
fn map_err_rejection(rejection: Option<&syn::Path>) -> TokenStream {
rejection
.as_ref()
.map(|rejection| {

View File

@ -40,8 +40,8 @@ impl<I> WithPosition<I>
where
I: Iterator,
{
pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> WithPosition<I> {
WithPosition {
pub(crate) fn new(iter: impl IntoIterator<IntoIter = I>) -> Self {
Self {
handled_first: false,
peekable: iter.into_iter().fuse().peekable(),
}
@ -72,7 +72,7 @@ pub(crate) enum Position<T> {
impl<T> Position<T> {
pub(crate) fn into_inner(self) -> T {
match self {
Position::First(x) | Position::Middle(x) | Position::Last(x) | Position::Only(x) => x,
Self::First(x) | Self::Middle(x) | Self::Last(x) | Self::Only(x) => x,
}
}
}

View File

@ -2,19 +2,19 @@ error[E0277]: the trait bound `bool: FromRequest<(), axum_core::extract::private
--> tests/debug_handler/fail/argument_not_extractor.rs:4:24
|
4 | async fn handler(_foo: bool) {}
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`, which is required by `bool: FromRequest<(), _>`
| ^^^^ the trait `FromRequestParts<()>` is not implemented for `bool`
|
= note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details
= help: the following other types implement trait `FromRequestParts<S>`:
`()` implements `FromRequestParts<S>`
`(T1, T2)` implements `FromRequestParts<S>`
`(T1, T2, T3)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
`ConnectInfo<T>` implements `FromRequestParts<S>`
`Extension<T>` implements `FromRequestParts<S>`
`HeaderMap` implements `FromRequestParts<S>`
`MatchedPath` implements `FromRequestParts<S>`
`Method` implements `FromRequestParts<S>`
`OriginalUri` implements `FromRequestParts<S>`
`Query<T>` implements `FromRequestParts<S>`
`RawPathParams` implements `FromRequestParts<S>`
and $N others
= note: required for `bool` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_handler_0_from_request_check`

View File

@ -2,18 +2,17 @@ error[E0277]: the trait bound `NonCloneType: Clone` is not satisfied
--> tests/debug_handler/fail/extension_not_clone.rs:7:38
|
7 | async fn test_extension_non_clone(_: Extension<NonCloneType>) {}
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonCloneType`, which is required by `Extension<NonCloneType>: FromRequest<(), _>`
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `NonCloneType`
|
= help: the following other types implement trait `FromRequest<S, M>`:
(T1, T2)
(T1, T2, T3)
(T1, T2, T3, T4)
(T1, T2, T3, T4, T5)
(T1, T2, T3, T4, T5, T6)
(T1, T2, T3, T4, T5, T6, T7)
(T1, T2, T3, T4, T5, T6, T7, T8)
(T1, T2, T3, T4, T5, T6, T7, T8, T9)
and $N others
Body
Form<T>
Json<T>
RawForm
Result<T, <T as FromRequest<S>>::Rejection>
String
axum::body::Bytes
axum::http::Request<Body>
= note: required for `Extension<NonCloneType>` to implement `FromRequestParts<()>`
= note: required for `Extension<NonCloneType>` to implement `FromRequest<(), axum_core::extract::private::ViaParts>`
note: required by a bound in `__axum_macros_check_test_extension_non_clone_0_from_request_check`

View File

@ -2,8 +2,13 @@ error[E0277]: the trait bound `Struct: serde::Deserialize<'de>` is not satisfied
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
| ^^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Struct`
--> tests/debug_handler/fail/json_not_deserialize.rs:4:1
|
4 | struct Struct {}
| ^^^^^^^^^^^^^
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Struct` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
@ -28,8 +33,13 @@ error[E0277]: the trait bound `Struct: serde::Deserialize<'de>` is not satisfied
--> tests/debug_handler/fail/json_not_deserialize.rs:7:24
|
7 | async fn handler(_foo: Json<Struct>) {}
| ^^^^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Struct`, which is required by `Json<Struct>: FromRequest<()>`
| ^^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `Struct`
--> tests/debug_handler/fail/json_not_deserialize.rs:4:1
|
4 | struct Struct {}
| ^^^^^^^^^^^^^
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `Struct` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:

View File

@ -4,7 +4,7 @@ error: future cannot be sent between threads safely
3 | #[debug_handler]
| ^^^^^^^^^^^^^^^^ future returned by `handler` is not `Send`
|
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`, which is required by `impl Future<Output = ()>: Send`
= help: within `impl Future<Output = ()>`, the trait `Send` is not implemented for `Rc<()>`
note: future is not `Send` as this value is used across an await
--> tests/debug_handler/fail/not_send.rs:6:14
|

View File

@ -1,11 +1,11 @@
error: Cannot return tuples with more than 17 elements
--> tests/debug_handler/fail/output_tuple_too_many.rs:4:20
|
4 | async fn handler() -> (
4 | async fn handler() -> (
| ____________________^
5 | | axum::http::StatusCode,
6 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
7 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
5 | | axum::http::StatusCode,
6 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
7 | | AppendHeaders<[(axum::http::HeaderName, &'static str); 1]>,
... |
23 | | axum::http::StatusCode,
24 | | ) {

View File

@ -2,8 +2,13 @@ error[E0277]: the trait bound `NotIntoResponse: IntoResponse` is not satisfied
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:6:23
|
6 | async fn handler() -> (NotIntoResponse) {
| ^^^^^^^^^^^^^^^^^ the trait `IntoResponse` is not implemented for `NotIntoResponse`
| ^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `IntoResponse` is not implemented for `NotIntoResponse`
--> tests/debug_handler/fail/single_wrong_return_tuple.rs:3:1
|
3 | struct NotIntoResponse;
| ^^^^^^^^^^^^^^^^^^^^^^
= help: the following other types implement trait `IntoResponse`:
&'static [u8; N]
&'static [u8]

View File

@ -1,10 +1,10 @@
error: Handlers cannot take more than 16 arguments. Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors
--> tests/debug_handler/fail/too_many_extractors.rs:6:5
|
6 | / _e1: Uri,
7 | | _e2: Uri,
8 | | _e3: Uri,
9 | | _e4: Uri,
6 | / _e1: Uri,
7 | | _e2: Uri,
8 | | _e3: Uri,
9 | | _e4: Uri,
... |
21 | | _e16: Uri,
22 | | _e17: Uri,

View File

@ -8,8 +8,13 @@ error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not sat
--> tests/debug_handler/fail/wrong_return_tuple.rs:21:5
|
21 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
| ^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
--> tests/debug_handler/fail/wrong_return_tuple.rs:12:1
|
12 | struct CustomIntoResponse {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^
= help: the following other types implement trait `IntoResponseParts`:
()
(T1, T2)
@ -23,15 +28,20 @@ error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not sat
= help: see issue #48214
help: add `#![feature(trivial_bounds)]` to the crate attributes to enable
|
3 + #![feature(trivial_bounds)]
3 + #![feature(trivial_bounds)]
|
error[E0277]: the trait bound `CustomIntoResponse: IntoResponseParts` is not satisfied
--> tests/debug_handler/fail/wrong_return_tuple.rs:21:5
|
21 | CustomIntoResponse,
| ^^^^^^^^^^^^^^^^^^ the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
| ^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
|
help: the trait `IntoResponseParts` is not implemented for `CustomIntoResponse`
--> tests/debug_handler/fail/wrong_return_tuple.rs:12:1
|
12 | struct CustomIntoResponse {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^
= help: the following other types implement trait `IntoResponseParts`:
()
(T1, T2)

View File

@ -13,9 +13,6 @@ error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {fo
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|

View File

@ -13,9 +13,6 @@ error[E0277]: the trait bound `fn(Extractor<()>) -> impl Future<Output = ()> {fo
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|

View File

@ -13,9 +13,6 @@ error[E0277]: the trait bound `fn(MyExtractor) -> impl Future<Output = ()> {hand
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `axum::routing::get`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|
@ -35,9 +32,6 @@ error[E0277]: the trait bound `fn(Result<MyExtractor, MyRejection>) -> impl Futu
| required by a bound introduced by this call
|
= note: Consider using `#[axum::debug_handler]` to improve the error message
= help: the following other types implement trait `Handler<T, S>`:
`Layered<L, H, T, S>` implements `Handler<T, S>`
`MethodRouter<S>` implements `Handler<(), S>`
note: required by a bound in `MethodRouter::<S>::post`
--> $WORKSPACE/axum/src/routing/method_routing.rs
|

View File

@ -7,12 +7,18 @@ error[E0277]: the trait bound `String: FromRequestParts<_>` is not satisfied
= note: Function argument is not a valid axum extractor.
See `https://docs.rs/axum/0.8/axum/extract/index.html` for details
= help: the following other types implement trait `FromRequestParts<S>`:
`()` implements `FromRequestParts<S>`
`(T1, T2)` implements `FromRequestParts<S>`
`(T1, T2, T3)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7)` implements `FromRequestParts<S>`
`(T1, T2, T3, T4, T5, T6, T7, T8)` implements `FromRequestParts<S>`
`ConnectInfo<T>` implements `FromRequestParts<S>`
`Extension<T>` implements `FromRequestParts<S>`
`Extractor` implements `FromRequestParts<S>`
`HeaderMap` implements `FromRequestParts<S>`
`MatchedPath` implements `FromRequestParts<S>`
`Method` implements `FromRequestParts<S>`
`OriginalUri` implements `FromRequestParts<S>`
`Query<T>` implements `FromRequestParts<S>`
and $N others
error[E0282]: type annotations needed
--> tests/from_request/fail/parts_extracting_body.rs:5:11
|
5 | body: String,
| ^^^^^^ cannot infer type

View File

@ -1,6 +1,7 @@
use axum::{
extract::{FromRequest, Json},
response::Response,
use axum::extract::{
rejection::JsonRejection,
FromRequest,
Json,
};
use serde::Deserialize;
@ -14,7 +15,7 @@ struct Extractor {
fn assert_from_request()
where
Extractor: FromRequest<(), Rejection = Response>,
Extractor: FromRequest<(), Rejection = JsonRejection>,
{
}

View File

@ -1,6 +1,7 @@
use axum::{
extract::{Extension, FromRequestParts},
response::Response,
use axum::extract::{
rejection::ExtensionRejection,
Extension,
FromRequestParts,
};
#[derive(Clone, FromRequestParts)]
@ -13,7 +14,7 @@ struct Extractor {
fn assert_from_request()
where
Extractor: FromRequestParts<(), Rejection = Response>,
Extractor: FromRequestParts<(), Rejection = ExtensionRejection>,
{
}

View File

@ -10,5 +10,9 @@ help: include the missing field in the pattern
| ++++++
help: if you don't care about this missing field, you can explicitly ignore it
|
5 | #[typed_path("/users" { id: _ })]
| +++++++++
help: or always ignore missing fields here
|
5 | #[typed_path("/users" { .. })]
| ++++++

View File

@ -1,9 +1,39 @@
error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satisfied
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
3 | #[derive(TypedPath)]
| ^^^^^^^^^ unsatisfied trait bound
|
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`
--> tests/typed_path/fail/not_deserialize.rs:5:1
|
5 | struct MyPath {
| ^^^^^^^^^^^^^
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `MyPath` to implement `serde_core::de::DeserializeOwned`
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
= note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `MyPath: serde::Deserialize<'de>` is not satisfied
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
3 | #[derive(TypedPath)]
| ^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
| ^^^^^^^^^ unsatisfied trait bound
|
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`
--> tests/typed_path/fail/not_deserialize.rs:5:1
|
5 | struct MyPath {
| ^^^^^^^^^^^^^
= note: for local types consider adding `#[derive(serde::Deserialize)]` to your `MyPath` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
@ -23,8 +53,13 @@ error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satis
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
3 | #[derive(TypedPath)]
| ^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
| ^^^^^^^^^ unsatisfied trait bound
|
help: the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`
--> tests/typed_path/fail/not_deserialize.rs:5:1
|
5 | struct MyPath {
| ^^^^^^^^^^^^^
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
@ -37,23 +72,3 @@ error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satis
and $N others
= note: required for `MyPath` to implement `serde_core::de::DeserializeOwned`
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
error[E0277]: the trait bound `MyPath: serde::de::DeserializeOwned` is not satisfied
--> tests/typed_path/fail/not_deserialize.rs:3:10
|
3 | #[derive(TypedPath)]
| ^^^^^^^^^ the trait `for<'de> serde_core::de::Deserialize<'de>` is not implemented for `MyPath`, which is required by `axum::extract::Path<MyPath>: FromRequestParts<S>`
|
= help: the following other types implement trait `serde_core::de::Deserialize<'de>`:
&'a [u8]
&'a serde_json::raw::RawValue
&'a std::path::Path
&'a str
()
(T,)
(T0, T1)
(T0, T1, T2)
and $N others
= note: required for `MyPath` to implement `serde_core::de::DeserializeOwned`
= note: required for `axum::extract::Path<MyPath>` to implement `FromRequestParts<S>`
= note: this error originates in the derive macro `TypedPath` (in Nightly builds, run with -Z macro-backtrace for more info)

View File

@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
# Unreleased
- **breaking:** Router fallbacks are now properly merged for nested routers ([#3158])
- **breaking:** `#[from_request(via(Extractor))]` now uses the extractor's
rejection type instead of `axum::response::Response` ([#3261])
- **breaking:** `axum::serve` now applies hyper's default `header_read_timeout` ([#3478])
- **breaking:** `axum::serve` future output type has been adjusted to remove `io::Result`
(never returned `Err`) and be an uninhabited type if `with_graceful_shutdown` is not used
(because it was already never terminating if that method wasn't used) ([#3601])
- **added:** New `ListenerExt::limit_connections` allows limiting concurrent `axum::serve` connections ([#3489])
- **added:** `MethodRouter::method_filter` ([#3586])
- **added:** `WebSocketUpgrade::{requested_protocols, set_selected_protocol}` for more
flexible subprotocol selection ([#3597])
- **changed:** `serve` has an additional generic argument and can now work with any response body
type, not just `axum::body::Body` ([#3205])
- **changed:** Update minimum rust version to 1.80 ([#3620])
- **changed:** `Redirect` constructors now accept any `impl Into<String>` ([#3635])
[#3158]: https://github.com/tokio-rs/axum/pull/3158
[#3261]: https://github.com/tokio-rs/axum/pull/3261
[#3205]: https://github.com/tokio-rs/axum/pull/3205
[#3478]: https://github.com/tokio-rs/axum/pull/3478
[#3601]: https://github.com/tokio-rs/axum/pull/3601
[#3489]: https://github.com/tokio-rs/axum/pull/3489
[#3586]: https://github.com/tokio-rs/axum/pull/3586
[#3597]: https://github.com/tokio-rs/axum/pull/3597
[#3620]: https://github.com/tokio-rs/axum/pull/3620
# 0.8.8
- Clarify documentation for `Router::route_layer` ([#3567])
[#3567]: https://github.com/tokio-rs/axum/pull/3567
# 0.8.7
- Relax implicit `Send` / `Sync` bounds on `RouterAsService`, `RouterIntoService` ([#3555])

View File

@ -1,16 +1,42 @@
[package]
name = "axum"
version = "0.8.7" # remember to bump the version that axum-extra depends on
version = "0.8.8" # remember to bump the version that axum-extra depends on
categories = ["asynchronous", "network-programming", "web-programming::http-server"]
description = "Web framework that focuses on ergonomics and modularity"
description = "HTTP routing and request handling library that focuses on ergonomics and modularity"
edition = "2021"
rust-version = { workspace = true }
homepage = "https://github.com/tokio-rs/axum"
keywords = ["http", "web", "framework"]
keywords = ["http", "web", "routing"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/tokio-rs/axum"
[package.metadata.docs.rs]
all-features = true
[package.metadata.playground]
features = ["http1", "http2", "json", "multipart", "ws"]
[package.metadata.cargo_check_external_types]
allowed_external_types = [
# our crates
"axum_core::*",
"axum_macros::*",
# not 1.0
"futures_core::*",
"futures_sink::*",
"futures_util::*",
"tower_layer::*",
"tower_service::*",
# >=1.0
"bytes::*",
"http",
"http::*",
"http_body::*",
"serde_core::*",
"tokio::*",
]
[features]
default = [
"form",
@ -23,7 +49,13 @@ default = [
"tower-log",
"tracing",
]
form = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
form = [
"dep:form_urlencoded",
"dep:serde_html_form",
"dep:serde_path_to_error",
"serde_html_form/ser",
"serde_html_form/de",
]
http1 = ["dep:hyper", "hyper?/http1", "hyper-util?/http1"]
http2 = ["dep:hyper", "hyper?/http2", "hyper-util?/http2"]
json = ["dep:serde_json", "dep:serde_path_to_error"]
@ -31,11 +63,30 @@ macros = ["dep:axum-macros"]
matched-path = []
multipart = ["dep:multer"]
original-uri = []
query = ["dep:form_urlencoded", "dep:serde_urlencoded", "dep:serde_path_to_error"]
tokio = ["dep:hyper-util", "dep:tokio", "tokio/net", "tokio/rt", "tower/make", "tokio/macros"]
query = [
"dep:form_urlencoded",
"dep:serde_html_form",
"dep:serde_path_to_error",
"serde_html_form/de",
]
tokio = [
"dep:hyper-util",
"dep:tokio",
"tokio/net",
"tokio/rt",
"tower/make",
"tokio/macros",
]
tower-log = ["tower/log"]
tracing = ["dep:tracing", "axum-core/tracing"]
ws = ["dep:hyper", "tokio", "dep:tokio-tungstenite", "dep:sha1", "dep:base64"]
ws = [
"dep:hyper",
"dep:futures-sink",
"tokio",
"dep:tokio-tungstenite",
"dep:sha1",
"dep:base64",
]
__private_docs = [
# We re-export some docs from axum-core via #[doc(inline)],
@ -52,8 +103,9 @@ __private_docs = [
__private = ["tokio", "http1", "dep:reqwest"]
[dependencies]
axum-core = { path = "../axum-core", version = "0.5.5" }
bytes = "1.0"
axum-core = { path = "../axum-core", version = "0.5.6" }
bytes = "1.7"
futures-core = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
http = "1.0.0"
http-body = "1.0.0"
@ -74,13 +126,14 @@ tower-service = "0.3"
axum-macros = { path = "../axum-macros", version = "0.5.0", optional = true }
base64 = { version = "0.22.1", optional = true }
form_urlencoded = { version = "1.1.0", optional = true }
hyper = { version = "1.1.0", optional = true }
hyper-util = { version = "0.1.3", features = ["tokio", "server", "service"], optional = true }
futures-sink = { version = "0.3", optional = true }
hyper = { version = "1.4.0", optional = true }
hyper-util = { version = "0.1.4", features = ["tokio", "server", "service"], optional = true }
multer = { version = "3.0.0", optional = true }
reqwest = { version = "0.12", optional = true, default-features = false, features = ["json", "stream", "multipart"] }
serde_json = { version = "1.0", features = ["raw_value"], optional = true }
serde_html_form = { version = "0.4.0", optional = true, default-features = false, features = ["std"] }
serde_json = { version = "1.0.29", features = ["raw_value"], optional = true }
serde_path_to_error = { version = "0.1.8", optional = true }
serde_urlencoded = { version = "0.7", optional = true }
sha1 = { version = "0.10", optional = true }
tokio = { package = "tokio", version = "1.44", features = ["time"], optional = true }
tokio-tungstenite = { version = "0.28.0", optional = true }
@ -133,13 +186,13 @@ quickcheck = "1.0"
quickcheck_macros = "1.0"
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
serde = { version = "1.0.221", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
serde_json = { version = "1.0.29", features = ["raw_value"] }
time = { version = "0.3", features = ["serde-human-readable"] }
tokio = { package = "tokio", version = "1.44.2", features = ["macros", "rt", "rt-multi-thread", "net", "test-util"] }
tokio-stream = "0.1"
tokio-tungstenite = "0.28.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["json"] }
tracing-subscriber = { version = "0.3.20", features = ["json"] }
uuid = { version = "1.0", features = ["serde", "v4"] }
[dev-dependencies.tower]
@ -191,43 +244,6 @@ features = [
[lints]
workspace = true
[package.metadata.docs.rs]
all-features = true
[package.metadata.playground]
features = [
"http1",
"http2",
"json",
"multipart",
"ws",
]
[package.metadata.cargo-public-api-crates]
allowed = [
# our crates
"axum_core",
"axum_macros",
# not 1.0
"futures_core",
"futures_sink",
"futures_util",
"pin_project_lite",
"tower_layer",
"tower_service",
# >=1.0
"bytes",
"http",
"http_body",
"serde_core",
"tokio",
# for the `__private` feature
"reqwest",
]
[[bench]]
name = "benches"
harness = false

View File

@ -1,25 +0,0 @@
Copyright (c) 2019 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.

1
axum/LICENSE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE

View File

@ -1,6 +1,6 @@
# axum
`axum` is a web application framework that focuses on ergonomics and modularity.
`axum` is an HTTP routing and request-handling library that focuses on ergonomics and modularity.
[![Build status](https://github.com/tokio-rs/axum/actions/workflows/CI.yml/badge.svg?branch=main)](https://github.com/tokio-rs/axum/actions/workflows/CI.yml)
[![Crates.io](https://img.shields.io/crates/v/axum)](https://crates.io/crates/axum)
@ -17,12 +17,19 @@ More information about this crate can be found in the [crate documentation][docs
- Take full advantage of the [`tower`] and [`tower-http`] ecosystem of
middleware, services, and utilities.
In particular the last point is what sets `axum` apart from other frameworks.
In particular the last point is what sets `axum` apart from other libraries / frameworks.
`axum` doesn't have its own middleware system but instead uses
[`tower::Service`]. This means `axum` gets timeouts, tracing, compression,
authorization, and more, for free. It also enables you to share middleware with
applications written using [`hyper`] or [`tonic`].
## ⚠ Breaking changes ⚠
We are currently working towards axum 0.9 so the `main` branch contains breaking
changes. See the [`0.8.x`] branch for what's released to crates.io.
[`0.8.x`]: https://github.com/tokio-rs/axum/tree/v0.8.x
## Usage example
```rust
@ -47,7 +54,7 @@ async fn main() {
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
axum::serve(listener, app).await;
}
// basic handler that responds with a static string
@ -104,7 +111,7 @@ This crate uses `#![forbid(unsafe_code)]` to ensure everything is implemented in
## Minimum supported Rust version
axum's MSRV is 1.78.
axum's MSRV is 1.80.
## Examples

View File

@ -163,9 +163,9 @@ impl BenchmarkBuilder {
.unwrap();
let addr = listener.local_addr().unwrap();
#[allow(unreachable_code)] // buggy lint, fixed in nightly
std::thread::spawn(move || {
rt.block_on(axum::serve(listener, app).into_future())
.unwrap();
rt.block_on(axum::serve(listener, app).into_future());
});
let mut cmd = Command::new("rewrk");

View File

@ -48,8 +48,8 @@ async fn fallback_two() -> impl IntoResponse { /* ... */ }
## Setting the `Allow` header
By default `MethodRouter` will set the `Allow` header when returning `405 Method
Not Allowed`. This is also done when the fallback is used unless the response
generated by the fallback already sets the `Allow` header.
Not Allowed`. This is also done when the fallback returns `405 Method Not Allowed`
unless the response generated by the fallback already sets the `Allow` header.
This means if you use `fallback` to accept additional methods, you should make
sure you set the `Allow` header correctly.

View File

@ -1,8 +1,7 @@
Apply a [`tower::Layer`] to the router that will only run if the request matches
a route.
Note that the middleware is only applied to existing routes. So you have to
first add your routes (and / or fallback) and then call `route_layer`
Note that the middleware is only applied to existing routes. First add your routes and then call `route_layer`
afterwards. Additional routes added after `route_layer` is called will not have
the middleware added.

View File

@ -208,7 +208,7 @@ use axum::{
body::Body,
extract::Request,
};
use futures_util::future::BoxFuture;
use futures_core::future::BoxFuture;
use tower::{Service, Layer};
use std::task::{Context, Poll};
@ -557,7 +557,7 @@ let app_with_middleware = middleware.layer(app);
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app_with_middleware.into_make_service()).await.unwrap();
axum::serve(listener, app_with_middleware.into_make_service()).await;
# };
```

View File

@ -43,7 +43,7 @@ let app = Router::new().fallback(handler);
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
axum::serve(listener, app).await;
# };
```
@ -56,6 +56,6 @@ async fn handler() {}
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, handler.into_make_service()).await.unwrap();
axum::serve(listener, handler.into_make_service()).await;
# };
```

View File

@ -22,7 +22,7 @@ async fn handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await;
# };
```
@ -60,7 +60,7 @@ impl Connected<IncomingStream<'_, TcpListener>> for MyConnectInfo {
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<MyConnectInfo>()).await.unwrap();
axum::serve(listener, app.into_make_service_with_connect_info::<MyConnectInfo>()).await;
# };
```

View File

@ -27,7 +27,7 @@ async fn main() {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();
axum::serve(listener, router).await;
}
```

View File

@ -36,8 +36,7 @@ documentation for more details.
It is not possible to create segments that only match some types like numbers or
regular expression. You must handle that manually in your handlers.
[`MatchedPath`](crate::extract::MatchedPath) can be used to extract the matched
path rather than the actual path.
[`MatchedPath`] can be used to extract the matched path rather than the actual path.
# Wildcards

View File

@ -15,7 +15,7 @@ let routes = Router::new()
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();
axum::serve(listener, routes).await;
# };
```
@ -41,7 +41,7 @@ let routes = routes().with_state(AppState {});
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();
axum::serve(listener, routes).await;
# };
```
@ -64,7 +64,7 @@ let routes = routes(AppState {});
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();
axum::serve(listener, routes).await;
# };
```
@ -91,7 +91,7 @@ let routes = Router::new().nest("/api", routes(AppState {}));
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, routes).await.unwrap();
axum::serve(listener, routes).await;
# };
```
@ -124,7 +124,7 @@ let router: Router<()> = router.with_state(AppState {});
// because it is still missing an `AppState`.
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, router).await.unwrap();
axum::serve(listener, router).await;
# };
```
@ -153,7 +153,7 @@ let final_router: Router<()> = string_router.with_state("foo".to_owned());
// Since we have a `Router<()>` we can run it.
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, final_router).await.unwrap();
axum::serve(listener, final_router).await;
# };
```
@ -179,7 +179,7 @@ let app = routes(AppState {});
// but `app` is a `Router<AppState>`
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
axum::serve(listener, app).await;
# };
```
@ -202,7 +202,7 @@ let app = routes(AppState {});
// We can now call `Router::into_make_service`
# async {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
axum::serve(listener, app).await;
# };
```

View File

@ -30,12 +30,6 @@ pub struct IntoMakeServiceWithConnectInfo<S, C> {
_connect_info: PhantomData<fn() -> C>,
}
#[test]
fn traits() {
use crate::test_helpers::*;
assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>();
}
impl<S, C> IntoMakeServiceWithConnectInfo<S, C> {
pub(crate) fn new(svc: S) -> Self {
Self {
@ -85,28 +79,19 @@ pub trait Connected<T>: Clone + Send + Sync + 'static {
#[cfg(all(feature = "tokio", any(feature = "http1", feature = "http2")))]
const _: () = {
use crate::serve;
use tokio::net::TcpListener;
impl Connected<serve::IncomingStream<'_, TcpListener>> for SocketAddr {
fn connect_info(stream: serve::IncomingStream<'_, TcpListener>) -> Self {
*stream.remote_addr()
}
}
impl<'a, L, F> Connected<serve::IncomingStream<'a, serve::TapIo<L, F>>> for L::Addr
impl<L> Connected<serve::IncomingStream<'_, L>> for SocketAddr
where
L: serve::Listener,
L::Addr: Clone + Sync + 'static,
F: FnMut(&mut L::Io) + Send + 'static,
L: serve::Listener<Addr = Self>,
{
fn connect_info(stream: serve::IncomingStream<'a, serve::TapIo<L, F>>) -> Self {
stream.remote_addr().clone()
fn connect_info(stream: serve::IncomingStream<'_, L>) -> Self {
*stream.remote_addr()
}
}
};
impl Connected<SocketAddr> for SocketAddr {
fn connect_info(remote_addr: SocketAddr) -> Self {
impl Connected<Self> for SocketAddr {
fn connect_info(remote_addr: Self) -> Self {
remote_addr
}
}
@ -234,8 +219,90 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::{routing::get, serve::IncomingStream, test_helpers::TestClient, Router};
use tokio::net::TcpListener;
use crate::{
extract::connect_info::Connected, routing::get, serve::IncomingStream, serve::Listener,
test_helpers::TestClient, Router,
};
use tokio::net::{TcpListener, TcpStream};
#[test]
fn into_make_service_traits() {
use crate::test_helpers::*;
assert_send::<IntoMakeServiceWithConnectInfo<(), NotSendSync>>();
}
#[allow(dead_code)]
#[allow(clippy::todo)]
fn connected_traits() {
// Test that the `Connected` trait can be used with custom address and listener types.
fn create_router() -> Router {
todo!()
}
fn tcp_listener() -> TcpListener {
todo!()
}
#[derive(Clone)]
struct CustomAddr(SocketAddr);
impl Connected<IncomingStream<'_, TcpListener>> for CustomAddr {
fn connect_info(_stream: IncomingStream<'_, TcpListener>) -> Self {
todo!()
}
}
impl Connected<IncomingStream<'_, CustomListener>> for CustomAddr {
fn connect_info(_stream: IncomingStream<'_, CustomListener>) -> Self {
todo!()
}
}
struct CustomListener {}
impl Listener for CustomListener {
type Io = TcpStream;
type Addr = SocketAddr;
async fn accept(&mut self) -> (Self::Io, Self::Addr) {
todo!()
}
fn local_addr(&self) -> tokio::io::Result<Self::Addr> {
todo!()
}
}
fn custom_connected() {
let router = create_router();
let _ = crate::serve(
tcp_listener(),
router.into_make_service_with_connect_info::<CustomAddr>(),
);
}
fn custom_listener() {
let router = create_router();
let _ = crate::serve(CustomListener {}, router.into_make_service());
}
fn custom_listener_with_connect() {
let router = create_router();
let _ = crate::serve(
CustomListener {},
router.into_make_service_with_connect_info::<SocketAddr>(),
);
}
fn custom_listener_with_custom_connect() {
let router = create_router();
let _ = crate::serve(
CustomListener {},
router.into_make_service_with_connect_info::<CustomAddr>(),
);
}
}
#[crate::test]
async fn socket_addr() {
@ -254,8 +321,7 @@ mod tests {
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
.await;
});
rx.await.unwrap();
@ -296,8 +362,7 @@ mod tests {
listener,
app.into_make_service_with_connect_info::<MyConnectInfo>(),
)
.await
.unwrap();
.await;
});
rx.await.unwrap();
@ -343,8 +408,7 @@ mod tests {
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.unwrap();
.await;
});
let client = reqwest::Client::new();

View File

@ -103,9 +103,7 @@ pub(crate) fn set_matched_path_for_request(
route_id_to_path: &HashMap<RouteId, Arc<str>>,
extensions: &mut http::Extensions,
) {
let matched_path = if let Some(matched_path) = route_id_to_path.get(&id) {
matched_path
} else {
let Some(matched_path) = route_id_to_path.get(&id) else {
#[cfg(debug_assertions)]
panic!("should always have a matched path for a route id");
#[cfg(not(debug_assertions))]

View File

@ -81,15 +81,11 @@ pub use self::ws::WebSocketUpgrade;
// this is duplicated in `axum-extra/src/extract/form.rs`
pub(super) fn has_content_type(headers: &HeaderMap, expected_content_type: &mime::Mime) -> bool {
let content_type = if let Some(content_type) = headers.get(header::CONTENT_TYPE) {
content_type
} else {
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
return false;
};
let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
let Ok(content_type) = content_type.to_str() else {
return false;
};

View File

@ -10,7 +10,7 @@ use axum_core::{
response::{IntoResponse, Response},
RequestExt,
};
use futures_util::stream::Stream;
use futures_core::Stream;
use http::{
header::{HeaderMap, CONTENT_TYPE},
StatusCode,

View File

@ -47,6 +47,7 @@ impl NestedPath {
}
}
#[diagnostic::do_not_recommend] // pretty niche type
impl<S> FromRequestParts<S> for NestedPath
where
S: Send + Sync,

View File

@ -76,7 +76,7 @@ where
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let uri = Extension::<Self>::from_request_parts(parts, state)
.await
.unwrap_or_else(|_| Extension(OriginalUri(parts.uri.clone())))
.unwrap_or_else(|_| Extension(Self(parts.uri.clone())))
.0;
Ok(uri)
}

View File

@ -639,8 +639,7 @@ enum KeyOrIdx<'de> {
impl<'de> KeyOrIdx<'de> {
fn key(&self) -> &'de str {
match &self {
Self::Key(key) => key,
Self::Idx { key, .. } => key,
Self::Idx { key, .. } | Self::Key(key) => key,
}
}
}

View File

@ -183,7 +183,7 @@ where
}
match T::deserialize(de::PathDeserializer::new(get_params(parts)?)) {
Ok(val) => Ok(Path(val)),
Ok(val) => Ok(Self(val)),
Err(e) => Err(failed_to_deserialize_path_params(e)),
}
}
@ -357,9 +357,9 @@ pub enum ErrorKind {
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ErrorKind::Message(error) => error.fmt(f),
ErrorKind::InvalidUtf8InPathParam { key } => write!(f, "Invalid UTF-8 in `{key}`"),
ErrorKind::WrongNumberOfParameters { got, expected } => {
Self::Message(error) => error.fmt(f),
Self::InvalidUtf8InPathParam { key } => write!(f, "Invalid UTF-8 in `{key}`"),
Self::WrongNumberOfParameters { got, expected } => {
write!(
f,
"Wrong number of path arguments for `Path`. Expected {expected} but got {got}"
@ -371,8 +371,8 @@ impl fmt::Display for ErrorKind {
Ok(())
}
ErrorKind::UnsupportedType { name } => write!(f, "Unsupported type `{name}`"),
ErrorKind::ParseErrorAtKey {
Self::UnsupportedType { name } => write!(f, "Unsupported type `{name}`"),
Self::ParseErrorAtKey {
key,
value,
expected_type,
@ -380,11 +380,11 @@ impl fmt::Display for ErrorKind {
f,
"Cannot parse `{key}` with value `{value}` to a `{expected_type}`"
),
ErrorKind::ParseError {
Self::ParseError {
value,
expected_type,
} => write!(f, "Cannot parse `{value}` to a `{expected_type}`"),
ErrorKind::ParseErrorAtIndex {
Self::ParseErrorAtIndex {
index,
value,
expected_type,
@ -392,7 +392,7 @@ impl fmt::Display for ErrorKind {
f,
"Cannot parse value at index {index} with value `{value}` to a `{expected_type}`"
),
ErrorKind::DeserializeError {
Self::DeserializeError {
key,
value,
message,
@ -772,7 +772,7 @@ mod tests {
D: serde::Deserializer<'de>,
{
let s = <&str as serde::Deserialize>::deserialize(deserializer)?;
Ok(Param(s.to_owned()))
Ok(Self(s.to_owned()))
}
}

View File

@ -36,16 +36,6 @@ use serde_core::de::DeserializeOwned;
///
/// 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
///
/// For handling multiple values for the same query parameter, in a `?foo=1&foo=2&foo=3`
/// fashion, use [`axum_extra::extract::Query`] instead.
///
/// [`axum_extra::extract::Query`]: https://docs.rs/axum-extra/latest/axum_extra/extract/struct.Query.html
#[cfg_attr(docsrs, doc(cfg(feature = "query")))]
#[derive(Debug, Clone, Copy, Default)]
pub struct Query<T>(pub T);
@ -88,10 +78,10 @@ where
pub fn try_from_uri(value: &Uri) -> Result<Self, QueryRejection> {
let query = value.query().unwrap_or_default();
let deserializer =
serde_urlencoded::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
serde_html_form::Deserializer::new(form_urlencoded::parse(query.as_bytes()));
let params = serde_path_to_error::deserialize(deserializer)
.map_err(FailedToDeserializeQueryString::from_err)?;
Ok(Query(params))
Ok(Self(params))
}
}

View File

@ -157,7 +157,7 @@ use std::{
///
/// # async {
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, handler_with_state.into_make_service()).await.unwrap();
/// axum::serve(listener, handler_with_state.into_make_service()).await;
/// # };
/// ```
///

View File

@ -94,10 +94,9 @@ use self::rejection::*;
use super::FromRequestParts;
use crate::{body::Bytes, response::Response, Error};
use axum_core::body::Body;
use futures_util::{
sink::{Sink, SinkExt},
stream::{FusedStream, Stream, StreamExt},
};
use futures_core::{FusedStream, Stream};
use futures_sink::Sink;
use futures_util::{sink::SinkExt, stream::StreamExt};
use http::{
header::{self, HeaderMap, HeaderName, HeaderValue},
request::Parts,
@ -107,8 +106,10 @@ use hyper_util::rt::TokioIo;
use sha1::{Digest, Sha1};
use std::{
borrow::Cow,
collections::BTreeSet,
future::Future,
pin::Pin,
str,
task::{ready, Context, Poll},
};
use tokio_tungstenite::{
@ -138,7 +139,7 @@ pub struct WebSocketUpgrade<F = DefaultOnFailedUpgrade> {
sec_websocket_key: Option<HeaderValue>,
on_upgrade: hyper::upgrade::OnUpgrade,
on_failed_upgrade: F,
sec_websocket_protocol: Option<HeaderValue>,
sec_websocket_protocol: BTreeSet<HeaderValue>,
}
impl<F> std::fmt::Debug for WebSocketUpgrade<F> {
@ -242,35 +243,61 @@ impl<F> WebSocketUpgrade<F> {
I: IntoIterator,
I::Item: Into<Cow<'static, str>>,
{
if let Some(req_protocols) = self
.sec_websocket_protocol
.as_ref()
.and_then(|p| p.to_str().ok())
{
self.protocol = protocols
.into_iter()
// FIXME: This will often allocate a new `String` and so is less efficient than it
// could be. But that can't be fixed without breaking changes to the public API.
.map(Into::into)
.find(|protocol| {
req_protocols
.split(',')
.any(|req_protocol| req_protocol.trim() == protocol)
})
.map(|protocol| match protocol {
Cow::Owned(s) => HeaderValue::from_str(&s).unwrap(),
Cow::Borrowed(s) => HeaderValue::from_static(s),
});
}
self.protocol = protocols
.into_iter()
.map(Into::into)
.find(|proto| {
// FIXME: When https://github.com/hyperium/http/pull/814
// is merged + released, we can look use
// `contains(proto.as_bytes())` without converting
// to `HeaderValue` first.
let Ok(proto) = HeaderValue::from_str(proto) else {
return false;
};
self.sec_websocket_protocol.contains(&proto)
})
.map(|protocol| match protocol {
Cow::Owned(s) => HeaderValue::from_str(&s).unwrap(),
Cow::Borrowed(s) => HeaderValue::from_static(s),
});
self
}
/// Return the WebSocket subprotocols requested by the client.
///
/// # Examples
///
/// If the client sends the following HTTP header in the WebSocket upgrade request:
///
/// ```txt
/// Sec-WebSocket-Protocol: soap, wamp
/// ```
///
/// this method returns an iterator yielding `"soap"` and `"wamp"`.
pub fn requested_protocols(&self) -> impl Iterator<Item = &HeaderValue> {
self.sec_websocket_protocol.iter()
}
/// Set the chosen WebSocket subprotocol.
///
/// Another method, [`protocols()`][Self::protocols], also sets the chosen WebSocket
/// subprotocol. If both methods are called, only the latter call takes effect.
///
/// # Notes
///
/// - The chosen protocol is echoed back in the WebSocket upgrade
/// response as required by RFC 6455. Some browsers may reject a
/// value that was not present in the client's request.
pub fn set_selected_protocol(&mut self, protocol: HeaderValue) {
self.protocol = Some(protocol);
}
/// Return the selected WebSocket subprotocol, if one has been chosen.
///
/// If [`protocols()`][Self::protocols] has been called and a matching
/// protocol has been selected, the return value will be `Some` containing
/// said protocol. Otherwise, it will be `None`.
/// If [`protocols()`][Self::protocols] selects a matching protocol, or
/// [`set_selected_protocol()`][Self::set_selected_protocol] has been called, the return
/// value will be `Some` containing the selected protocol. Otherwise, it will be `None`.
pub fn selected_protocol(&self) -> Option<&HeaderValue> {
self.protocol.as_ref()
}
@ -424,11 +451,11 @@ where
return Err(MethodNotGet.into());
}
if !header_contains(&parts.headers, header::CONNECTION, "upgrade") {
if !header_contains(&parts.headers, &header::CONNECTION, "upgrade") {
return Err(InvalidConnectionHeader.into());
}
if !header_eq(&parts.headers, header::UPGRADE, "websocket") {
if !header_eq(&parts.headers, &header::UPGRADE, "websocket") {
return Err(InvalidUpgradeHeader.into());
}
@ -458,7 +485,7 @@ where
None
};
if !header_eq(&parts.headers, header::SEC_WEBSOCKET_VERSION, "13") {
if !header_eq(&parts.headers, &header::SEC_WEBSOCKET_VERSION, "13") {
return Err(InvalidWebSocketVersionHeader.into());
}
@ -467,7 +494,16 @@ where
.remove::<hyper::upgrade::OnUpgrade>()
.ok_or(ConnectionNotUpgradable)?;
let sec_websocket_protocol = parts.headers.get(header::SEC_WEBSOCKET_PROTOCOL).cloned();
let sec_websocket_protocol = parts
.headers
.get_all(header::SEC_WEBSOCKET_PROTOCOL)
.iter()
.flat_map(|val| val.as_bytes().split(|&b| b == b','))
.map(|proto| {
HeaderValue::from_bytes(proto.trim_ascii())
.expect("substring of HeaderValue is valid HeaderValue")
})
.collect();
Ok(Self {
config: Default::default(),
@ -480,18 +516,16 @@ where
}
}
fn header_eq(headers: &HeaderMap, key: HeaderName, value: &'static str) -> bool {
if let Some(header) = headers.get(&key) {
fn header_eq(headers: &HeaderMap, key: &HeaderName, value: &'static str) -> bool {
if let Some(header) = headers.get(key) {
header.as_bytes().eq_ignore_ascii_case(value.as_bytes())
} else {
false
}
}
fn header_contains(headers: &HeaderMap, key: HeaderName, value: &'static str) -> bool {
let header = if let Some(header) = headers.get(&key) {
header
} else {
fn header_contains(headers: &HeaderMap, key: &HeaderName, value: &'static str) -> bool {
let Some(header) = headers.get(key) else {
return false;
};
@ -842,49 +876,49 @@ impl Message {
}
/// Create a new text WebSocket message from a stringable.
pub fn text<S>(string: S) -> Message
pub fn text<S>(string: S) -> Self
where
S: Into<Utf8Bytes>,
{
Message::Text(string.into())
Self::Text(string.into())
}
/// Create a new binary WebSocket message by converting to `Bytes`.
pub fn binary<B>(bin: B) -> Message
pub fn binary<B>(bin: B) -> Self
where
B: Into<Bytes>,
{
Message::Binary(bin.into())
Self::Binary(bin.into())
}
}
impl From<String> for Message {
fn from(string: String) -> Self {
Message::Text(string.into())
Self::Text(string.into())
}
}
impl<'s> From<&'s str> for Message {
fn from(string: &'s str) -> Self {
Message::Text(string.into())
Self::Text(string.into())
}
}
impl<'b> From<&'b [u8]> for Message {
fn from(data: &'b [u8]) -> Self {
Message::Binary(Bytes::copy_from_slice(data))
Self::Binary(Bytes::copy_from_slice(data))
}
}
impl From<Bytes> for Message {
fn from(data: Bytes) -> Self {
Message::Binary(data)
Self::Binary(data)
}
}
impl From<Vec<u8>> for Message {
fn from(data: Vec<u8>) -> Self {
Message::Binary(data.into())
Self::Binary(data.into())
}
}

View File

@ -1,6 +1,6 @@
use crate::extract::Request;
use crate::extract::{rejection::*, FromRequest, RawForm};
use axum_core::response::{IntoResponse, Response};
use axum_core::response::{IntoResponse, IntoResponseFailed, Response};
use axum_core::RequestExt;
use http::header::CONTENT_TYPE;
use http::StatusCode;
@ -84,7 +84,7 @@ where
match req.extract().await {
Ok(RawForm(bytes)) => {
let deserializer =
serde_urlencoded::Deserializer::new(form_urlencoded::parse(&bytes));
serde_html_form::Deserializer::new(form_urlencoded::parse(&bytes));
let value = serde_path_to_error::deserialize(deserializer).map_err(
|err| -> FormRejection {
if is_get_or_head {
@ -94,7 +94,7 @@ where
}
},
)?;
Ok(Form(value))
Ok(Self(value))
}
Err(RawFormRejection::BytesRejection(r)) => Err(FormRejection::BytesRejection(r)),
Err(RawFormRejection::InvalidFormContentType(r)) => {
@ -110,18 +110,23 @@ where
{
fn into_response(self) -> Response {
// Extracted into separate fn so it's only compiled once for all T.
fn make_response(ser_result: Result<String, serde_urlencoded::ser::Error>) -> Response {
fn make_response(ser_result: Result<String, serde_html_form::ser::Error>) -> Response {
match ser_result {
Ok(body) => (
[(CONTENT_TYPE, mime::APPLICATION_WWW_FORM_URLENCODED.as_ref())],
body,
)
.into_response(),
Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
IntoResponseFailed,
err.to_string(),
)
.into_response(),
}
}
make_response(serde_urlencoded::to_string(&self.0))
make_response(serde_html_form::to_string(&self.0))
}
}
axum_core::__impl_deref!(Form);
@ -160,7 +165,7 @@ mod tests {
.uri("http://example.com/test")
.method(Method::POST)
.header(CONTENT_TYPE, APPLICATION_WWW_FORM_URLENCODED.as_ref())
.body(Body::from(serde_urlencoded::to_string(&value).unwrap()))
.body(Body::from(serde_html_form::to_string(&value).unwrap()))
.unwrap();
assert_eq!(Form::<T>::from_request(req, &()).await.unwrap().0, value);
}
@ -223,7 +228,7 @@ mod tests {
.method(Method::POST)
.header(CONTENT_TYPE, mime::APPLICATION_JSON.as_ref())
.body(Body::from(
serde_urlencoded::to_string(&Pagination {
serde_html_form::to_string(&Pagination {
size: Some(10),
page: None,
})

View File

@ -204,6 +204,7 @@ pub trait Handler<T, S>: Clone + Send + Sync + Sized + 'static {
}
}
#[diagnostic::do_not_recommend]
impl<F, Fut, Res, S> Handler<((),), S> for F
where
F: FnOnce() -> Fut + Clone + Send + Sync + 'static,
@ -221,6 +222,7 @@ macro_rules! impl_handler {
(
[$($ty:ident),*], $last:ident
) => {
#[diagnostic::do_not_recommend]
#[allow(non_snake_case, unused_mut)]
impl<F, Fut, S, Res, M, $($ty,)* $last> Handler<(M, $($ty,)* $last,), S> for F
where
@ -265,6 +267,7 @@ mod private {
pub enum IntoResponseHandler {}
}
#[diagnostic::do_not_recommend]
impl<T, S> Handler<private::IntoResponseHandler, S> for T
where
T: IntoResponse + Clone + Send + Sync + 'static,
@ -310,6 +313,7 @@ where
}
}
#[diagnostic::do_not_recommend]
impl<H, S, T, L> Handler<T, S> for Layered<L, H, T, S>
where
L: Layer<HandlerService<H, T, S>> + Clone + Send + Sync + 'static,

View File

@ -55,12 +55,12 @@ impl<H, T, S> HandlerService<H, T, S> {
///
/// # async {
/// let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
/// axum::serve(listener, app.into_make_service()).await.unwrap();
/// axum::serve(listener, app.into_make_service()).await;
/// # };
/// ```
///
/// [`MakeService`]: tower::make::MakeService
pub fn into_make_service(self) -> IntoMakeService<HandlerService<H, T, S>> {
pub fn into_make_service(self) -> IntoMakeService<Self> {
IntoMakeService::new(self)
}
@ -94,16 +94,14 @@ impl<H, T, S> HandlerService<H, T, S> {
/// axum::serve(
/// listener,
/// app.into_make_service_with_connect_info::<SocketAddr>(),
/// ).await.unwrap();
/// ).await;
/// # };
/// ```
///
/// [`MakeService`]: tower::make::MakeService
/// [`Router::into_make_service_with_connect_info`]: crate::routing::Router::into_make_service_with_connect_info
#[cfg(feature = "tokio")]
pub fn into_make_service_with_connect_info<C>(
self,
) -> IntoMakeServiceWithConnectInfo<HandlerService<H, T, S>, C> {
pub fn into_make_service_with_connect_info<C>(self) -> IntoMakeServiceWithConnectInfo<Self, C> {
IntoMakeServiceWithConnectInfo::new(self)
}
}

View File

@ -1,7 +1,7 @@
use crate::extract::Request;
use crate::extract::{rejection::*, FromRequest};
use axum_core::extract::OptionalFromRequest;
use axum_core::response::{IntoResponse, Response};
use axum_core::response::{IntoResponse, IntoResponseFailed, Response};
use bytes::{BufMut, Bytes, BytesMut};
use http::{
header::{self, HeaderMap, HeaderValue},
@ -136,22 +136,14 @@ where
}
fn json_content_type(headers: &HeaderMap) -> bool {
let Some(content_type) = headers.get(header::CONTENT_TYPE) else {
return false;
};
let Ok(content_type) = content_type.to_str() else {
return false;
};
let Ok(mime) = content_type.parse::<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
headers
.get(header::CONTENT_TYPE)
.and_then(|content_type| content_type.to_str().ok())
.and_then(|content_type| content_type.parse::<mime::Mime>().ok())
.is_some_and(|mime| {
mime.type_() == "application"
&& (mime.subtype() == "json" || mime.suffix().is_some_and(|name| name == "json"))
})
}
axum_core::__impl_deref!(Json);
@ -224,6 +216,7 @@ where
header::CONTENT_TYPE,
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
)],
IntoResponseFailed,
err.to_string(),
)
.into_response(),

View File

@ -1,4 +1,4 @@
//! axum is a web application framework that focuses on ergonomics and modularity.
//! axum is an HTTP routing and request-handling library that focuses on ergonomics and modularity.
//!
//! # High-level features
//!
@ -9,7 +9,7 @@
//! - Take full advantage of the [`tower`] and [`tower-http`] ecosystem of
//! middleware, services, and utilities.
//!
//! In particular, the last point is what sets `axum` apart from other frameworks.
//! In particular, the last point is what sets `axum` apart from other libraries / frameworks.
//! `axum` doesn't have its own middleware system but instead uses
//! [`tower::Service`]. This means `axum` gets timeouts, tracing, compression,
//! authorization, and more, for free. It also enables you to share middleware with
@ -37,7 +37,7 @@
//!
//! // run our app with hyper, listening globally on port 3000
//! let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
//! axum::serve(listener, app).await.unwrap();
//! axum::serve(listener, app).await;
//! }
//! ```
//!
@ -455,6 +455,7 @@ pub mod serve;
#[cfg(any(test, feature = "__private"))]
#[allow(missing_docs, missing_debug_implementations, clippy::print_stdout)]
#[doc(hidden)]
pub mod test_helpers;
#[doc(no_inline)]

View File

@ -2,7 +2,7 @@ use crate::{
extract::FromRequestParts,
response::{IntoResponse, Response},
};
use futures_util::future::BoxFuture;
use futures_core::future::BoxFuture;
use http::Request;
use pin_project_lite::pin_project;
use std::{

Some files were not shown because too many files have changed in this diff Show More