diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index a4b54ca7a..008c33f89 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -5,6 +5,9 @@ on: branches: - master +permissions: + contents: read # to fetch code (actions/checkout) + jobs: check_benchmark: runs-on: ubuntu-latest diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 1ee97b591..7ac6388d4 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -4,6 +4,9 @@ on: push: branches: [master] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: build_and_test_nightly: strategy: @@ -92,29 +95,21 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true + - uses: dtolnay/rust-toolchain@stable - name: Install cargo-hack uses: taiki-e/install-action@cargo-hack - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } + run: cargo generate-lockfile - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset } - + run: cargo ci-check-all-feature-powerset + - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset-linux } + run: cargo ci-check-all-feature-powerset-linux nextest: name: nextest @@ -127,24 +122,15 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - override: true + - uses: dtolnay/rust-toolchain@stable - name: Install nextest uses: taiki-e/install-action@nextest - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } + run: cargo generate-lockfile - name: Cache Dependencies uses: Swatinem/rust-cache@v1.3.0 - name: Test with cargo-nextest - uses: actions-rs/cargo@v1 - with: - command: nextest - args: run + run: cargo nextest run diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de1e1fe18..421becc63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: push: branches: [master] +permissions: + contents: read # to fetch code (actions/checkout) + jobs: build_and_test: strategy: @@ -63,6 +66,11 @@ jobs: - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 + - name: workaround MSRV issues + if: matrix.version != 'stable' + run: | + cargo update -p=zstd-sys --precise=2.0.1+zstd.1.5.2 + - name: check minimal uses: actions-rs/cargo@v1 with: { command: ci-check-min } @@ -96,16 +104,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true + - uses: dtolnay/rust-toolchain@stable - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } + run: cargo generate-lockfile - name: Cache Dependencies uses: Swatinem/rust-cache@v1.3.0 @@ -123,20 +125,13 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install Rust (nightly) - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly-x86_64-unknown-linux-gnu - profile: minimal - override: true + - uses: dtolnay/rust-toolchain@nightly - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } + run: cargo generate-lockfile - name: Cache Dependencies uses: Swatinem/rust-cache@v1.3.0 - name: doc tests - uses: actions-rs/cargo@v1 + run: cargo ci-doctest timeout-minutes: 60 - with: { command: ci-doctest } diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index bc2cec145..e94c4d1af 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -9,54 +9,37 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: rustfmt - - name: Check with rustfmt - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - uses: dtolnay/rust-toolchain@nightly + with: { components: rustfmt } + - run: cargo fmt --all -- --check clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: clippy - override: true + - uses: dtolnay/rust-toolchain@stable + with: { components: clippy } - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } + run: cargo generate-lockfile - name: Cache Dependencies uses: Swatinem/rust-cache@v1.2.0 - + - name: Check with Clippy uses: actions-rs/clippy-check@v1 with: - token: ${{ secrets.GITHUB_TOKEN }} args: --workspace --tests --examples --all-features + token: ${{ secrets.GITHUB_TOKEN }} lint-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - profile: minimal - components: rust-docs + + - uses: dtolnay/rust-toolchain@stable + with: { components: rust-docs } + - name: Check for broken intra-doc links uses: actions-rs/cargo@v1 env: diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml index 07f839e34..9aadafafc 100644 --- a/.github/workflows/upload-doc.yml +++ b/.github/workflows/upload-doc.yml @@ -4,31 +4,29 @@ on: push: branches: [master] +permissions: {} jobs: build: + permissions: + contents: write # to push changes in repo (jamesives/github-pages-deploy-action) + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly-x86_64-unknown-linux-gnu - profile: minimal - override: true + - uses: dtolnay/rust-toolchain@nightly - name: Build Docs - uses: actions-rs/cargo@v1 - with: - command: doc - args: --workspace --all-features --no-deps + run: cargo +nightly doc --no-deps --workspace --all-features + env: + RUSTDOCFLAGS: --cfg=docsrs - name: Tweak HTML run: echo '' > target/doc/index.html - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4.4.0 + uses: JamesIves/github-pages-deploy-action@v4.4.1 with: folder: target/doc single-commit: true diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 33de0e6d9..018acdfb1 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -3,7 +3,6 @@ name = "actix-files" version = "0.6.2" authors = [ "Nikolay Kim ", - "fakeshadow <24548779@qq.com>", "Rob Ede ", ] description = "Static file serving for Actix Web" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 30e436160..a8b888ef4 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -77,6 +77,8 @@ mime = "0.3" percent-encoding = "2.1" pin-project-lite = "0.2" smallvec = "1.6.1" +tokio = { version = "1.13.1", features = [] } +tokio-util = { version = "0.7", features = ["io", "codec"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] } # http2 diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index c4f0503cd..6af6d5095 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -10,13 +10,13 @@ use std::{ time::Duration, }; -use actix_codec::Encoder; use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response}; use actix_rt::time::{interval, Interval}; use actix_server::Server; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::{ready, Stream}; +use tokio_util::codec::Encoder; use tracing::{info, trace}; #[actix_rt::main] diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index 75c88d00c..6a0d531d0 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -1,9 +1,9 @@ use std::{fmt, io}; -use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::{Bytes, BytesMut}; use http::{Method, Version}; +use tokio_util::codec::{Decoder, Encoder}; use super::{ decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 80afd7455..e11f175c9 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -1,9 +1,9 @@ use std::{fmt, io}; -use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::BytesMut; use http::{Method, Version}; +use tokio_util::codec::{Decoder, Encoder}; use super::{ decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 81090667d..60660b85b 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -8,13 +8,15 @@ use std::{ task::{Context, Poll}, }; -use actix_codec::{AsyncRead, AsyncWrite, Decoder as _, Encoder as _, Framed, FramedParts}; +use actix_codec::{Framed, FramedParts}; use actix_rt::time::sleep_until; use actix_service::Service; use bitflags::bitflags; use bytes::{Buf, BytesMut}; use futures_core::ready; use pin_project_lite::pin_project; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_util::codec::{Decoder as _, Encoder as _}; use tracing::{error, trace}; use crate::{ @@ -1004,7 +1006,7 @@ where this.read_buf.reserve(HW_BUFFER_SIZE - remaining); } - match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) { + match tokio_util::io::poll_read_buf(io.as_mut(), cx, this.read_buf) { Poll::Ready(Ok(n)) => { this.flags.remove(Flags::FINISHED); diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 4a2e741b6..6a149f9a4 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -1,7 +1,7 @@ -use actix_codec::{Decoder, Encoder}; use bitflags::bitflags; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; +use tokio_util::codec::{Decoder, Encoder}; use tracing::error; use super::{ diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 2f6b2363b..396f1e86c 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -76,7 +76,9 @@ mod inner { use pin_project_lite::pin_project; use tracing::debug; - use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; + use actix_codec::Framed; + use tokio::io::{AsyncRead, AsyncWrite}; + use tokio_util::codec::{Decoder, Encoder}; use crate::{body::BoxBody, Response}; diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 203315f3e..5d15d37cf 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -30,8 +30,8 @@ futures-util = { version = "0.3.7", default-features = false } httparse = "1.3" local-waker = "0.1" log = "0.4" +memchr = "2.5" mime = "0.3" -twoway = "0.2" serde = "1.0" serde_plain = "1.0" serde_json = "1.0" diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index c3757177f..1d0510039 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -606,7 +606,7 @@ impl InnerField { } loop { - return if let Some(idx) = twoway::find_bytes(&payload.buf[pos..], b"\r") { + return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") { let cur = pos + idx; // check if we have enough data for boundary detection @@ -827,7 +827,7 @@ impl PayloadBuffer { /// Read until specified ending fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { - let res = twoway::find_bytes(&self.buf, line) + let res = memchr::memmem::find(&self.buf, line) .map(|idx| self.buf.split_to(idx + line.len()).freeze()); if res.is_none() && self.eof { diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 8222fc864..26b1c09de 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -24,6 +24,7 @@ bytestring = "1" futures-core = { version = "0.3.7", default-features = false } pin-project-lite = "0.2" tokio = { version = "1.13.1", features = ["sync"] } +tokio-util = { version = "0.7", features = ["codec"] } [dev-dependencies] actix-rt = "2.2" diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 9a4880159..e1110eddb 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -74,7 +74,6 @@ use actix::{ Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, SpawnHandle, }; -use actix_codec::{Decoder as _, Encoder as _}; use actix_http::ws::{hash_key, Codec}; pub use actix_http::ws::{ CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, @@ -92,6 +91,7 @@ use bytestring::ByteString; use futures_core::Stream; use pin_project_lite::pin_project; use tokio::sync::oneshot; +use tokio_util::codec::{Decoder as _, Encoder as _}; /// Builder for Websocket session response. /// diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index a018bc248..6440ad693 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -5,7 +5,11 @@ - Add `ContentDisposition::attachment` constructor. [#2867] - Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784] - Add `Logger::custom_response_replace()`. [#2631] +- Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961] +- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265] +[#1961]: https://github.com/actix/actix-web/pull/1961 +[#2265]: https://github.com/actix/actix-web/pull/2265 [#2631]: https://github.com/actix/actix-web/pull/2631 [#2784]: https://github.com/actix/actix-web/pull/2784 [#2867]: https://github.com/actix/actix-web/pull/2867 diff --git a/actix-web/src/guard/acceptable.rs b/actix-web/src/guard/acceptable.rs new file mode 100644 index 000000000..a31494a18 --- /dev/null +++ b/actix-web/src/guard/acceptable.rs @@ -0,0 +1,99 @@ +use super::{Guard, GuardContext}; +use crate::http::header::Accept; + +/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type. +/// +/// An exception is that matching `*/*` must be explicitly enabled because most browsers send this +/// as part of their `Accept` header for almost every request. +/// +/// # Examples +/// ``` +/// use actix_web::{guard::Acceptable, web, HttpResponse}; +/// +/// web::resource("/images") +/// .guard(Acceptable::new(mime::IMAGE_STAR)) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("only called when images responses are acceptable") +/// })); +/// ``` +#[derive(Debug, Clone)] +pub struct Acceptable { + mime: mime::Mime, + + /// Wether to match `*/*` mime type. + /// + /// Defaults to false because it's not very useful otherwise. + match_star_star: bool, +} + +impl Acceptable { + /// Constructs new `Acceptable` guard with the given `mime` type/pattern. + pub fn new(mime: mime::Mime) -> Self { + Self { + mime, + match_star_star: false, + } + } + + /// Allows `*/*` in the `Accept` header to pass the guard check. + pub fn match_star_star(mut self) -> Self { + self.match_star_star = true; + self + } +} + +impl Guard for Acceptable { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + let accept = match ctx.header::() { + Some(hdr) => hdr, + None => return false, + }; + + let target_type = self.mime.type_(); + let target_subtype = self.mime.subtype(); + + for mime in accept.0.into_iter().map(|q| q.item) { + return match (mime.type_(), mime.subtype()) { + (typ, subtype) if typ == target_type && subtype == target_subtype => true, + (typ, mime::STAR) if typ == target_type => true, + (mime::STAR, mime::STAR) if self.match_star_star => true, + _ => continue, + }; + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{http::header, test::TestRequest}; + + #[test] + fn test_acceptable() { + let req = TestRequest::default().to_srv_request(); + assert!(!Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); + + let req = TestRequest::default() + .insert_header((header::ACCEPT, "application/json")) + .to_srv_request(); + assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); + + let req = TestRequest::default() + .insert_header((header::ACCEPT, "text/html, application/json")) + .to_srv_request(); + assert!(Acceptable::new(mime::APPLICATION_JSON).check(&req.guard_ctx())); + } + + #[test] + fn test_acceptable_star() { + let req = TestRequest::default() + .insert_header((header::ACCEPT, "text/html, */*;q=0.8")) + .to_srv_request(); + + assert!(Acceptable::new(mime::APPLICATION_JSON) + .match_star_star() + .check(&req.guard_ctx())); + } +} diff --git a/actix-web/src/guard.rs b/actix-web/src/guard/mod.rs similarity index 99% rename from actix-web/src/guard.rs rename to actix-web/src/guard/mod.rs index ef1301075..5fcaec0de 100644 --- a/actix-web/src/guard.rs +++ b/actix-web/src/guard/mod.rs @@ -56,6 +56,9 @@ use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; +mod acceptable; +pub use self::acceptable::Acceptable; + /// Provides access to request parts that are useful during routing. #[derive(Debug)] pub struct GuardContext<'a> { diff --git a/actix-web/src/http/header/accept.rs b/actix-web/src/http/header/accept.rs index 744c9b6e8..1be136b19 100644 --- a/actix-web/src/http/header/accept.rs +++ b/actix-web/src/http/header/accept.rs @@ -6,8 +6,7 @@ use super::{common_header, QualityItem}; use crate::http::header; common_header! { - /// `Accept` header, defined - /// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) + /// `Accept` header, defined in [RFC 7231 §5.3.2]. /// /// The `Accept` header field can be used by user agents to specify /// response media types that are acceptable. Accept header fields can @@ -71,6 +70,8 @@ common_header! { /// ]) /// ); /// ``` + /// + /// [RFC 7231 §5.3.2]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 (Accept, header::ACCEPT) => (QualityItem)* test_parse_and_format { @@ -101,13 +102,12 @@ common_header! { vec![b"text/plain; charset=utf-8"], Some(Accept(vec![ QualityItem::max(mime::TEXT_PLAIN_UTF_8), - ]))); + ]))); crate::http::header::common_header_test!( test4, vec![b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ - QualityItem::new(mime::TEXT_PLAIN_UTF_8, - q(0.5)), + QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)), ]))); #[test] diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index 8d9e2dbcd..338541208 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -86,6 +86,7 @@ mod helpers; pub mod http; mod info; pub mod middleware; +mod redirect; mod request; mod request_data; mod resource; diff --git a/actix-web/src/redirect.rs b/actix-web/src/redirect.rs new file mode 100644 index 000000000..ca9e23aa4 --- /dev/null +++ b/actix-web/src/redirect.rs @@ -0,0 +1,238 @@ +//! See [`Redirect`] for service/responder documentation. + +use std::borrow::Cow; + +use actix_utils::future::ready; + +use crate::{ + dev::{fn_service, AppService, HttpServiceFactory, ResourceDef, ServiceRequest}, + http::{header::LOCATION, StatusCode}, + HttpRequest, HttpResponse, Responder, +}; + +/// An HTTP service for redirecting one path to another path or URL. +/// +/// By default, the "307 Temporary Redirect" status is used when responding. See [this MDN +/// article][mdn-redirects] on why 307 is preferred over 302. +/// +/// # Examples +/// As service: +/// ``` +/// use actix_web::{web, App}; +/// +/// App::new() +/// // redirect "/duck" to DuckDuckGo +/// .service(web::redirect("/duck", "https://duck.com")) +/// .service( +/// // redirect "/api/old" to "/api/new" +/// web::scope("/api").service(web::redirect("/old", "/new")) +/// ); +/// ``` +/// +/// As responder: +/// ``` +/// use actix_web::web::Redirect; +/// +/// async fn handler() -> impl Responder { +/// // sends a permanent (308) redirect to duck.com +/// Redirect::to("https://duck.com").permanent() +/// } +/// # actix_web::web::to(handler); +/// ``` +/// +/// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections +#[derive(Debug, Clone)] +pub struct Redirect { + from: Cow<'static, str>, + to: Cow<'static, str>, + status_code: StatusCode, +} + +impl Redirect { + /// Construct a new `Redirect` service that matches a path. + /// + /// This service will match exact paths equal to `from` within the current scope. I.e., when + /// registered on the root `App`, it will match exact, whole paths. But when registered on a + /// `Scope`, it will match paths under that scope, ignoring the defined scope prefix, just like + /// a normal `Resource` or `Route`. + /// + /// The `to` argument can be path or URL; whatever is provided shall be used verbatim when + /// setting the redirect location. This means that relative paths can be used to navigate + /// relatively to matched paths. + /// + /// Prefer [`Redirect::to()`](Self::to) when using `Redirect` as a responder since `from` has + /// no meaning in that context. + /// + /// # Examples + /// ``` + /// # use actix_web::{web::Redirect, App}; + /// App::new() + /// // redirects "/oh/hi/mark" to "/oh/bye/johnny" + /// .service(Redirect::new("/oh/hi/mark", "../../bye/johnny")); + /// ``` + pub fn new(from: impl Into>, to: impl Into>) -> Self { + Self { + from: from.into(), + to: to.into(), + status_code: StatusCode::TEMPORARY_REDIRECT, + } + } + + /// Construct a new `Redirect` to use as a responder. + /// + /// Only receives the `to` argument since responders do not need to do route matching. + /// + /// # Examples + /// ``` + /// use actix_web::web::Redirect; + /// + /// async fn admin_page() -> impl Responder { + /// // sends a temporary 307 redirect to the login path + /// Redirect::to("/login") + /// } + /// # actix_web::web::to(handler); + /// ``` + pub fn to(to: impl Into>) -> Self { + Self { + from: "/".into(), + to: to.into(), + status_code: StatusCode::TEMPORARY_REDIRECT, + } + } + + /// Use the "308 Permanent Redirect" status when responding. + /// + /// See [this MDN article][mdn-redirects] on why 308 is preferred over 301. + /// + /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections + pub fn permanent(self) -> Self { + self.using_status_code(StatusCode::PERMANENT_REDIRECT) + } + + /// Use the "307 Temporary Redirect" status when responding. + /// + /// See [this MDN article][mdn-redirects] on why 307 is preferred over 302. + /// + /// [mdn-redirects]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#temporary_redirections + pub fn temporary(self) -> Self { + self.using_status_code(StatusCode::TEMPORARY_REDIRECT) + } + + /// Use the "303 See Other" status when responding. + /// + /// This status code is semantically correct as the response to a successful login, for example. + pub fn see_other(self) -> Self { + self.using_status_code(StatusCode::SEE_OTHER) + } + + /// Allows the use of custom status codes for less common redirect types. + /// + /// In most cases, the default status ("308 Permanent Redirect") or using the `temporary` + /// method, which uses the "307 Temporary Redirect" status have more consistent behavior than + /// 301 and 302 codes, respectively. + /// + /// ``` + /// # use actix_web::{http::StatusCode, web::Redirect}; + /// // redirects would use "301 Moved Permanently" status code + /// Redirect::new("/old", "/new") + /// .using_status_code(StatusCode::MOVED_PERMANENTLY); + /// + /// // redirects would use "302 Found" status code + /// Redirect::new("/old", "/new") + /// .using_status_code(StatusCode::FOUND); + /// ``` + pub fn using_status_code(mut self, status: StatusCode) -> Self { + self.status_code = status; + self + } +} + +impl HttpServiceFactory for Redirect { + fn register(self, config: &mut AppService) { + let redirect = self.clone(); + let rdef = ResourceDef::new(self.from.into_owned()); + let redirect_factory = fn_service(move |mut req: ServiceRequest| { + let res = redirect.clone().respond_to(req.parts_mut().0); + ready(Ok(req.into_response(res.map_into_boxed_body()))) + }); + + config.register_service(rdef, None, redirect_factory, None) + } +} + +impl Responder for Redirect { + type Body = (); + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let mut res = HttpResponse::with_body(self.status_code, ()); + + if let Ok(hdr_val) = self.to.parse() { + res.headers_mut().insert(LOCATION, hdr_val); + } else { + log::error!( + "redirect target location can not be converted to header value: {:?}", + self.to + ); + } + + res + } +} + +#[cfg(test)] +mod tests { + use crate::{dev::Service, http::StatusCode, test, App}; + + use super::*; + + #[actix_rt::test] + async fn absolute_redirects() { + let redirector = Redirect::new("/one", "/two").permanent(); + + let svc = test::init_service(App::new().service(redirector)).await; + + let req = test::TestRequest::default().uri("/one").to_request(); + let res = svc.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::from_u16(308).unwrap()); + let hdr = res.headers().get(&LOCATION).unwrap(); + assert_eq!(hdr.to_str().unwrap(), "/two"); + } + + #[actix_rt::test] + async fn relative_redirects() { + let redirector = Redirect::new("/one", "two").permanent(); + + let svc = test::init_service(App::new().service(redirector)).await; + + let req = test::TestRequest::default().uri("/one").to_request(); + let res = svc.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::from_u16(308).unwrap()); + let hdr = res.headers().get(&LOCATION).unwrap(); + assert_eq!(hdr.to_str().unwrap(), "two"); + } + + #[actix_rt::test] + async fn temporary_redirects() { + let external_service = Redirect::new("/external", "https://duck.com"); + + let svc = test::init_service(App::new().service(external_service)).await; + + let req = test::TestRequest::default().uri("/external").to_request(); + let res = svc.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::from_u16(307).unwrap()); + let hdr = res.headers().get(&LOCATION).unwrap(); + assert_eq!(hdr.to_str().unwrap(), "https://duck.com"); + } + + #[actix_rt::test] + async fn as_responder() { + let responder = Redirect::to("https://duck.com"); + + let req = test::TestRequest::default().to_http_request(); + let res = responder.respond_to(&req); + + assert_eq!(res.status(), StatusCode::from_u16(307).unwrap()); + let hdr = res.headers().get(&LOCATION).unwrap(); + assert_eq!(hdr.to_str().unwrap(), "https://duck.com"); + } +} diff --git a/actix-web/src/service.rs b/actix-web/src/service.rs index 0ad92d8a1..ea23f09f5 100644 --- a/actix-web/src/service.rs +++ b/actix-web/src/service.rs @@ -327,9 +327,7 @@ impl ServiceRequest { .push(extensions); } - /// Creates a context object for use with a [guard](crate::guard). - /// - /// Useful if you are implementing + /// Creates a context object for use with a routing [guard](crate::guard). #[inline] pub fn guard_ctx(&self) -> GuardContext<'_> { GuardContext { req: self } diff --git a/actix-web/src/types/form.rs b/actix-web/src/types/form.rs index 9c09c6b73..d73f8ba74 100644 --- a/actix-web/src/types/form.rs +++ b/actix-web/src/types/form.rs @@ -35,6 +35,7 @@ use crate::{ /// /// Use [`FormConfig`] to configure extraction options. /// +/// ## Examples /// ``` /// use actix_web::{post, web}; /// use serde::Deserialize; @@ -46,20 +47,18 @@ use crate::{ /// /// // This handler is only called if: /// // - request headers declare the content type as `application/x-www-form-urlencoded` -/// // - request payload is deserialized into a `Info` struct from the URL encoded format +/// // - request payload deserializes into an `Info` struct from the URL encoded format /// #[post("/")] -/// async fn index(form: web::Form) -> String { +/// async fn index(web::Form(form): web::Form) -> String { /// format!("Welcome {}!", form.name) /// } /// ``` /// /// # Responder -/// The `Form` type also allows you to create URL encoded responses: -/// simply return a value of type Form where T is the type to be URL encoded. -/// The type must implement [`serde::Serialize`]. -/// -/// Responses use +/// The `Form` type also allows you to create URL encoded responses by returning a value of type +/// `Form` where `T` is the type to be URL encoded, as long as `T` implements [`Serialize`]. /// +/// ## Examples /// ``` /// use actix_web::{get, web}; /// use serde::Serialize; @@ -77,7 +76,7 @@ use crate::{ /// #[get("/")] /// async fn index() -> web::Form { /// web::Form(SomeForm { -/// name: "actix".into(), +/// name: "actix".to_owned(), /// age: 123 /// }) /// } diff --git a/actix-web/src/web.rs b/actix-web/src/web.rs index f5845d7f6..0533f7f8f 100644 --- a/actix-web/src/web.rs +++ b/actix-web/src/web.rs @@ -11,10 +11,12 @@ //! - [`Bytes`]: Raw payload //! //! # Responders -//! - [`Json`]: JSON request payload -//! - [`Bytes`]: Raw request payload +//! - [`Json`]: JSON response +//! - [`Form`]: URL-encoded response +//! - [`Bytes`]: Raw bytes response +//! - [`Redirect`](Redirect::to): Convenient redirect responses -use std::future::Future; +use std::{borrow::Cow, future::Future}; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; @@ -26,6 +28,7 @@ use crate::{ pub use crate::config::ServiceConfig; pub use crate::data::Data; +pub use crate::redirect::Redirect; pub use crate::request_data::ReqData; pub use crate::types::*; @@ -45,6 +48,7 @@ pub use crate::types::*; /// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store /// `userid` and `friend` in the exposed `Path` object: /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// @@ -74,6 +78,7 @@ pub fn resource(path: T) -> Resource { /// - `/{project_id}/path2` /// - `/{project_id}/path3` /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// @@ -183,6 +188,25 @@ pub fn service(path: T) -> WebService { WebService::new(path) } +/// Create a relative or absolute redirect. +/// +/// See [`Redirect`] docs for usage details. +/// +/// # Examples +/// ``` +/// use actix_web::{web, App}; +/// +/// let app = App::new() +/// // the client will resolve this redirect to /api/to-path +/// .service(web::redirect("/api/from-path", "to-path")); +/// ``` +pub fn redirect( + from: impl Into>, + to: impl Into>, +) -> Redirect { + Redirect::new(from, to) +} + /// Executes blocking function on a thread pool, returns future that resolves to result of the /// function execution. pub fn block(f: F) -> impl Future> diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 2f0027725..e7ac43d22 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,10 +1,7 @@ [package] name = "awc" version = "3.0.1" -authors = [ - "Nikolay Kim ", - "fakeshadow <24548779@qq.com>", -] +authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] categories = [