Merge branch 'master' into multipart-forms

This commit is contained in:
Rob Ede 2022-11-25 22:05:17 +00:00 committed by GitHub
commit 7344a0f4fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 452 additions and 118 deletions

View File

@ -5,6 +5,9 @@ on:
branches:
- master
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
check_benchmark:
runs-on: ubuntu-latest

View File

@ -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

View File

@ -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 }

View File

@ -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:

View File

@ -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 '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > 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

View File

@ -3,7 +3,6 @@ name = "actix-files"
version = "0.6.2"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Static file serving for Actix Web"

View File

@ -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

View File

@ -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]

View File

@ -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},

View File

@ -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},

View File

@ -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);

View File

@ -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::{

View File

@ -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};

View File

@ -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"

View File

@ -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<Option<Bytes>, 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 {

View File

@ -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"

View File

@ -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.
///

View File

@ -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

View File

@ -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::<Accept>() {
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()));
}
}

View File

@ -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> {

View File

@ -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<Mime>)*
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]

View File

@ -86,6 +86,7 @@ mod helpers;
pub mod http;
mod info;
pub mod middleware;
mod redirect;
mod request;
mod request_data;
mod resource;

238
actix-web/src/redirect.rs Normal file
View File

@ -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<Cow<'static, str>>, to: impl Into<Cow<'static, str>>) -> 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<Cow<'static, str>>) -> 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<Self::Body> {
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");
}
}

View File

@ -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 }

View File

@ -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<Info>) -> String {
/// async fn index(web::Form(form): web::Form<Info>) -> String {
/// format!("Welcome {}!", form.name)
/// }
/// ```
///
/// # Responder
/// The `Form` type also allows you to create URL encoded responses:
/// simply return a value of type Form<T> 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<T>` 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<SomeForm> {
/// web::Form(SomeForm {
/// name: "actix".into(),
/// name: "actix".to_owned(),
/// age: 123
/// })
/// }

View File

@ -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<T: IntoPatterns>(path: T) -> Resource {
/// - `/{project_id}/path2`
/// - `/{project_id}/path3`
///
/// # Examples
/// ```
/// use actix_web::{web, App, HttpResponse};
///
@ -183,6 +188,25 @@ pub fn service<T: IntoPatterns>(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<Cow<'static, str>>,
to: impl Into<Cow<'static, str>>,
) -> 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, R>(f: F) -> impl Future<Output = Result<R, BlockingError>>

View File

@ -1,10 +1,7 @@
[package]
name = "awc"
version = "3.0.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
]
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Async HTTP and WebSocket client library"
keywords = ["actix", "http", "framework", "async", "web"]
categories = [