1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-23 09:18:20 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Rob Ede
d54c26ad30 add redirect option to NormalizePath 2022-07-03 23:56:33 +01:00
55 changed files with 370 additions and 561 deletions

View File

@@ -1,14 +1,8 @@
# Changes
## Unreleased - 2022-xx-xx
## 0.6.2 - 2022-07-23
- Allow partial range responses for video content to start streaming sooner. [#2817]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2817]: https://github.com/actix/actix-web/pull/2817
## 0.6.1 - 2022-06-11
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.2"
version = "0.6.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
@@ -46,6 +46,6 @@ actix-server = { version = "2.1", optional = true } # ensure matching tokio-urin
[dev-dependencies]
actix-rt = "2.7"
actix-test = "0.1"
actix-test = "0.1.0-beta.13"
actix-web = "4"
tempfile = "3.2"

View File

@@ -3,11 +3,11 @@
> Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.2)](https://docs.rs/actix-files/0.6.2)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.1)](https://docs.rs/actix-files/0.6.1)
![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2)
[![dependency status](https://deps.rs/crate/actix-files/0.6.1/status.svg)](https://deps.rs/crate/actix-files/0.6.1)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display;
/// Errors which can occur when serving static files.
#[derive(Debug, PartialEq, Eq, Display)]
#[derive(Display, Debug, PartialEq)]
pub enum FilesError {
/// Path is not a directory
#[allow(dead_code)]
@@ -22,7 +22,7 @@ impl ResponseError for FilesError {
}
#[allow(clippy::enum_variant_names)]
#[derive(Debug, PartialEq, Eq, Display)]
#[derive(Display, Debug, PartialEq)]
#[non_exhaustive]
pub enum UriSegmentError {
/// The segment started with the wrapped invalid character.

View File

@@ -528,26 +528,11 @@ impl NamedFile {
length = ranges[0].length;
offset = ranges[0].start;
// When a Content-Encoding header is present in a 206 partial content response
// for video content, it prevents browser video players from starting playback
// before loading the whole video and also prevents seeking.
//
// See: https://github.com/actix/actix-web/issues/2815
//
// The assumption of this fix is that the video player knows to not send an
// Accept-Encoding header for this request and that downstream middleware will
// not attempt compression for requests without it.
//
// TODO: Solve question around what to do if self.encoding is set and partial
// range is requested. Reject request? Ignoring self.encoding seems wrong, too.
// In practice, it should not come up.
if req.headers().contains_key(&header::ACCEPT_ENCODING) {
// don't allow compression middleware to modify partial content
res.insert_header((
header::CONTENT_ENCODING,
HeaderValue::from_static("identity"),
));
}
// don't allow compression middleware to modify partial content
res.insert_header((
header::CONTENT_ENCODING,
HeaderValue::from_static("identity"),
));
res.insert_header((
header::CONTENT_RANGE,

View File

@@ -1,11 +1,11 @@
use actix_files::{Files, NamedFile};
use actix_files::Files;
use actix_web::{
http::{
header::{self, HeaderValue},
StatusCode,
},
test::{self, TestRequest},
web, App,
App,
};
#[actix_web::test]
@@ -36,31 +36,3 @@ async fn test_utf8_file_contents() {
Some(&HeaderValue::from_static("text/plain")),
);
}
#[actix_web::test]
async fn partial_range_response_encoding() {
let srv = test::init_service(App::new().default_service(web::to(|| async {
NamedFile::open_async("./tests/test.binary").await.unwrap()
})))
.await;
// range request without accept-encoding returns no content-encoding header
let req = TestRequest::with_uri("/")
.append_header((header::RANGE, "bytes=10-20"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
// range request with accept-encoding returns a content-encoding header
let req = TestRequest::with_uri("/")
.append_header((header::RANGE, "bytes=10-20"))
.append_header((header::ACCEPT_ENCODING, "identity"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(
res.headers().get(header::CONTENT_ENCODING).unwrap(),
"identity"
);
}

View File

@@ -1,24 +1,9 @@
# Changes
## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
## 3.0.0 - 2022-07-24
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
- Added `TestServer::client_headers` method. [#2097]
- Update `actix-server` dependency to `2`.
- Update `actix-tls` dependency to `3`.
- Update `bytes` to `1.0`. [#1813]
- Minimum supported Rust version (MSRV) is now 1.57.
[#2442]: https://github.com/actix/actix-web/pull/2442
[#2097]: https://github.com/actix/actix-web/pull/2097
[#1813]: https://github.com/actix/actix-web/pull/1813
<details>
<summary>3.0.0 Pre-Releases</summary>
## 3.0.0-beta.13 - 2022-02-16
- No significant changes since `3.0.0-beta.12`.
@@ -84,7 +69,6 @@
[#1813]: https://github.com/actix/actix-web/pull/1813
</details>
## 2.1.0 - 2020-11-25
- Add ability to set address for `TestServer`. [#1645]

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-http-test"
version = "3.0.0"
version = "3.0.0-beta.13"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"]

View File

@@ -3,11 +3,11 @@
> Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0)](https://docs.rs/actix-http-test/3.0.0)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.13)](https://docs.rs/actix-http-test/3.0.0-beta.13)
![Version](https://img.shields.io/badge/rustc-1.57+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.13)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@@ -97,7 +97,7 @@ flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.11", optional = true }
[dev-dependencies]
actix-http-test = { version = "3", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
actix-server = "2"
actix-tls = { version = "3", features = ["openssl"] }
actix-web = "4"

View File

@@ -388,7 +388,7 @@ impl StdError for DispatchError {
/// A set of error that can occur during parsing content type.
#[derive(Debug, Display, Error)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[cfg_attr(test, derive(PartialEq))]
#[non_exhaustive]
pub enum ContentTypeError {
/// Can not parse content type

View File

@@ -15,7 +15,7 @@ macro_rules! byte (
})
);
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, PartialEq, Clone)]
pub(super) enum ChunkedState {
Size,
SizeLws,

View File

@@ -440,7 +440,7 @@ impl HeaderIndex {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
/// Chunk type yielded while decoding a payload.
pub enum PayloadItem {
Chunk(Bytes),
@@ -450,7 +450,7 @@ pub enum PayloadItem {
/// Decoder that can handle different payload types.
///
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub struct PayloadDecoder {
kind: Kind,
}
@@ -476,7 +476,7 @@ impl PayloadDecoder {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
enum Kind {
/// A reader used when a `Content-Length` header is passed with a positive integer.
Length(u64),

View File

@@ -16,7 +16,7 @@ use crate::error::PayloadError;
/// max buffer size 32k
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum PayloadStatus {
Read,
Pause,

View File

@@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE};
/// - A character sequence representing the actual value (`value`), separated by single quotes.
///
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string.
pub charset: Charset,

View File

@@ -147,7 +147,7 @@ mod tests {
// copy of encoding from actix-web headers
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, PartialEq, Debug)]
pub enum Encoding {
Chunked,
Brotli,

View File

@@ -3,7 +3,7 @@ use std::{cell::RefCell, ops, rc::Rc};
use bitflags::bitflags;
/// Represents various types of connection
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ConnectionType {
/// Close connection after response.
Close,

View File

@@ -11,7 +11,7 @@ use super::{
};
/// A WebSocket message.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum Message {
/// Text message.
Text(ByteString),
@@ -36,7 +36,7 @@ pub enum Message {
}
/// A WebSocket frame.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum Frame {
/// Text frame. Note that the codec does not validate UTF-8 encoding.
Text(Bytes),
@@ -58,7 +58,7 @@ pub enum Frame {
}
/// A WebSocket continuation item.
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum Item {
FirstText(Bytes),
FirstBinary(Bytes),

View File

@@ -67,7 +67,7 @@ pub enum ProtocolError {
}
/// WebSocket handshake errors
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)]
pub enum HandshakeError {
/// Only get method is allowed.
#[display(fmt = "Method not allowed.")]

View File

@@ -21,7 +21,7 @@ default = ["http"]
[dependencies]
bytestring = ">=0.1.5, <2"
http = { version = "0.2.5", optional = true }
http = { version = "0.2.3", optional = true }
regex = "1.5"
serde = "1"
tracing = { version = "0.1.30", default-features = false, features = ["log"] }

View File

@@ -27,7 +27,7 @@ impl<'a> ResourcePath for &'a str {
impl ResourcePath for bytestring::ByteString {
fn path(&self) -> &str {
self
&*self
}
}

View File

@@ -1,6 +1,6 @@
use crate::{IntoPatterns, Resource, ResourceDef};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct ResourceId(pub u16);
/// Resource router.

View File

@@ -1,9 +1,6 @@
# Changes
## Unreleased - 2022-xx-xx
## 0.1.0 - 2022-07-24
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.

View File

@@ -1,6 +1,6 @@
[package]
name = "actix-test"
version = "0.1.0"
version = "0.1.0-beta.13"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
@@ -30,7 +30,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies]
actix-codec = "0.5"
actix-http = "3"
actix-http-test = "3"
actix-http-test = "3.0.0-beta.13"
actix-rt = "2.1"
actix-service = "2"
actix-utils = "3"

View File

@@ -27,7 +27,7 @@ tokio = { version = "1.13.1", features = ["sync"] }
[dev-dependencies]
actix-rt = "2.2"
actix-test = "0.1"
actix-test = "0.1.0-beta.13"
awc = { version = "3", default-features = false }
actix-web = { version = "4", features = ["macros"] }

View File

@@ -1,11 +1,8 @@
# Changes
## Unreleased - 2022-xx-xx
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2718]: https://github.com/actix/actix-web/pull/2718
## 4.0.1 - 2022-06-11
- Fix support for guard paths in route handler macros. [#2771]

View File

@@ -18,12 +18,12 @@ proc-macro = true
actix-router = "0.5.0"
proc-macro2 = "1"
quote = "1"
syn = { version = "1", features = ["full", "extra-traits"] }
syn = { version = "1", features = ["full", "parsing"] }
[dev-dependencies]
actix-macros = "0.2.3"
actix-rt = "2.2"
actix-test = "0.1"
actix-test = "0.1.0-beta.13"
actix-utils = "3.0.0"
actix-web = "4.0.0"

View File

@@ -46,20 +46,9 @@
//! ```
//!
//! # Multiple Path Handlers
//! Acts as a wrapper for multiple single method handler macros. It takes no arguments and
//! delegates those to the macros for the individual methods. See [macro@routes] macro docs.
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
//!
//! ```
//! # use actix_web::HttpResponse;
//! # use actix_web_codegen::routes;
//! #[routes]
//! #[get("/test")]
//! #[get("/test2")]
//! #[delete("/test")]
//! async fn example() -> HttpResponse {
//! HttpResponse::Ok().finish()
//! }
//! ```
//! [this issue]: https://github.com/actix/actix-web/issues/1709
//!
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
//! [GET]: macro@get
@@ -115,39 +104,6 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
route::with_method(None, args, input)
}
/// Creates resource handler, allowing multiple HTTP methods and paths.
///
/// # Syntax
/// ```plain
/// #[routes]
/// #[<method>("path", ...)]
/// #[<method>("path", ...)]
/// ...
/// ```
///
/// # Attributes
/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
///
/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
///
/// # Examples
/// ```
/// # use actix_web::HttpResponse;
/// # use actix_web_codegen::routes;
/// #[routes]
/// #[get("/test")]
/// #[get("/test2")]
/// #[delete("/test")]
/// async fn example() -> HttpResponse {
/// HttpResponse::Ok().finish()
/// }
/// ```
#[proc_macro_attribute]
pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
route::with_methods(input)
}
macro_rules! method_macro {
($variant:ident, $method:ident) => {
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]

View File

@@ -3,12 +3,24 @@ use std::{collections::HashSet, convert::TryFrom};
use actix_router::ResourceDef;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta, Path};
enum ResourceType {
Async,
Sync,
}
impl ToTokens for ResourceType {
fn to_tokens(&self, stream: &mut TokenStream2) {
let ident = format_ident!("to");
stream.append(ident);
}
}
macro_rules! method_type {
(
$($variant:ident, $upper:ident, $lower:ident,)+
$($variant:ident, $upper:ident,)+
) => {
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum MethodType {
@@ -30,27 +42,20 @@ macro_rules! method_type {
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
}
}
fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
}
}
}
};
}
method_type! {
Get, GET, get,
Post, POST, post,
Put, PUT, put,
Delete, DELETE, delete,
Head, HEAD, head,
Connect, CONNECT, connect,
Options, OPTIONS, options,
Trace, TRACE, trace,
Patch, PATCH, patch,
Get, GET,
Post, POST,
Put, PUT,
Delete, DELETE,
Head, HEAD,
Connect, CONNECT,
Options, OPTIONS,
Trace, TRACE,
Patch, PATCH,
}
impl ToTokens for MethodType {
@@ -85,18 +90,6 @@ impl Args {
let mut wrappers = Vec::new();
let mut methods = HashSet::new();
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let is_route_macro = method.is_none();
if let Some(method) = method {
methods.insert(method);
@@ -190,27 +183,55 @@ impl Args {
}
pub struct Route {
/// Name of the handler function being annotated.
name: syn::Ident,
/// Args passed to routing macro.
///
/// When using `#[routes]`, this will contain args for each specific routing macro.
args: Vec<Args>,
/// AST of the handler function being annotated.
args: Args,
ast: syn::ItemFn,
resource_type: ResourceType,
/// The doc comment attributes to copy to generated struct, if any.
doc_attributes: Vec<syn::Attribute>,
}
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
let mut guess = ResourceType::Sync;
if let syn::Type::ImplTrait(typ) = typ {
for bound in typ.bounds.iter() {
if let syn::TypeParamBound::Trait(bound) = bound {
for bound in bound.path.segments.iter() {
if bound.ident == "Future" {
guess = ResourceType::Async;
break;
} else if bound.ident == "Responder" {
guess = ResourceType::Sync;
break;
}
}
}
}
}
guess
}
impl Route {
pub fn new(
args: AttributeArgs,
ast: syn::ItemFn,
method: Option<MethodType>,
) -> syn::Result<Self> {
if args.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
format!(
r#"invalid service definition, expected #[{}("<some path>")]"#,
method
.map_or("route", |it| it.as_str())
.to_ascii_lowercase()
),
));
}
let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct.
@@ -223,7 +244,6 @@ impl Route {
.collect();
let args = Args::new(args, method)?;
if args.methods.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
@@ -231,44 +251,25 @@ impl Route {
));
}
if matches!(ast.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
Ok(Self {
name,
args: vec![args],
ast,
doc_attributes,
})
}
fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
let name = ast.sig.ident.clone();
// Try and pull out the doc comments so that we can reapply them to the generated struct.
// Note that multi line doc comments are converted to multiple doc attributes.
let doc_attributes = ast
.attrs
.iter()
.filter(|attr| attr.path.is_ident("doc"))
.cloned()
.collect();
if matches!(ast.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
let resource_type = if ast.sig.asyncness.is_some() {
ResourceType::Async
} else {
match ast.sig.output {
syn::ReturnType::Default => {
return Err(syn::Error::new_spanned(
ast,
"Function has no return type. Cannot be used as handler",
));
}
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
}
};
Ok(Self {
name,
args,
ast,
resource_type,
doc_attributes,
})
}
@@ -279,57 +280,38 @@ impl ToTokens for Route {
let Self {
name,
ast,
args,
doc_attributes,
} = self;
let registrations: TokenStream2 = args
.iter()
.map(|args| {
let Args {
args:
Args {
path,
resource_name,
guards,
wrappers,
methods,
} = args;
let resource_name = resource_name
.as_ref()
.map_or_else(|| name.to_string(), LitStr::value);
let method_guards = {
let mut others = methods.iter();
// unwrapping since length is checked to be at least one
let first = others.next().unwrap();
if methods.len() > 1 {
quote! {
.guard(
::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(::actix_web::guard::#others()))*
)
}
} else {
quote! {
.guard(::actix_web::guard::#first())
}
}
};
},
resource_type,
doc_attributes,
} = self;
let resource_name = resource_name
.as_ref()
.map_or_else(|| name.to_string(), LitStr::value);
let method_guards = {
let mut others = methods.iter();
// unwrapping since length is checked to be at least one
let first = others.next().unwrap();
if methods.len() > 1 {
quote! {
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.to(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
.guard(
::actix_web::guard::Any(::actix_web::guard::#first())
#(.or(::actix_web::guard::#others()))*
)
}
})
.collect();
} else {
quote! {
.guard(::actix_web::guard::#first())
}
}
};
let stream = quote! {
#(#doc_attributes)*
@@ -339,7 +321,14 @@ impl ToTokens for Route {
impl ::actix_web::dev::HttpServiceFactory for #name {
fn register(self, __config: &mut actix_web::dev::AppService) {
#ast
#registrations
let __resource = ::actix_web::Resource::new(#path)
.name(#resource_name)
#method_guards
#(.guard(::actix_web::guard::fn_guard(#guards)))*
#(.wrap(#wrappers))*
.#resource_type(#name);
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
}
}
};
@@ -368,57 +357,6 @@ pub(crate) fn with_method(
}
}
pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) {
Ok(ast) => ast,
// on parse error, make IDEs happy; see fn docs
Err(err) => return input_and_compile_error(input, err),
};
let (methods, others) = ast
.attrs
.into_iter()
.map(|attr| match MethodType::from_path(&attr.path) {
Ok(method) => Ok((method, attr)),
Err(_) => Err(attr),
})
.partition::<Vec<_>, _>(Result::is_ok);
ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
let methods =
match methods
.into_iter()
.map(Result::unwrap)
.map(|(method, attr)| {
attr.parse_meta().and_then(|args| {
if let Meta::List(args) = args {
Args::new(args.nested.into_iter().collect(), Some(method))
} else {
Err(syn::Error::new_spanned(attr, "Invalid input for macro"))
}
})
})
.collect::<Result<Vec<_>, _>>()
{
Ok(methods) if methods.is_empty() => return input_and_compile_error(
input,
syn::Error::new(
Span::call_site(),
"The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
),
),
Ok(methods) => methods,
Err(err) => return input_and_compile_error(input, err),
};
match Route::multiple(methods, ast) {
Ok(route) => route.into_token_stream().into(),
// on macro related error, make IDEs happy; see fn docs
Err(err) => input_and_compile_error(input, err),
}
}
/// Converts the error to a token stream and appends it to the original input.
///
/// Returning the original input in addition to the error is good for IDEs which can gracefully

View File

@@ -8,11 +8,9 @@ use actix_web::{
header::{HeaderName, HeaderValue},
StatusCode,
},
web, App, Error, HttpRequest, HttpResponse, Responder,
};
use actix_web_codegen::{
connect, delete, get, head, options, patch, post, put, route, routes, trace,
web, App, Error, HttpResponse, Responder,
};
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace};
use futures_core::future::LocalBoxFuture;
// Make sure that we can name function as 'config'
@@ -91,41 +89,8 @@ async fn route_test() -> impl Responder {
HttpResponse::Ok()
}
#[routes]
#[get("/routes/test")]
#[get("/routes/test2")]
#[post("/routes/test")]
async fn routes_test() -> impl Responder {
HttpResponse::Ok()
}
// routes overlap with the more specific route first, therefore accessible
#[routes]
#[get("/routes/overlap/test")]
#[get("/routes/overlap/{foo}")]
async fn routes_overlapping_test(req: HttpRequest) -> impl Responder {
// foo is only populated when route is not /routes/overlap/test
match req.match_info().get("foo") {
None => assert!(req.uri() == "/routes/overlap/test"),
Some(_) => assert!(req.uri() != "/routes/overlap/test"),
}
HttpResponse::Ok()
}
// routes overlap with the more specific route last, therefore inaccessible
#[routes]
#[get("/routes/overlap2/{foo}")]
#[get("/routes/overlap2/test")]
async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responder {
// foo is always populated even when path is /routes/overlap2/test
assert!(req.match_info().get("foo").is_some());
HttpResponse::Ok()
}
#[get("/custom_resource_name", name = "custom")]
async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder {
async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder {
assert!(req.url_for_static("custom").is_ok());
assert!(req.url_for_static("custom_resource_name_test").is_err());
HttpResponse::Ok()
@@ -236,9 +201,6 @@ async fn test_body() {
.service(patch_test)
.service(test_handler)
.service(route_test)
.service(routes_overlapping_test)
.service(routes_overlapping_inaccessible_test)
.service(routes_test)
.service(custom_resource_name_test)
.service(guard_test)
});
@@ -296,38 +258,6 @@ async fn test_body() {
let response = request.send().await.unwrap();
assert!(!response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/test2"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::POST, srv.url("/routes/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/not-set"));
let response = request.send().await.unwrap();
assert!(response.status().is_client_error());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/bar"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/test"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/bar"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
let response = request.send().await.unwrap();
assert!(response.status().is_success());

View File

@@ -12,10 +12,6 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
t.pass("tests/trybuild/routes-ok.rs");
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
t.pass("tests/trybuild/docstring-ok.rs");
t.pass("tests/trybuild/test-runtime.rs");

View File

@@ -1,14 +0,0 @@
use actix_web_codegen::*;
#[routes]
#[get]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index));
}

View File

@@ -1,21 +0,0 @@
error: invalid service definition, expected #[get("<path>")]
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
4 | #[get]
| ^^^^^^
|
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
error: Invalid input for macro
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
4 | #[get]
| ^^^^^^
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/routes-missing-args-fail.rs:13:55
|
13 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@@ -1,13 +0,0 @@
use actix_web_codegen::*;
#[routes]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index));
}

View File

@@ -1,15 +0,0 @@
error: The #[routes] macro requires at least one `#[<method>(..)]` attribute.
--> tests/trybuild/routes-missing-method-fail.rs:3:1
|
3 | #[routes]
| ^^^^^^^^^
|
= note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
--> tests/trybuild/routes-missing-method-fail.rs:12:55
|
12 | let srv = actix_test::start(|| App::new().service(index));
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
| |
| required by a bound introduced by this call

View File

@@ -1,23 +0,0 @@
use actix_web_codegen::*;
#[routes]
#[get("/")]
#[post("/")]
async fn index() -> String {
"Hello World!".to_owned()
}
#[actix_web::main]
async fn main() {
use actix_web::App;
let srv = actix_test::start(|| App::new().service(index));
let request = srv.get("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
let request = srv.post("/");
let response = request.send().await.unwrap();
assert!(response.status().is_success());
}

View File

@@ -2,14 +2,12 @@
## Unreleased - 2022-xx-xx
### Added
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
### Changed
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2718]: https://github.com/actix/actix-web/pull/2718
[#2752]: https://github.com/actix/actix-web/pull/2752
[#2786]: https://github.com/actix/actix-web/pull/2786

View File

@@ -84,12 +84,11 @@ derive_more = "0.99.5"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
http = "0.2.8"
itoa = "1"
language-tags = "0.3"
once_cell = "1.5"
log = "0.4"
mime = "0.3"
once_cell = "1.5"
pin-project-lite = "0.2.7"
regex = "1.5.5"
serde = "1.0"
@@ -102,7 +101,7 @@ url = "2.1"
[dev-dependencies]
actix-files = "0.6"
actix-test = { version = "0.1", features = ["openssl", "rustls"] }
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
awc = { version = "3", features = ["openssl"] }
brotli = "3.3.3"

View File

@@ -118,7 +118,7 @@ impl<T: ?Sized> Deref for Data<T> {
impl<T: ?Sized> Clone for Data<T> {
fn clone(&self) -> Data<T> {
Data(Arc::clone(&self.0))
Data(self.0.clone())
}
}

View File

@@ -42,7 +42,7 @@ pub struct BlockingError;
impl ResponseError for crate::error::BlockingError {}
/// Errors which can occur when attempting to generate resource uri.
#[derive(Debug, PartialEq, Eq, Display, Error, From)]
#[derive(Debug, PartialEq, Display, Error, From)]
#[non_exhaustive]
pub enum UrlGenerationError {
/// Resource not found.

View File

@@ -10,10 +10,9 @@
//! - Browser conformance tests at: <http://greenbytes.de/tech/tc2231/>
//! - IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml>
use std::fmt::{self, Write};
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt::{self, Write};
use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
use crate::http::header;
@@ -37,7 +36,7 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
}
/// The implied disposition of the content of the HTTP body.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionType {
/// Inline implies default processing.
Inline,
@@ -79,7 +78,7 @@ impl<'a> From<&'a str> for DispositionType {
/// assert!(param.is_filename());
/// assert_eq!(param.as_filename().unwrap(), "sample.txt");
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
@@ -302,7 +301,7 @@ impl DispositionParam {
/// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
/// The disposition type
pub disposition: DispositionType,

View File

@@ -57,7 +57,7 @@ use crate::HttpMessage;
/// IfRange::Date(fetched.into())
/// );
/// ```
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
pub enum IfRange {
/// The entity-tag the client has of the resource.
EntityTag(EntityTag),

View File

@@ -224,11 +224,10 @@ macro_rules! common_header {
// List header, one or more items with "*" option
($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => {
$(#[$attrs])*
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
pub enum $id {
/// Any value is a match
Any,
/// Only the listed items are a match
Items(Vec<$item>),
}

View File

@@ -53,7 +53,7 @@ use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderVa
/// builder.insert_header(Range::bytes(1, 100));
/// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)]));
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(PartialEq, Clone, Debug)]
pub enum Range {
/// Byte range.
Bytes(Vec<ByteRangeSpec>),

View File

@@ -2,6 +2,7 @@ use std::{convert::Infallible, net::SocketAddr};
use actix_utils::future::{err, ok, Ready};
use derive_more::{Display, Error};
use once_cell::sync::Lazy;
use crate::{
dev::{AppConfig, Payload, RequestHead},
@@ -12,9 +13,12 @@ use crate::{
FromRequest, HttpRequest, ResponseError,
};
static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
static X_FORWARDED_FOR: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-for"));
static X_FORWARDED_HOST: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-host"));
static X_FORWARDED_PROTO: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-proto"));
/// Trim whitespace then any quote marks.
fn unquote(val: &str) -> &str {
@@ -113,21 +117,21 @@ impl ConnectionInfo {
}
let scheme = scheme
.or_else(|| first_header_value(req, &X_FORWARDED_PROTO))
.or_else(|| first_header_value(req, &*X_FORWARDED_PROTO))
.or_else(|| req.uri.scheme().map(Scheme::as_str))
.or_else(|| Some("https").filter(|_| cfg.secure()))
.unwrap_or("http")
.to_owned();
let host = host
.or_else(|| first_header_value(req, &X_FORWARDED_HOST))
.or_else(|| first_header_value(req, &*X_FORWARDED_HOST))
.or_else(|| req.headers.get(&header::HOST)?.to_str().ok())
.or_else(|| req.uri.authority().map(Authority::as_str))
.unwrap_or_else(|| cfg.host())
.to_owned();
let realip_remote_addr = realip_remote_addr
.or_else(|| first_header_value(req, &X_FORWARDED_FOR))
.or_else(|| first_header_value(req, &*X_FORWARDED_FOR))
.map(str::to_owned);
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());

View File

@@ -132,7 +132,6 @@ macro_rules! codegen_reexport {
codegen_reexport!(main);
codegen_reexport!(test);
codegen_reexport!(route);
codegen_reexport!(routes);
codegen_reexport!(head);
codegen_reexport!(get);
codegen_reexport!(post);

View File

@@ -220,25 +220,32 @@ static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
encoding.join(", ")
});
static SUPPORTED_ENCODINGS: &[Encoding] = &[
Encoding::identity(),
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
let mut encodings = vec![Encoding::identity()];
#[cfg(feature = "compress-brotli")]
{
Encoding::brotli()
},
encodings.push(Encoding::brotli());
}
#[cfg(feature = "compress-gzip")]
{
Encoding::gzip()
},
#[cfg(feature = "compress-gzip")]
{
Encoding::deflate()
},
encodings.push(Encoding::gzip());
encodings.push(Encoding::deflate());
}
#[cfg(feature = "compress-zstd")]
{
Encoding::zstd()
},
];
encodings.push(Encoding::zstd());
}
assert!(
!encodings.is_empty(),
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
);
encodings
});
// move cfg(feature) to prevents_double_compressing if more tests are added
#[cfg(feature = "compress-gzip")]
@@ -319,7 +326,6 @@ mod tests {
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
#[allow(clippy::mutable_key_type)]
let vary_headers = res.headers().get_all(header::VARY).collect::<HashSet<_>>();
assert!(vary_headers.contains(&HeaderValue::from_static("x-test")));
assert!(vary_headers.contains(&HeaderValue::from_static("accept-encoding")));

View File

@@ -1,17 +1,31 @@
//! For middleware documentation, see [`NormalizePath`].
use actix_http::uri::{PathAndQuery, Uri};
use std::{
future::Future,
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use actix_service::{Service, Transform};
use actix_utils::future::{ready, Ready};
use bytes::Bytes;
use futures_core::ready;
use pin_project_lite::pin_project;
use regex::Regex;
use crate::{
body::EitherBody,
http::{
header,
uri::{PathAndQuery, Uri},
StatusCode,
},
service::{ServiceRequest, ServiceResponse},
Error,
Error, HttpResponse,
};
/// Determines the behavior of the [`NormalizePath`] middleware.
/// Determines the path rewriting behavior of the [`NormalizePath`] middleware.
///
/// The default is `TrailingSlash::Trim`.
#[non_exhaustive]
@@ -86,7 +100,13 @@ impl Default for TrailingSlash {
/// # })
/// ```
#[derive(Debug, Clone, Copy)]
pub struct NormalizePath(TrailingSlash);
pub struct NormalizePath {
/// Controls path normalization behavior.
trailing_slash_behavior: TrailingSlash,
/// Returns redirects for non-normalized paths if `Some`.
use_redirects: Option<StatusCode>,
}
impl Default for NormalizePath {
fn default() -> Self {
@@ -95,14 +115,20 @@ impl Default for NormalizePath {
in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`."
);
Self(TrailingSlash::Trim)
Self {
trailing_slash_behavior: TrailingSlash::default(),
use_redirects: None,
}
}
}
impl NormalizePath {
/// Create new `NormalizePath` middleware with the specified trailing slash style.
pub fn new(trailing_slash_style: TrailingSlash) -> Self {
Self(trailing_slash_style)
pub fn new(behavior: TrailingSlash) -> Self {
Self {
trailing_slash_behavior: behavior,
use_redirects: None,
}
}
/// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics.
@@ -111,6 +137,32 @@ impl NormalizePath {
pub fn trim() -> Self {
Self::new(TrailingSlash::Trim)
}
/// Configures middleware to respond to requests with non-normalized paths with a 307 redirect.
///
/// If configured
///
/// For example, a request with the path `/api//v1/foo/` would receive a response with a
/// `Location: /api/v1/foo` header (assuming `Trim` trailing slash behavior.)
///
/// To customize the status code, use [`use_redirects_with`](Self::use_redirects_with).
pub fn use_redirects(mut self) -> Self {
self.use_redirects = Some(StatusCode::TEMPORARY_REDIRECT);
self
}
/// Configures middleware to respond to requests with non-normalized paths with a redirect.
///
/// For example, a request with the path `/api//v1/foo/` would receive a 307 response with a
/// `Location: /api/v1/foo` header (assuming `Trim` trailing slash behavior.)
///
/// # Panics
/// Panics if `status_code` is not a redirect (300-399).
pub fn use_redirects_with(mut self, status_code: StatusCode) -> Self {
assert!(status_code.is_redirection());
self.use_redirects = Some(status_code);
self
}
}
impl<S, B> Transform<S, ServiceRequest> for NormalizePath
@@ -118,35 +170,37 @@ where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Response = ServiceResponse<EitherBody<B, ()>>;
type Error = Error;
type Transform = NormalizePathNormalization<S>;
type Transform = NormalizePathService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(NormalizePathNormalization {
ready(Ok(NormalizePathService {
service,
merge_slash: Regex::new("//+").unwrap(),
trailing_slash_behavior: self.0,
trailing_slash_behavior: self.trailing_slash_behavior,
use_redirects: self.use_redirects,
}))
}
}
pub struct NormalizePathNormalization<S> {
pub struct NormalizePathService<S> {
service: S,
merge_slash: Regex,
trailing_slash_behavior: TrailingSlash,
use_redirects: Option<StatusCode>,
}
impl<S, B> Service<ServiceRequest> for NormalizePathNormalization<S>
impl<S, B> Service<ServiceRequest> for NormalizePathService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Response = ServiceResponse<EitherBody<B, ()>>;
type Error = Error;
type Future = S::Future;
type Future = NormalizePathFuture<S, B>;
actix_service::forward_ready!(service);
@@ -189,7 +243,7 @@ where
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
let path = match query {
Some(q) => Bytes::from(format!("{}?{}", path, q)),
Some(query) => Bytes::from(format!("{}?{}", path, query)),
None => Bytes::copy_from_slice(path.as_bytes()),
};
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
@@ -199,20 +253,87 @@ where
req.head_mut().uri = uri;
}
}
self.service.call(req)
match self.use_redirects {
Some(code) => {
let mut res = HttpResponse::with_body(code, ());
res.headers_mut().insert(
header::LOCATION,
req.head_mut().uri.to_string().parse().unwrap(),
);
NormalizePathFuture::redirect(req.into_response(res))
}
None => NormalizePathFuture::service(self.service.call(req)),
}
}
}
pin_project! {
pub struct NormalizePathFuture<S: Service<ServiceRequest>, B> {
#[pin] inner: Inner<S, B>,
}
}
impl<S: Service<ServiceRequest>, B> NormalizePathFuture<S, B> {
fn service(fut: S::Future) -> Self {
Self {
inner: Inner::Service {
fut,
_body: PhantomData,
},
}
}
fn redirect(res: ServiceResponse<()>) -> Self {
Self {
inner: Inner::Redirect { res: Some(res) },
}
}
}
pin_project! {
#[project = InnerProj]
enum Inner<S: Service<ServiceRequest>, B> {
Redirect { res: Option<ServiceResponse<()>>, },
Service {
#[pin] fut: S::Future,
_body: PhantomData<B>,
},
}
}
impl<S, B> Future for NormalizePathFuture<S, B>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Output = Result<ServiceResponse<EitherBody<B, ()>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.inner.project() {
InnerProj::Redirect { res } => {
Poll::Ready(Ok(res.take().unwrap().map_into_right_body()))
}
InnerProj::Service { fut, .. } => {
let res = ready!(fut.poll(cx))?;
Poll::Ready(Ok(res.map_into_left_body()))
}
}
}
}
#[cfg(test)]
mod tests {
use actix_http::StatusCode;
use actix_service::IntoService;
use super::*;
use crate::{
dev::ServiceRequest,
guard::fn_guard,
test::{call_service, init_service, TestRequest},
test::{self, call_service, init_service, TestRequest},
web, App, HttpResponse,
};
@@ -256,7 +377,7 @@ mod tests {
async fn trim_trailing_slashes() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Trim))
.wrap(NormalizePath::new(TrailingSlash::Trim))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(
@@ -292,11 +413,13 @@ mod tests {
#[actix_rt::test]
async fn trim_root_trailing_slashes_with_query() {
let app = init_service(
App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
web::resource("/")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
App::new()
.wrap(NormalizePath::new(TrailingSlash::Trim))
.service(
web::resource("/")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
@@ -313,7 +436,7 @@ mod tests {
async fn ensure_trailing_slash() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Always))
.wrap(NormalizePath::new(TrailingSlash::Always))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something/").to(HttpResponse::Ok))
.service(
@@ -350,7 +473,7 @@ mod tests {
async fn ensure_root_trailing_slash_with_query() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Always))
.wrap(NormalizePath::new(TrailingSlash::Always))
.service(
web::resource("/")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
@@ -372,7 +495,7 @@ mod tests {
async fn keep_trailing_slash_unchanged() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::MergeOnly))
.wrap(NormalizePath::new(TrailingSlash::MergeOnly))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(web::resource("/v1/").to(HttpResponse::Ok))
@@ -486,4 +609,27 @@ mod tests {
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success());
}
#[actix_rt::test]
async fn should_return_redirects_when_configured() {
let normalize = NormalizePath::trim()
.use_redirects()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_uri("/v1/something/").to_srv_request();
let res = normalize.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::TEMPORARY_REDIRECT);
let normalize = NormalizePath::trim()
.use_redirects_with(StatusCode::PERMANENT_REDIRECT)
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::with_uri("/v1/something/").to_srv_request();
let res = normalize.call(req).await.unwrap();
assert_eq!(res.status(), StatusCode::PERMANENT_REDIRECT);
}
}

View File

@@ -253,7 +253,7 @@ impl HttpRequest {
#[inline]
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
if !self.extensions().contains::<ConnectionInfo>() {
let info = ConnectionInfo::new(self.head(), self.app_config());
let info = ConnectionInfo::new(self.head(), &*self.app_config());
self.extensions_mut().insert(info);
}

View File

@@ -401,8 +401,6 @@ where
.into_factory()
.map_err(|err| err.into().error_response());
// false positive lint (?)
#[allow(clippy::significant_drop_in_scrutinee)]
let acceptor_config = match c.tls_handshake_timeout {
Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur),
None => TlsAcceptorConfig::default(),

View File

@@ -73,7 +73,7 @@ use crate::{
/// }
/// }
/// ```
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub enum Either<L, R> {
/// A value of type `L`.
Left(L),

View File

@@ -134,7 +134,6 @@ where
/// ```
#[derive(Clone, Default)]
pub struct PathConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>,
}

View File

@@ -169,7 +169,6 @@ impl<T: DeserializeOwned> FromRequest for Query<T> {
/// ```
#[derive(Clone, Default)]
pub struct QueryConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>,
}

View File

@@ -94,9 +94,9 @@ trust-dns-resolver = { version = "0.21", optional = true }
[dev-dependencies]
actix-http = { version = "3", features = ["openssl"] }
actix-http-test = { version = "3", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
actix-server = "2"
actix-test = { version = "0.1", features = ["openssl", "rustls"] }
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
actix-tls = { version = "3", features = ["openssl", "rustls"] }
actix-utils = "3"
actix-web = { version = "4", features = ["openssl"] }

View File

@@ -98,7 +98,7 @@ rm -f $README_FILE.bak
echo "manifest, changelog, and readme updated"
echo
echo "check other references:"
rg --glob='**/{Cargo.toml,README.md}' "\
rg --glob='**/Cargo.toml' "\
${PACKAGE_NAME} ?= ?\"[^\"]+\"\
|${PACKAGE_NAME} ?=.*version ?= ?\"([^\"]+)\"\
|package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\"([^\"]+)\"\