mirror of
https://github.com/tokio-rs/axum.git
synced 2024-12-12 09:50:45 +01:00
Merge branch 'tokio-rs:main' into reverse-proxy-example-patch
This commit is contained in:
commit
e01086b179
23 changed files with 403 additions and 157 deletions
2
.github/workflows/CI.yml
vendored
2
.github/workflows/CI.yml
vendored
|
@ -227,4 +227,4 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check the spelling of the files in our repo
|
||||
uses: crate-ci/typos@v1.16.2
|
||||
uses: crate-ci/typos@v1.20.8
|
||||
|
|
|
@ -26,6 +26,7 @@ http-body = "1.0.0"
|
|||
http-body-util = "0.1.0"
|
||||
mime = "0.3.16"
|
||||
pin-project-lite = "0.2.7"
|
||||
rustversion = "1.0.9"
|
||||
sync_wrapper = "1.0.0"
|
||||
tower-layer = "0.3"
|
||||
tower-service = "0.3"
|
||||
|
@ -34,9 +35,6 @@ tower-service = "0.3"
|
|||
tower-http = { version = "0.5.0", optional = true, features = ["limit"] }
|
||||
tracing = { version = "0.1.37", default-features = false, optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
rustversion = "1.0.9"
|
||||
|
||||
[dev-dependencies]
|
||||
axum = { path = "../axum", version = "0.7.2" }
|
||||
axum-extra = { path = "../axum-extra", features = ["typed-header"] }
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#[rustversion::nightly]
|
||||
fn main() {
|
||||
println!("cargo:rustc-cfg=nightly_error_messages");
|
||||
}
|
||||
|
||||
#[rustversion::not(nightly)]
|
||||
fn main() {}
|
|
@ -43,8 +43,8 @@ mod private {
|
|||
///
|
||||
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
|
||||
#[async_trait]
|
||||
#[cfg_attr(
|
||||
nightly_error_messages,
|
||||
#[rustversion::attr(
|
||||
since(1.78),
|
||||
diagnostic::on_unimplemented(
|
||||
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/0.7/axum/extract/index.html` for details",
|
||||
)
|
||||
|
@ -70,8 +70,8 @@ pub trait FromRequestParts<S>: Sized {
|
|||
///
|
||||
/// [`axum::extract`]: https://docs.rs/axum/0.7/axum/extract/index.html
|
||||
#[async_trait]
|
||||
#[cfg_attr(
|
||||
nightly_error_messages,
|
||||
#[rustversion::attr(
|
||||
since(1.78),
|
||||
diagnostic::on_unimplemented(
|
||||
note = "Function argument is not a valid axum extractor. \nSee `https://docs.rs/axum/0.7/axum/extract/index.html` for details",
|
||||
)
|
||||
|
|
|
@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
# Unreleased
|
||||
|
||||
- None.
|
||||
- **added:** Add `#[debug_middleware]` ([#1993], [#2725])
|
||||
|
||||
[#1993]: https://github.com/tokio-rs/axum/pull/1993
|
||||
[#2725]: https://github.com/tokio-rs/axum/pull/2725
|
||||
|
||||
# 0.4.1 (13. January, 2024)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashSet;
|
||||
use std::{collections::HashSet, fmt};
|
||||
|
||||
use crate::{
|
||||
attr_parsing::{parse_assignment_attribute, second},
|
||||
|
@ -8,13 +8,13 @@ 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) -> 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);
|
||||
let check_path_extractor = check_path_extractor(&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)
|
||||
|
@ -37,8 +37,10 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
|||
err = Some(
|
||||
syn::Error::new(
|
||||
Span::call_site(),
|
||||
"can't infer state type, please add set it explicitly, as in \
|
||||
`#[debug_handler(state = MyStateType)]`",
|
||||
format!(
|
||||
"can't infer state type, please add set it explicitly, as in \
|
||||
`#[axum_macros::debug_{kind}(state = MyStateType)]`"
|
||||
),
|
||||
)
|
||||
.into_compile_error(),
|
||||
);
|
||||
|
@ -48,16 +50,16 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
|||
err.unwrap_or_else(|| {
|
||||
let state_ty = state_ty.unwrap_or_else(|| syn::parse_quote!(()));
|
||||
|
||||
let check_future_send = check_future_send(&item_fn);
|
||||
let check_future_send = check_future_send(&item_fn, kind);
|
||||
|
||||
if let Some(check_input_order) = check_input_order(&item_fn) {
|
||||
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);
|
||||
check_inputs_impls_from_request(&item_fn, state_ty, kind);
|
||||
|
||||
quote! {
|
||||
#check_inputs_impls_from_request
|
||||
|
@ -68,17 +70,45 @@ pub(crate) fn expand(attr: Attrs, item_fn: ItemFn) -> TokenStream {
|
|||
} else {
|
||||
syn::Error::new_spanned(
|
||||
&item_fn.sig.generics,
|
||||
"`#[axum_macros::debug_handler]` doesn't support generic functions",
|
||||
format!("`#[axum_macros::debug_{kind}]` doesn't support generic functions"),
|
||||
)
|
||||
.into_compile_error()
|
||||
};
|
||||
|
||||
let middleware_takes_next_as_last_arg =
|
||||
matches!(kind, FunctionKind::Middleware).then(|| next_is_last_input(&item_fn));
|
||||
|
||||
quote! {
|
||||
#item_fn
|
||||
#check_extractor_count
|
||||
#check_path_extractor
|
||||
#check_output_impls_into_response
|
||||
#check_inputs_and_future_send
|
||||
#middleware_takes_next_as_last_arg
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum FunctionKind {
|
||||
Handler,
|
||||
Middleware,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FunctionKind {
|
||||
fn name_uppercase_plural(&self) -> &'static str {
|
||||
match self {
|
||||
FunctionKind::Handler => "Handlers",
|
||||
FunctionKind::Middleware => "Middleware",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,25 +140,36 @@ impl Parse for Attrs {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_extractor_count(item_fn: &ItemFn) -> Option<TokenStream> {
|
||||
fn check_extractor_count(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
|
||||
let max_extractors = 16;
|
||||
if item_fn.sig.inputs.len() <= max_extractors {
|
||||
let inputs = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind))
|
||||
.count();
|
||||
if inputs <= max_extractors {
|
||||
None
|
||||
} else {
|
||||
let error_message = format!(
|
||||
"Handlers cannot take more than {max_extractors} arguments. \
|
||||
"{} cannot take more than {max_extractors} arguments. \
|
||||
Use `(a, b): (ExtractorA, ExtractorA)` to further nest extractors",
|
||||
kind.name_uppercase_plural(),
|
||||
);
|
||||
let error = syn::Error::new_spanned(&item_fn.sig.inputs, error_message).to_compile_error();
|
||||
Some(error)
|
||||
}
|
||||
}
|
||||
|
||||
fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
|
||||
fn extractor_idents(
|
||||
item_fn: &ItemFn,
|
||||
kind: FunctionKind,
|
||||
) -> impl Iterator<Item = (usize, &syn::FnArg, &syn::Ident)> {
|
||||
item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(move |arg| skip_next_arg(arg, kind))
|
||||
.enumerate()
|
||||
.filter_map(|(idx, fn_arg)| match fn_arg {
|
||||
FnArg::Receiver(_) => None,
|
||||
|
@ -146,8 +187,8 @@ fn extractor_idents(item_fn: &ItemFn) -> impl Iterator<Item = (usize, &syn::FnAr
|
|||
})
|
||||
}
|
||||
|
||||
fn check_path_extractor(item_fn: &ItemFn) -> TokenStream {
|
||||
let path_extractors = extractor_idents(item_fn)
|
||||
fn check_path_extractor(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
let path_extractors = extractor_idents(item_fn, kind)
|
||||
.filter(|(_, _, ident)| *ident == "Path")
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -179,113 +220,122 @@ fn is_self_pat_type(typed: &syn::PatType) -> bool {
|
|||
ident == "self"
|
||||
}
|
||||
|
||||
fn check_inputs_impls_from_request(item_fn: &ItemFn, state_ty: Type) -> TokenStream {
|
||||
fn check_inputs_impls_from_request(
|
||||
item_fn: &ItemFn,
|
||||
state_ty: Type,
|
||||
kind: FunctionKind,
|
||||
) -> TokenStream {
|
||||
let takes_self = item_fn.sig.inputs.first().map_or(false, |arg| match arg {
|
||||
FnArg::Receiver(_) => true,
|
||||
FnArg::Typed(typed) => is_self_pat_type(typed),
|
||||
});
|
||||
|
||||
WithPosition::new(&item_fn.sig.inputs)
|
||||
.enumerate()
|
||||
.map(|(idx, arg)| {
|
||||
let must_impl_from_request_parts = match &arg {
|
||||
Position::First(_) | Position::Middle(_) => true,
|
||||
Position::Last(_) | Position::Only(_) => false,
|
||||
};
|
||||
WithPosition::new(
|
||||
item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind)),
|
||||
)
|
||||
.enumerate()
|
||||
.map(|(idx, arg)| {
|
||||
let must_impl_from_request_parts = match &arg {
|
||||
Position::First(_) | Position::Middle(_) => true,
|
||||
Position::Last(_) | Position::Only(_) => false,
|
||||
};
|
||||
|
||||
let arg = arg.into_inner();
|
||||
let arg = arg.into_inner();
|
||||
|
||||
let (span, ty) = match arg {
|
||||
FnArg::Receiver(receiver) => {
|
||||
if receiver.reference.is_some() {
|
||||
return syn::Error::new_spanned(
|
||||
receiver,
|
||||
"Handlers must only take owned values",
|
||||
)
|
||||
.into_compile_error();
|
||||
}
|
||||
let (span, ty) = match arg {
|
||||
FnArg::Receiver(receiver) => {
|
||||
if receiver.reference.is_some() {
|
||||
return syn::Error::new_spanned(
|
||||
receiver,
|
||||
"Handlers must only take owned values",
|
||||
)
|
||||
.into_compile_error();
|
||||
}
|
||||
|
||||
let span = receiver.span();
|
||||
let span = receiver.span();
|
||||
(span, syn::parse_quote!(Self))
|
||||
}
|
||||
FnArg::Typed(typed) => {
|
||||
let ty = &typed.ty;
|
||||
let span = ty.span();
|
||||
|
||||
if is_self_pat_type(typed) {
|
||||
(span, syn::parse_quote!(Self))
|
||||
}
|
||||
FnArg::Typed(typed) => {
|
||||
let ty = &typed.ty;
|
||||
let span = ty.span();
|
||||
|
||||
if is_self_pat_type(typed) {
|
||||
(span, syn::parse_quote!(Self))
|
||||
} else {
|
||||
(span, ty.clone())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let consumes_request = request_consuming_type_name(&ty).is_some();
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_call_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn_body = if takes_self {
|
||||
quote_spanned! {span=>
|
||||
Self::#check_fn();
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#check_fn();
|
||||
}
|
||||
};
|
||||
|
||||
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { <M> }
|
||||
};
|
||||
|
||||
let from_request_bound = if must_impl_from_request_parts {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
|
||||
}
|
||||
} else if consumes_request {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty> + Send
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
|
||||
}
|
||||
};
|
||||
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn #check_fn_generics()
|
||||
where
|
||||
#from_request_bound,
|
||||
{}
|
||||
|
||||
// we have to call the function to actually trigger a compile error
|
||||
// since the function is generic, just defining it is not enough
|
||||
#[allow(warnings)]
|
||||
#[allow(unreachable_code)]
|
||||
#[doc(hidden)]
|
||||
fn #call_check_fn() {
|
||||
#call_check_fn_body
|
||||
} else {
|
||||
(span, ty.clone())
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>()
|
||||
};
|
||||
|
||||
let consumes_request = request_consuming_type_name(&ty).is_some();
|
||||
|
||||
let check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn = format_ident!(
|
||||
"__axum_macros_check_{}_{}_from_request_call_check",
|
||||
item_fn.sig.ident,
|
||||
idx,
|
||||
span = span,
|
||||
);
|
||||
|
||||
let call_check_fn_body = if takes_self {
|
||||
quote_spanned! {span=>
|
||||
Self::#check_fn();
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#check_fn();
|
||||
}
|
||||
};
|
||||
|
||||
let check_fn_generics = if must_impl_from_request_parts || consumes_request {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { <M> }
|
||||
};
|
||||
|
||||
let from_request_bound = if must_impl_from_request_parts {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequestParts<#state_ty> + Send
|
||||
}
|
||||
} else if consumes_request {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty> + Send
|
||||
}
|
||||
} else {
|
||||
quote_spanned! {span=>
|
||||
#ty: ::axum::extract::FromRequest<#state_ty, M> + Send
|
||||
}
|
||||
};
|
||||
|
||||
quote_spanned! {span=>
|
||||
#[allow(warnings)]
|
||||
#[doc(hidden)]
|
||||
fn #check_fn #check_fn_generics()
|
||||
where
|
||||
#from_request_bound,
|
||||
{}
|
||||
|
||||
// we have to call the function to actually trigger a compile error
|
||||
// since the function is generic, just defining it is not enough
|
||||
#[allow(warnings)]
|
||||
#[doc(hidden)]
|
||||
fn #call_check_fn()
|
||||
{
|
||||
#call_check_fn_body
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<TokenStream>()
|
||||
}
|
||||
|
||||
fn check_output_tuples(item_fn: &ItemFn) -> TokenStream {
|
||||
|
@ -445,11 +495,19 @@ fn check_into_response_parts(ty: &Type, ident: &Ident, index: usize) -> TokenStr
|
|||
}
|
||||
}
|
||||
|
||||
fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
|
||||
fn check_input_order(item_fn: &ItemFn, kind: FunctionKind) -> Option<TokenStream> {
|
||||
let number_of_inputs = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind))
|
||||
.count();
|
||||
|
||||
let types_that_consume_the_request = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter(|arg| skip_next_arg(arg, kind))
|
||||
.enumerate()
|
||||
.filter_map(|(idx, arg)| {
|
||||
let ty = match arg {
|
||||
|
@ -469,7 +527,7 @@ fn check_input_order(item_fn: &ItemFn) -> Option<TokenStream> {
|
|||
// exactly one type that consumes the request
|
||||
if types_that_consume_the_request.len() == 1 {
|
||||
// and that is not the last
|
||||
if types_that_consume_the_request[0].0 != item_fn.sig.inputs.len() - 1 {
|
||||
if types_that_consume_the_request[0].0 != number_of_inputs - 1 {
|
||||
let (_idx, type_name, span) = &types_that_consume_the_request[0];
|
||||
let error = format!(
|
||||
"`{type_name}` consumes the request body and thus must be \
|
||||
|
@ -653,13 +711,13 @@ fn check_output_impls_into_response(item_fn: &ItemFn) -> TokenStream {
|
|||
}
|
||||
}
|
||||
|
||||
fn check_future_send(item_fn: &ItemFn) -> TokenStream {
|
||||
fn check_future_send(item_fn: &ItemFn, kind: FunctionKind) -> TokenStream {
|
||||
if item_fn.sig.asyncness.is_none() {
|
||||
match &item_fn.sig.output {
|
||||
syn::ReturnType::Default => {
|
||||
return syn::Error::new_spanned(
|
||||
item_fn.sig.fn_token,
|
||||
"Handlers must be `async fn`s",
|
||||
format!("{} must be `async fn`s", kind.name_uppercase_plural()),
|
||||
)
|
||||
.into_compile_error();
|
||||
}
|
||||
|
@ -763,7 +821,69 @@ fn state_types_from_args(item_fn: &ItemFn) -> HashSet<Type> {
|
|||
crate::infer_state_types(types).collect()
|
||||
}
|
||||
|
||||
fn next_is_last_input(item_fn: &ItemFn) -> TokenStream {
|
||||
let next_args = item_fn
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, arg)| !skip_next_arg(arg, FunctionKind::Middleware))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if next_args.is_empty() {
|
||||
return quote! {
|
||||
compile_error!(
|
||||
"Middleware functions must take `axum::middleware::Next` as the last argument",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
if next_args.len() == 1 {
|
||||
let (idx, arg) = &next_args[0];
|
||||
if *idx != item_fn.sig.inputs.len() - 1 {
|
||||
return quote_spanned! {arg.span()=>
|
||||
compile_error!("`axum::middleware::Next` must the last argument");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if next_args.len() >= 2 {
|
||||
return quote! {
|
||||
compile_error!(
|
||||
"Middleware functions can only take one argument of type `axum::middleware::Next`",
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
quote! {}
|
||||
}
|
||||
|
||||
fn skip_next_arg(arg: &FnArg, kind: FunctionKind) -> bool {
|
||||
match kind {
|
||||
FunctionKind::Handler => true,
|
||||
FunctionKind::Middleware => match arg {
|
||||
FnArg::Receiver(_) => true,
|
||||
FnArg::Typed(pat_type) => {
|
||||
if let Type::Path(type_path) = &*pat_type.ty {
|
||||
type_path
|
||||
.path
|
||||
.segments
|
||||
.last()
|
||||
.map_or(true, |path_segment| path_segment.ident != "Next")
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui() {
|
||||
fn ui_debug_handler() {
|
||||
crate::run_ui_tests("debug_handler");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_debug_middleware() {
|
||||
crate::run_ui_tests("debug_middleware");
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
#![cfg_attr(test, allow(clippy::float_cmp))]
|
||||
#![cfg_attr(not(test), warn(clippy::print_stdout, clippy::dbg_macro))]
|
||||
|
||||
use debug_handler::FunctionKind;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{parse::Parse, Type};
|
||||
|
@ -464,7 +465,7 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
|||
expand_with(item, |item| from_request::expand(item, FromRequestParts))
|
||||
}
|
||||
|
||||
/// Generates better error messages when applied handler functions.
|
||||
/// Generates better error messages when applied to handler functions.
|
||||
///
|
||||
/// While using [`axum`], you can get long error messages for simple mistakes. For example:
|
||||
///
|
||||
|
@ -515,17 +516,15 @@ pub fn derive_from_request_parts(item: TokenStream) -> TokenStream {
|
|||
///
|
||||
/// As the error message says, handler function needs to be async.
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use axum::{routing::get, Router, debug_handler};
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// # async {
|
||||
/// 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();
|
||||
/// # };
|
||||
/// }
|
||||
///
|
||||
/// #[debug_handler]
|
||||
|
@ -618,7 +617,65 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
|||
return input;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
return expand_attr_with(_attr, input, debug_handler::expand);
|
||||
return expand_attr_with(_attr, input, |attrs, item_fn| {
|
||||
debug_handler::expand(attrs, item_fn, FunctionKind::Handler)
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates better error messages when applied to middleware functions.
|
||||
///
|
||||
/// This works similarly to [`#[debug_handler]`](macro@debug_handler) except for middleware using
|
||||
/// [`axum::middleware::from_fn`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use axum::{
|
||||
/// routing::get,
|
||||
/// extract::Request,
|
||||
/// response::Response,
|
||||
/// Router,
|
||||
/// middleware::{self, Next},
|
||||
/// debug_middleware,
|
||||
/// };
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let app = Router::new()
|
||||
/// .route("/", get(|| async {}))
|
||||
/// .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();
|
||||
/// }
|
||||
///
|
||||
/// // if this wasn't a valid middleware function #[debug_middleware] would
|
||||
/// // improve compile error
|
||||
/// #[debug_middleware]
|
||||
/// async fn my_middleware(
|
||||
/// request: Request,
|
||||
/// next: Next,
|
||||
/// ) -> Response {
|
||||
/// next.run(request).await
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Performance
|
||||
///
|
||||
/// This macro has no effect when compiled with the release profile. (eg. `cargo build --release`)
|
||||
///
|
||||
/// [`axum`]: https://docs.rs/axum/latest
|
||||
/// [`axum::middleware::from_fn`]: https://docs.rs/axum/0.7/axum/middleware/fn.from_fn.html
|
||||
/// [`debug_middleware`]: macro@debug_middleware
|
||||
#[proc_macro_attribute]
|
||||
pub fn debug_middleware(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
#[cfg(not(debug_assertions))]
|
||||
return input;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
return expand_attr_with(_attr, input, |attrs, item_fn| {
|
||||
debug_handler::expand(attrs, item_fn, FunctionKind::Middleware)
|
||||
});
|
||||
}
|
||||
|
||||
/// Private API: Do no use this!
|
||||
|
|
13
axum-macros/tests/debug_middleware/fail/doesnt_take_next.rs
Normal file
13
axum-macros/tests/debug_middleware/fail/doesnt_take_next.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{
|
||||
debug_middleware,
|
||||
extract::Request,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(request: Request) -> Response {
|
||||
let _ = request;
|
||||
().into_response()
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,7 @@
|
|||
error: Middleware functions must take `axum::middleware::Next` as the last argument
|
||||
--> tests/debug_middleware/fail/doesnt_take_next.rs:7:1
|
||||
|
|
||||
7 | #[debug_middleware]
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `debug_middleware` (in Nightly builds, run with -Z macro-backtrace for more info)
|
13
axum-macros/tests/debug_middleware/fail/next_not_last.rs
Normal file
13
axum-macros/tests/debug_middleware/fail/next_not_last.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{
|
||||
extract::Request,
|
||||
response::Response,
|
||||
middleware::Next,
|
||||
debug_middleware,
|
||||
};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(next: Next, request: Request) -> Response {
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,5 @@
|
|||
error: `axum::middleware::Next` must the last argument
|
||||
--> tests/debug_middleware/fail/next_not_last.rs:9:24
|
||||
|
|
||||
9 | async fn my_middleware(next: Next, request: Request) -> Response {
|
||||
| ^^^^^^^^^^
|
|
@ -0,0 +1,9 @@
|
|||
use axum::{debug_middleware, extract::Request, middleware::Next, response::Response};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(request: Request, next: Next, next2: Next) -> Response {
|
||||
let _ = next2;
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -0,0 +1,7 @@
|
|||
error: Middleware functions can only take one argument of type `axum::middleware::Next`
|
||||
--> tests/debug_middleware/fail/takes_next_twice.rs:3:1
|
||||
|
|
||||
3 | #[debug_middleware]
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in the attribute macro `debug_middleware` (in Nightly builds, run with -Z macro-backtrace for more info)
|
13
axum-macros/tests/debug_middleware/pass/basic.rs
Normal file
13
axum-macros/tests/debug_middleware/pass/basic.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use axum::{
|
||||
extract::Request,
|
||||
response::Response,
|
||||
middleware::Next,
|
||||
debug_middleware,
|
||||
};
|
||||
|
||||
#[debug_middleware]
|
||||
async fn my_middleware(request: Request, next: Next) -> Response {
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -18,7 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
a `Router` or `MethodRouter` ([#2586])
|
||||
- **fixed:** `h2` is no longer pulled as a dependency unless the `http2` feature
|
||||
is enabled ([#2605])
|
||||
- **added:** Add `#[debug_middleware]` ([#1993], [#2725])
|
||||
|
||||
[#1993]: https://github.com/tokio-rs/axum/pull/1993
|
||||
[#2725]: https://github.com/tokio-rs/axum/pull/2725
|
||||
[#2586]: https://github.com/tokio-rs/axum/pull/2586
|
||||
[#2605]: https://github.com/tokio-rs/axum/pull/2605
|
||||
|
||||
|
|
|
@ -54,6 +54,7 @@ memchr = "2.4.1"
|
|||
mime = "0.3.16"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
rustversion = "1.0.9"
|
||||
serde = "1.0"
|
||||
sync_wrapper = "1.0.0"
|
||||
tower = { version = "0.4.13", default-features = false, features = ["util"] }
|
||||
|
@ -109,16 +110,12 @@ features = [
|
|||
"validate-request",
|
||||
]
|
||||
|
||||
[build-dependencies]
|
||||
rustversion = "1.0.9"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0"
|
||||
axum-macros = { path = "../axum-macros", version = "0.4.1", features = ["__private"] }
|
||||
quickcheck = "1.0"
|
||||
quickcheck_macros = "1.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "multipart"] }
|
||||
rustversion = "1.0.9"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
time = { version = "0.3", features = ["serde-human-readable"] }
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#[rustversion::nightly]
|
||||
fn main() {
|
||||
println!("cargo:rustc-cfg=nightly_error_messages");
|
||||
}
|
||||
|
||||
#[rustversion::not(nightly)]
|
||||
fn main() {}
|
|
@ -284,7 +284,7 @@ let app = Router::new().route("/users", post(create_user));
|
|||
# Customizing extractor responses
|
||||
|
||||
If an extractor fails it will return a response with the error and your
|
||||
handler will not be called. To customize the error response you have a two
|
||||
handler will not be called. To customize the error response you have two
|
||||
options:
|
||||
|
||||
1. Use `Result<T, T::Rejection>` as your extractor like shown in ["Optional
|
||||
|
|
|
@ -56,7 +56,22 @@ Note that `/*key` doesn't match empty segments. Thus:
|
|||
- `/*key` doesn't match `/` but does match `/a`, `/a/`, etc.
|
||||
- `/x/*key` doesn't match `/x` or `/x/` but does match `/x/a`, `/x/a/`, etc.
|
||||
|
||||
Wildcard captures can also be extracted using [`Path`](crate::extract::Path).
|
||||
Wildcard captures can also be extracted using [`Path`](crate::extract::Path):
|
||||
|
||||
```rust
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::Path,
|
||||
};
|
||||
|
||||
let app: Router = Router::new().route("/*key", get(handler));
|
||||
|
||||
async fn handler(Path(path): Path<String>) -> String {
|
||||
path
|
||||
}
|
||||
```
|
||||
|
||||
Note that the leading slash is not included, i.e. for the route `/foo/*rest` and
|
||||
the path `/foo/bar/baz` the value of `rest` will be `bar/baz`.
|
||||
|
||||
|
|
|
@ -125,8 +125,8 @@ pub use self::service::HandlerService;
|
|||
/// )));
|
||||
/// # let _: Router = app;
|
||||
/// ```
|
||||
#[cfg_attr(
|
||||
nightly_error_messages,
|
||||
#[rustversion::attr(
|
||||
since(1.78),
|
||||
diagnostic::on_unimplemented(
|
||||
note = "Consider using `#[axum::debug_handler]` to improve the error message"
|
||||
)
|
||||
|
|
|
@ -463,7 +463,7 @@ pub use self::form::Form;
|
|||
pub use axum_core::{BoxError, Error, RequestExt, RequestPartsExt};
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
pub use axum_macros::debug_handler;
|
||||
pub use axum_macros::{debug_handler, debug_middleware};
|
||||
|
||||
#[cfg(all(feature = "tokio", any(feature = "http1", feature = "http2")))]
|
||||
#[doc(inline)]
|
||||
|
|
|
@ -130,8 +130,8 @@ async fn websocket(stream: WebSocket, state: Arc<AppState>) {
|
|||
|
||||
// If any one of the tasks run to completion, we abort the other.
|
||||
tokio::select! {
|
||||
_ = (&mut send_task) => recv_task.abort(),
|
||||
_ = (&mut recv_task) => send_task.abort(),
|
||||
_ = &mut send_task => recv_task.abort(),
|
||||
_ = &mut recv_task => send_task.abort(),
|
||||
};
|
||||
|
||||
// Send "user left" message (similar to "joined" above).
|
||||
|
|
|
@ -99,7 +99,7 @@ async fn ws_handler(
|
|||
|
||||
/// Actual websocket statemachine (one will be spawned per connection)
|
||||
async fn handle_socket(mut socket: WebSocket, who: SocketAddr) {
|
||||
//send a ping (unsupported by some browsers) just to kick things off and get a response
|
||||
// send a ping (unsupported by some browsers) just to kick things off and get a response
|
||||
if socket.send(Message::Ping(vec![1, 2, 3])).await.is_ok() {
|
||||
println!("Pinged {who}...");
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue