mirror of
https://github.com/actix/actix-web.git
synced 2024-12-16 21:26:34 +00:00
Merge branch 'master' into fix/pending-stream-drop
This commit is contained in:
commit
2bbf0004fd
57 changed files with 337 additions and 145 deletions
3
.github/workflows/bench.yml
vendored
3
.github/workflows/bench.yml
vendored
|
@ -5,6 +5,9 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_benchmark:
|
check_benchmark:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
3
.github/workflows/ci-post-merge.yml
vendored
3
.github/workflows/ci-post-merge.yml
vendored
|
@ -4,6 +4,9 @@ on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_test_nightly:
|
build_and_test_nightly:
|
||||||
strategy:
|
strategy:
|
||||||
|
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -6,6 +6,9 @@ on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
strategy:
|
strategy:
|
||||||
|
|
6
.github/workflows/upload-doc.yml
vendored
6
.github/workflows/upload-doc.yml
vendored
|
@ -4,8 +4,12 @@ on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
permissions:
|
||||||
|
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
@ -28,7 +32,7 @@ jobs:
|
||||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.0
|
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
||||||
with:
|
with:
|
||||||
folder: target/doc
|
folder: target/doc
|
||||||
single-commit: true
|
single-commit: true
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2903]: https://github.com/actix/actix-web/pull/2903
|
||||||
|
|
||||||
## 0.6.2 - 2022-07-23
|
## 0.6.2 - 2022-07-23
|
||||||
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
||||||
|
|
|
@ -3,7 +3,6 @@ name = "actix-files"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"fakeshadow <24548779@qq.com>",
|
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
]
|
]
|
||||||
description = "Static file serving for Actix Web"
|
description = "Static file serving for Actix Web"
|
||||||
|
|
|
@ -132,7 +132,7 @@ impl NamedFile {
|
||||||
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
||||||
mime::APPLICATION => match ct.subtype() {
|
mime::APPLICATION => match ct.subtype() {
|
||||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||||
name if name == "wasm" => DispositionType::Inline,
|
name if name == "wasm" || name == "xhtml" => DispositionType::Inline,
|
||||||
_ => DispositionType::Attachment,
|
_ => DispositionType::Attachment,
|
||||||
},
|
},
|
||||||
_ => DispositionType::Attachment,
|
_ => DispositionType::Attachment,
|
||||||
|
|
|
@ -23,7 +23,7 @@ impl Deref for FilesService {
|
||||||
type Target = FilesServiceInner;
|
type Target = FilesServiceInner;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&*self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@
|
||||||
- Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868]
|
- Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868]
|
||||||
- Implement `MessageBody` for `Pin<B>` where `B::Target: MessageBody`. [#2868]
|
- Implement `MessageBody` for `Pin<B>` where `B::Target: MessageBody`. [#2868]
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
- Improve overall performance of operations on `Extensions`. [#2890]
|
||||||
|
|
||||||
[#2868]: https://github.com/actix/actix-web/pull/2868
|
[#2868]: https://github.com/actix/actix-web/pull/2868
|
||||||
|
[#2890]: https://github.com/actix/actix-web/pull/2890
|
||||||
|
|
||||||
|
|
||||||
## 3.2.2 - 2022-09-11
|
## 3.2.2 - 2022-09-11
|
||||||
|
|
|
@ -77,6 +77,8 @@ mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
smallvec = "1.6.1"
|
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"] }
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
# http2
|
# http2
|
||||||
|
|
|
@ -10,13 +10,13 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_codec::Encoder;
|
|
||||||
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
||||||
use actix_rt::time::{interval, Interval};
|
use actix_rt::time::{interval, Interval};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
use tokio_util::codec::Encoder;
|
||||||
use tracing::{info, trace};
|
use tracing::{info, trace};
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
|
|
|
@ -131,7 +131,7 @@ mod foreign_impls {
|
||||||
type Error = B::Error;
|
type Error = B::Error;
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
(&**self).size()
|
(**self).size()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll_next(
|
fn poll_next(
|
||||||
|
|
|
@ -42,7 +42,7 @@ pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||||
let body = body.as_mut();
|
let body = body.as_mut();
|
||||||
|
|
||||||
match ready!(body.poll_next(cx)) {
|
match ready!(body.poll_next(cx)) {
|
||||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
||||||
None => return Poll::Ready(Ok(())),
|
None => return Poll::Ready(Ok(())),
|
||||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,30 @@
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
|
collections::HashMap,
|
||||||
fmt,
|
fmt,
|
||||||
|
hash::{BuildHasherDefault, Hasher},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ahash::AHashMap;
|
/// A hasher for `TypeId`s that takes advantage of its known characteristics.
|
||||||
|
///
|
||||||
|
/// Author of `anymap` crate has done research on the topic:
|
||||||
|
/// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct NoOpHasher(u64);
|
||||||
|
|
||||||
|
impl Hasher for NoOpHasher {
|
||||||
|
fn write(&mut self, _bytes: &[u8]) {
|
||||||
|
unimplemented!("This NoOpHasher can only handle u64s")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_u64(&mut self, i: u64) {
|
||||||
|
self.0 = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&self) -> u64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A type map for request extensions.
|
/// A type map for request extensions.
|
||||||
///
|
///
|
||||||
|
@ -11,7 +32,7 @@ use ahash::AHashMap;
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Extensions {
|
pub struct Extensions {
|
||||||
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
||||||
map: AHashMap<TypeId, Box<dyn Any>>,
|
map: HashMap<TypeId, Box<dyn Any>, BuildHasherDefault<NoOpHasher>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extensions {
|
impl Extensions {
|
||||||
|
@ -19,7 +40,7 @@ impl Extensions {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Extensions {
|
pub fn new() -> Extensions {
|
||||||
Extensions {
|
Extensions {
|
||||||
map: AHashMap::new(),
|
map: HashMap::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
use actix_codec::{Decoder, Encoder};
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
|
use tokio_util::codec::{Decoder, Encoder};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
use actix_codec::{Decoder, Encoder};
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
|
use tokio_util::codec::{Decoder, Encoder};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||||
|
|
|
@ -8,13 +8,15 @@ use std::{
|
||||||
task::{Context, Poll},
|
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_rt::time::sleep_until;
|
||||||
use actix_service::Service;
|
use actix_service::Service;
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite};
|
||||||
|
use tokio_util::codec::{Decoder as _, Encoder as _};
|
||||||
use tracing::{error, trace};
|
use tracing::{error, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -1004,7 +1006,7 @@ where
|
||||||
this.read_buf.reserve(HW_BUFFER_SIZE - remaining);
|
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)) => {
|
Poll::Ready(Ok(n)) => {
|
||||||
this.flags.remove(Flags::FINISHED);
|
this.flags.remove(Flags::FINISHED);
|
||||||
|
|
||||||
|
|
|
@ -637,7 +637,7 @@ async fn expect_handling() {
|
||||||
|
|
||||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
let io = inner.io.as_ref().unwrap();
|
let io = inner.io.as_ref().unwrap();
|
||||||
let mut res = (&io.write_buf()[..]).to_owned();
|
let mut res = io.write_buf()[..].to_owned();
|
||||||
stabilize_date_header(&mut res);
|
stabilize_date_header(&mut res);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -699,7 +699,7 @@ async fn expect_eager() {
|
||||||
|
|
||||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
let io = inner.io.as_ref().unwrap();
|
let io = inner.io.as_ref().unwrap();
|
||||||
let mut res = (&io.write_buf()[..]).to_owned();
|
let mut res = io.write_buf()[..].to_owned();
|
||||||
stabilize_date_header(&mut res);
|
stabilize_date_header(&mut res);
|
||||||
|
|
||||||
// Despite the content-length header and even though the request payload has not
|
// Despite the content-length header and even though the request payload has not
|
||||||
|
|
|
@ -309,7 +309,7 @@ impl HeaderMap {
|
||||||
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
||||||
match self.get_value(key) {
|
match self.get_value(key) {
|
||||||
Some(value) => value.iter(),
|
Some(value) => value.iter(),
|
||||||
None => (&[]).iter(),
|
None => [].iter(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,14 +113,14 @@ impl<P> Request<P> {
|
||||||
#[inline]
|
#[inline]
|
||||||
/// Http message part of the request
|
/// Http message part of the request
|
||||||
pub fn head(&self) -> &RequestHead {
|
pub fn head(&self) -> &RequestHead {
|
||||||
&*self.head
|
&self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// Mutable reference to a HTTP message part of the request
|
/// Mutable reference to a HTTP message part of the request
|
||||||
pub fn head_mut(&mut self) -> &mut RequestHead {
|
pub fn head_mut(&mut self) -> &mut RequestHead {
|
||||||
&mut *self.head
|
&mut self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mutable reference to the message's headers.
|
/// Mutable reference to the message's headers.
|
||||||
|
|
|
@ -83,13 +83,13 @@ impl<B> Response<B> {
|
||||||
/// Returns a reference to the head of this response.
|
/// Returns a reference to the head of this response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn head(&self) -> &ResponseHead {
|
pub fn head(&self) -> &ResponseHead {
|
||||||
&*self.head
|
&self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a mutable reference to the head of this response.
|
/// Returns a mutable reference to the head of this response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn head_mut(&mut self) -> &mut ResponseHead {
|
pub fn head_mut(&mut self) -> &mut ResponseHead {
|
||||||
&mut *self.head
|
&mut self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the status code of this response.
|
/// Returns the status code of this response.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use actix_codec::{Decoder, Encoder};
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
|
use tokio_util::codec::{Decoder, Encoder};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
|
|
@ -76,7 +76,9 @@ mod inner {
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use tracing::debug;
|
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};
|
use crate::{body::BoxBody, Response};
|
||||||
|
|
||||||
|
|
|
@ -313,7 +313,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_frame_no_mask() {
|
fn test_parse_frame_no_mask() {
|
||||||
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
|
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
|
||||||
buf.extend(&[1u8]);
|
buf.extend([1u8]);
|
||||||
|
|
||||||
assert!(Parser::parse(&mut buf, true, 1024).is_err());
|
assert!(Parser::parse(&mut buf, true, 1024).is_err());
|
||||||
|
|
||||||
|
@ -326,7 +326,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_frame_max_size() {
|
fn test_parse_frame_max_size() {
|
||||||
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]);
|
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]);
|
||||||
buf.extend(&[1u8, 1u8]);
|
buf.extend([1u8, 1u8]);
|
||||||
|
|
||||||
assert!(Parser::parse(&mut buf, true, 1).is_err());
|
assert!(Parser::parse(&mut buf, true, 1).is_err());
|
||||||
|
|
||||||
|
@ -340,9 +340,9 @@ mod tests {
|
||||||
fn test_parse_frame_max_size_recoverability() {
|
fn test_parse_frame_max_size_recoverability() {
|
||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
// The first text frame with length == 2, payload doesn't matter.
|
// The first text frame with length == 2, payload doesn't matter.
|
||||||
buf.extend(&[0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
buf.extend([0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
||||||
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
|
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
|
||||||
buf.extend(&[0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
buf.extend([0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
||||||
|
|
||||||
assert_eq!(buf.len(), 8);
|
assert_eq!(buf.len(), 8);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
|
|
|
@ -244,7 +244,7 @@ pub fn hash_key(key: &[u8]) -> [u8; 28] {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut hash_b64 = [0; 28];
|
let mut hash_b64 = [0; 28];
|
||||||
let n = base64::encode_config_slice(&hash, base64::STANDARD, &mut hash_b64);
|
let n = base64::encode_config_slice(hash, base64::STANDARD, &mut hash_b64);
|
||||||
assert_eq!(n, 28);
|
assert_eq!(n, 28);
|
||||||
|
|
||||||
hash_b64
|
hash_b64
|
||||||
|
|
|
@ -41,7 +41,7 @@ where
|
||||||
let body = stream.as_mut();
|
let body = stream.as_mut();
|
||||||
|
|
||||||
match ready!(body.poll_next(cx)) {
|
match ready!(body.poll_next(cx)) {
|
||||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
||||||
None => return Poll::Ready(Ok(())),
|
None => return Poll::Ready(Ok(())),
|
||||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2880]
|
||||||
|
|
||||||
|
[#2880]: https://github.com/actix/actix-web/pull/2880
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0 - 2022-02-25
|
## 0.4.0 - 2022-02-25
|
||||||
|
|
|
@ -24,7 +24,7 @@ httparse = "1.3"
|
||||||
local-waker = "0.1"
|
local-waker = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
twoway = "0.2"
|
memchr = "2.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
|
|
|
@ -289,10 +289,8 @@ impl InnerMultipart {
|
||||||
match self.state {
|
match self.state {
|
||||||
// read until first boundary
|
// read until first boundary
|
||||||
InnerState::FirstBoundary => {
|
InnerState::FirstBoundary => {
|
||||||
match InnerMultipart::skip_until_boundary(
|
match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)?
|
||||||
&mut *payload,
|
{
|
||||||
&self.boundary,
|
|
||||||
)? {
|
|
||||||
Some(eof) => {
|
Some(eof) => {
|
||||||
if eof {
|
if eof {
|
||||||
self.state = InnerState::Eof;
|
self.state = InnerState::Eof;
|
||||||
|
@ -306,7 +304,7 @@ impl InnerMultipart {
|
||||||
}
|
}
|
||||||
// read boundary
|
// read boundary
|
||||||
InnerState::Boundary => {
|
InnerState::Boundary => {
|
||||||
match InnerMultipart::read_boundary(&mut *payload, &self.boundary)? {
|
match InnerMultipart::read_boundary(&mut payload, &self.boundary)? {
|
||||||
None => return Poll::Pending,
|
None => return Poll::Pending,
|
||||||
Some(eof) => {
|
Some(eof) => {
|
||||||
if eof {
|
if eof {
|
||||||
|
@ -323,7 +321,7 @@ impl InnerMultipart {
|
||||||
|
|
||||||
// read field headers for next field
|
// read field headers for next field
|
||||||
if self.state == InnerState::Headers {
|
if self.state == InnerState::Headers {
|
||||||
if let Some(headers) = InnerMultipart::read_headers(&mut *payload)? {
|
if let Some(headers) = InnerMultipart::read_headers(&mut payload)? {
|
||||||
self.state = InnerState::Boundary;
|
self.state = InnerState::Boundary;
|
||||||
headers
|
headers
|
||||||
} else {
|
} else {
|
||||||
|
@ -361,17 +359,18 @@ impl InnerMultipart {
|
||||||
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
|
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let ct: mime::Mime = headers
|
let ct: Option<mime::Mime> = headers
|
||||||
.get(&header::CONTENT_TYPE)
|
.get(&header::CONTENT_TYPE)
|
||||||
.and_then(|ct| ct.to_str().ok())
|
.and_then(|ct| ct.to_str().ok())
|
||||||
.and_then(|ct| ct.parse().ok())
|
.and_then(|ct| ct.parse().ok());
|
||||||
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
|
|
||||||
|
|
||||||
self.state = InnerState::Boundary;
|
self.state = InnerState::Boundary;
|
||||||
|
|
||||||
// nested multipart stream is not supported
|
// nested multipart stream is not supported
|
||||||
if ct.type_() == mime::MULTIPART {
|
if let Some(mime) = &ct {
|
||||||
return Poll::Ready(Some(Err(MultipartError::Nested)));
|
if mime.type_() == mime::MULTIPART {
|
||||||
|
return Poll::Ready(Some(Err(MultipartError::Nested)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let field =
|
let field =
|
||||||
|
@ -399,7 +398,7 @@ impl Drop for InnerMultipart {
|
||||||
|
|
||||||
/// A single field in a multipart stream
|
/// A single field in a multipart stream
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
ct: mime::Mime,
|
ct: Option<mime::Mime>,
|
||||||
cd: ContentDisposition,
|
cd: ContentDisposition,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
inner: Rc<RefCell<InnerField>>,
|
inner: Rc<RefCell<InnerField>>,
|
||||||
|
@ -410,7 +409,7 @@ impl Field {
|
||||||
fn new(
|
fn new(
|
||||||
safety: Safety,
|
safety: Safety,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
ct: mime::Mime,
|
ct: Option<mime::Mime>,
|
||||||
cd: ContentDisposition,
|
cd: ContentDisposition,
|
||||||
inner: Rc<RefCell<InnerField>>,
|
inner: Rc<RefCell<InnerField>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -428,9 +427,13 @@ impl Field {
|
||||||
&self.headers
|
&self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the field's content (mime) type.
|
/// Returns a reference to the field's content (mime) type, if it is supplied by the client.
|
||||||
pub fn content_type(&self) -> &mime::Mime {
|
///
|
||||||
&self.ct
|
/// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not
|
||||||
|
/// present, it should default to "text/plain". Note it is the responsibility of the client to
|
||||||
|
/// provide the appropriate content type, there is no attempt to validate this by the server.
|
||||||
|
pub fn content_type(&self) -> Option<&mime::Mime> {
|
||||||
|
self.ct.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the field's Content-Disposition.
|
/// Returns the field's Content-Disposition.
|
||||||
|
@ -482,7 +485,11 @@ impl Stream for Field {
|
||||||
|
|
||||||
impl fmt::Debug for Field {
|
impl fmt::Debug for Field {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "\nField: {}", self.ct)?;
|
if let Some(ct) = &self.ct {
|
||||||
|
writeln!(f, "\nField: {}", ct)?;
|
||||||
|
} else {
|
||||||
|
writeln!(f, "\nField:")?;
|
||||||
|
}
|
||||||
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
||||||
writeln!(f, " headers:")?;
|
writeln!(f, " headers:")?;
|
||||||
for (key, val) in self.headers.iter() {
|
for (key, val) in self.headers.iter() {
|
||||||
|
@ -599,7 +606,7 @@ impl InnerField {
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
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;
|
let cur = pos + idx;
|
||||||
|
|
||||||
// check if we have enough data for boundary detection
|
// check if we have enough data for boundary detection
|
||||||
|
@ -643,9 +650,9 @@ impl InnerField {
|
||||||
let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
||||||
if !self.eof {
|
if !self.eof {
|
||||||
let res = if let Some(ref mut len) = self.length {
|
let res = if let Some(ref mut len) = self.length {
|
||||||
InnerField::read_len(&mut *payload, len)
|
InnerField::read_len(&mut payload, len)
|
||||||
} else {
|
} else {
|
||||||
InnerField::read_stream(&mut *payload, &self.boundary)
|
InnerField::read_stream(&mut payload, &self.boundary)
|
||||||
};
|
};
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
|
@ -820,7 +827,7 @@ impl PayloadBuffer {
|
||||||
|
|
||||||
/// Read until specified ending
|
/// Read until specified ending
|
||||||
fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
|
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());
|
.map(|idx| self.buf.split_to(idx + line.len()).freeze());
|
||||||
|
|
||||||
if res.is_none() && self.eof {
|
if res.is_none() && self.eof {
|
||||||
|
@ -1024,8 +1031,8 @@ mod tests {
|
||||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||||
|
|
||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
match field.next().await.unwrap() {
|
match field.next().await.unwrap() {
|
||||||
Ok(chunk) => assert_eq!(chunk, "test"),
|
Ok(chunk) => assert_eq!(chunk, "test"),
|
||||||
|
@ -1041,8 +1048,8 @@ mod tests {
|
||||||
|
|
||||||
match multipart.next().await.unwrap() {
|
match multipart.next().await.unwrap() {
|
||||||
Ok(mut field) => {
|
Ok(mut field) => {
|
||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
match field.next().await {
|
match field.next().await {
|
||||||
Some(Ok(chunk)) => assert_eq!(chunk, "data"),
|
Some(Ok(chunk)) => assert_eq!(chunk, "data"),
|
||||||
|
@ -1086,8 +1093,8 @@ mod tests {
|
||||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||||
|
|
||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
assert_eq!(get_whole_field(&mut field).await, "test");
|
assert_eq!(get_whole_field(&mut field).await, "test");
|
||||||
}
|
}
|
||||||
|
@ -1096,8 +1103,8 @@ mod tests {
|
||||||
|
|
||||||
match multipart.next().await {
|
match multipart.next().await {
|
||||||
Some(Ok(mut field)) => {
|
Some(Ok(mut field)) => {
|
||||||
assert_eq!(field.content_type().type_(), mime::TEXT);
|
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
assert_eq!(get_whole_field(&mut field).await, "data");
|
assert_eq!(get_whole_field(&mut field).await, "data");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.5.1 - 2022-09-19
|
||||||
|
- Correct typo in error string for `i32` deserialization. [#2876]
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2876]: https://github.com/actix/actix-web/pull/2876
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0 - 2022-02-22
|
## 0.5.0 - 2022-02-22
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||||
|
|
|
@ -293,7 +293,7 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||||
parse_value!(deserialize_bool, visit_bool, "bool");
|
parse_value!(deserialize_bool, visit_bool, "bool");
|
||||||
parse_value!(deserialize_i8, visit_i8, "i8");
|
parse_value!(deserialize_i8, visit_i8, "i8");
|
||||||
parse_value!(deserialize_i16, visit_i16, "i16");
|
parse_value!(deserialize_i16, visit_i16, "i16");
|
||||||
parse_value!(deserialize_i32, visit_i32, "i16");
|
parse_value!(deserialize_i32, visit_i32, "i32");
|
||||||
parse_value!(deserialize_i64, visit_i64, "i64");
|
parse_value!(deserialize_i64, visit_i64, "i64");
|
||||||
parse_value!(deserialize_u8, visit_u8, "u8");
|
parse_value!(deserialize_u8, visit_u8, "u8");
|
||||||
parse_value!(deserialize_u16, visit_u16, "u16");
|
parse_value!(deserialize_u16, visit_u16, "u16");
|
||||||
|
|
|
@ -242,6 +242,7 @@ mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(clippy::needless_borrow)]
|
||||||
#[test]
|
#[test]
|
||||||
fn deref_impls() {
|
fn deref_impls() {
|
||||||
let mut foo = Path::new("/foo");
|
let mut foo = Path::new("/foo");
|
||||||
|
|
|
@ -1503,31 +1503,31 @@ mod tests {
|
||||||
fn build_path_list() {
|
fn build_path_list() {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/test");
|
let resource = ResourceDef::new("/user/{item1}/test");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
|
||||||
assert_eq!(s, "/user/user1/test");
|
assert_eq!(s, "/user/user1/test");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/{item2}/test");
|
let resource = ResourceDef::new("/user/{item1}/{item2}/test");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||||
assert_eq!(s, "/user/item/item2/test");
|
assert_eq!(s, "/user/item/item2/test");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/{item2}");
|
let resource = ResourceDef::new("/user/{item1}/{item2}");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||||
assert_eq!(s, "/user/item/item2");
|
assert_eq!(s, "/user/item/item2");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/{item2}/");
|
let resource = ResourceDef::new("/user/{item1}/{item2}/");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||||
assert_eq!(s, "/user/item/item2/");
|
assert_eq!(s, "/user/item/item2/");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter()));
|
assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
||||||
assert_eq!(s, "/user/item/item2/");
|
assert_eq!(s, "/user/item/item2/");
|
||||||
assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter()));
|
assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter()));
|
||||||
|
@ -1604,10 +1604,10 @@ mod tests {
|
||||||
let resource = ResourceDef::new("/user/{item1}*");
|
let resource = ResourceDef::new("/user/{item1}*");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter()));
|
assert!(!resource.resource_path_from_iter(&mut s, &mut [""; 0].iter()));
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
|
||||||
assert_eq!(s, "/user/user1");
|
assert_eq!(s, "/user/user1");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
|
|
|
@ -24,6 +24,7 @@ bytestring = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
tokio = { version = "1.13.1", features = ["sync"] }
|
tokio = { version = "1.13.1", features = ["sync"] }
|
||||||
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
|
|
|
@ -74,7 +74,6 @@ use actix::{
|
||||||
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
|
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
|
||||||
SpawnHandle,
|
SpawnHandle,
|
||||||
};
|
};
|
||||||
use actix_codec::{Decoder as _, Encoder as _};
|
|
||||||
use actix_http::ws::{hash_key, Codec};
|
use actix_http::ws::{hash_key, Codec};
|
||||||
pub use actix_http::ws::{
|
pub use actix_http::ws::{
|
||||||
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
|
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
|
||||||
|
@ -92,6 +91,7 @@ use bytestring::ByteString;
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
use tokio_util::codec::{Decoder as _, Encoder as _};
|
||||||
|
|
||||||
/// Builder for Websocket session response.
|
/// Builder for Websocket session response.
|
||||||
///
|
///
|
||||||
|
|
|
@ -15,7 +15,7 @@ edition = "2018"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-router = "0.5.0"
|
actix-router = "0.5"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "1", features = ["full", "extra-traits"] }
|
syn = { version = "1", features = ["full", "extra-traits"] }
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
### Added
|
### Added
|
||||||
- Add `ContentDisposition::attachment` constructor. [#2867]
|
- 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 `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]
|
||||||
|
|
||||||
|
[#2631]: https://github.com/actix/actix-web/pull/2631
|
||||||
[#2784]: https://github.com/actix/actix-web/pull/2784
|
[#2784]: https://github.com/actix/actix-web/pull/2784
|
||||||
[#2867]: https://github.com/actix/actix-web/pull/2867
|
[#2867]: https://github.com/actix/actix-web/pull/2867
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
cfg-if = "1"
|
cfg-if = "1"
|
||||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.8"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
|
|
@ -682,7 +682,7 @@ mod tests {
|
||||||
"/test",
|
"/test",
|
||||||
web::get().to(|req: HttpRequest| {
|
web::get().to(|req: HttpRequest| {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.body(req.url_for("youtube", &["12345"]).unwrap().to_string())
|
.body(req.url_for("youtube", ["12345"]).unwrap().to_string())
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -173,7 +173,7 @@ impl AppInitServiceState {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn rmap(&self) -> &ResourceMap {
|
pub(crate) fn rmap(&self) -> &ResourceMap {
|
||||||
&*self.rmap
|
&self.rmap
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -344,7 +344,7 @@ mod tests {
|
||||||
"/test",
|
"/test",
|
||||||
web::get().to(|req: HttpRequest| {
|
web::get().to(|req: HttpRequest| {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.body(req.url_for("youtube", &["12345"]).unwrap().to_string())
|
.body(req.url_for("youtube", ["12345"]).unwrap().to_string())
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,8 +6,7 @@ use super::{common_header, QualityItem};
|
||||||
use crate::http::header;
|
use crate::http::header;
|
||||||
|
|
||||||
common_header! {
|
common_header! {
|
||||||
/// `Accept` header, defined
|
/// `Accept` header, defined in [RFC 7231 §5.3.2].
|
||||||
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
|
|
||||||
///
|
///
|
||||||
/// The `Accept` header field can be used by user agents to specify
|
/// The `Accept` header field can be used by user agents to specify
|
||||||
/// response media types that are acceptable. Accept header fields can
|
/// 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>)*
|
(Accept, header::ACCEPT) => (QualityItem<Mime>)*
|
||||||
|
|
||||||
test_parse_and_format {
|
test_parse_and_format {
|
||||||
|
@ -101,13 +102,12 @@ common_header! {
|
||||||
vec![b"text/plain; charset=utf-8"],
|
vec![b"text/plain; charset=utf-8"],
|
||||||
Some(Accept(vec![
|
Some(Accept(vec![
|
||||||
QualityItem::max(mime::TEXT_PLAIN_UTF_8),
|
QualityItem::max(mime::TEXT_PLAIN_UTF_8),
|
||||||
])));
|
])));
|
||||||
crate::http::header::common_header_test!(
|
crate::http::header::common_header_test!(
|
||||||
test4,
|
test4,
|
||||||
vec![b"text/plain; charset=utf-8; q=0.5"],
|
vec![b"text/plain; charset=utf-8; q=0.5"],
|
||||||
Some(Accept(vec![
|
Some(Accept(vec![
|
||||||
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
|
QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)),
|
||||||
q(0.5)),
|
|
||||||
])));
|
])));
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -176,7 +176,7 @@ impl str::FromStr for CacheDirective {
|
||||||
|
|
||||||
_ => match s.find('=') {
|
_ => match s.find('=') {
|
||||||
Some(idx) if idx + 1 < s.len() => {
|
Some(idx) if idx + 1 < s.len() => {
|
||||||
match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) {
|
match (&s[..idx], s[idx + 1..].trim_matches('"')) {
|
||||||
("max-age", secs) => secs.parse().map(MaxAge).map_err(Some),
|
("max-age", secs) => secs.parse().map(MaxAge).map_err(Some),
|
||||||
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
|
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
|
||||||
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
|
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
|
||||||
|
|
|
@ -26,7 +26,7 @@ use crate::{
|
||||||
body::{BodySize, MessageBody},
|
body::{BodySize, MessageBody},
|
||||||
http::header::HeaderName,
|
http::header::HeaderName,
|
||||||
service::{ServiceRequest, ServiceResponse},
|
service::{ServiceRequest, ServiceResponse},
|
||||||
Error, HttpResponse, Result,
|
Error, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Middleware for logging request and response summaries to the terminal.
|
/// Middleware for logging request and response summaries to the terminal.
|
||||||
|
@ -69,10 +69,11 @@ use crate::{
|
||||||
/// `%D` | Time taken to serve the request, in milliseconds
|
/// `%D` | Time taken to serve the request, in milliseconds
|
||||||
/// `%U` | Request URL
|
/// `%U` | Request URL
|
||||||
/// `%{r}a` | "Real IP" remote address **\***
|
/// `%{r}a` | "Real IP" remote address **\***
|
||||||
/// `%{FOO}i` | `request.headers["FOO"]`
|
/// `%{FOO}i` | `request.headers["FOO"]`
|
||||||
/// `%{FOO}o` | `response.headers["FOO"]`
|
/// `%{FOO}o` | `response.headers["FOO"]`
|
||||||
/// `%{FOO}e` | `env_var["FOO"]`
|
/// `%{FOO}e` | `env_var["FOO"]`
|
||||||
/// `%{FOO}xi` | [Custom request replacement](Logger::custom_request_replace) labelled "FOO"
|
/// `%{FOO}xi` | [Custom request replacement](Logger::custom_request_replace) labelled "FOO"
|
||||||
|
/// `%{FOO}xo` | [Custom response replacement](Logger::custom_response_replace) labelled "FOO"
|
||||||
///
|
///
|
||||||
/// # Security
|
/// # Security
|
||||||
/// **\*** "Real IP" remote address is calculated using
|
/// **\*** "Real IP" remote address is calculated using
|
||||||
|
@ -179,6 +180,55 @@ impl Logger {
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a function that receives a `ServiceResponse` and returns a string for use in the
|
||||||
|
/// log line.
|
||||||
|
///
|
||||||
|
/// The label passed as the first argument should match a replacement substring in
|
||||||
|
/// the logger format like `%{label}xo`.
|
||||||
|
///
|
||||||
|
/// It is convention to print "-" to indicate no output instead of an empty string.
|
||||||
|
///
|
||||||
|
/// The replacement function does not have access to the response body.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_web::{dev::ServiceResponse, middleware::Logger};
|
||||||
|
/// fn log_if_error(res: &ServiceResponse) -> String {
|
||||||
|
/// if res.status().as_u16() >= 400 {
|
||||||
|
/// "ERROR".to_string()
|
||||||
|
/// } else {
|
||||||
|
/// "-".to_string()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// Logger::new("example %{ERROR_STATUS}xo")
|
||||||
|
/// .custom_response_replace("ERROR_STATUS", |res| log_if_error(res) );
|
||||||
|
/// ```
|
||||||
|
pub fn custom_response_replace(
|
||||||
|
mut self,
|
||||||
|
label: &str,
|
||||||
|
f: impl Fn(&ServiceResponse) -> String + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let inner = Rc::get_mut(&mut self.0).unwrap();
|
||||||
|
|
||||||
|
let ft = inner.format.0.iter_mut().find(
|
||||||
|
|ft| matches!(ft, FormatText::CustomResponse(unit_label, _) if label == unit_label),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(FormatText::CustomResponse(_, res_fn)) = ft {
|
||||||
|
*res_fn = Some(CustomResponseFn {
|
||||||
|
inner_fn: Rc::new(f),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"Attempted to register custom response logging function for non-existent label: {}",
|
||||||
|
label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Logger {
|
impl Default for Logger {
|
||||||
|
@ -210,10 +260,16 @@ where
|
||||||
|
|
||||||
fn new_transform(&self, service: S) -> Self::Future {
|
fn new_transform(&self, service: S) -> Self::Future {
|
||||||
for unit in &self.0.format.0 {
|
for unit in &self.0.format.0 {
|
||||||
// missing request replacement function diagnostic
|
|
||||||
if let FormatText::CustomRequest(label, None) = unit {
|
if let FormatText::CustomRequest(label, None) = unit {
|
||||||
warn!(
|
warn!(
|
||||||
"No custom request replacement function was registered for label \"{}\".",
|
"No custom request replacement function was registered for label: {}",
|
||||||
|
label
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let FormatText::CustomResponse(label, None) = unit {
|
||||||
|
warn!(
|
||||||
|
"No custom response replacement function was registered for label: {}",
|
||||||
label
|
label
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -308,11 +364,25 @@ where
|
||||||
debug!("Error in response: {:?}", error);
|
debug!("Error in response: {:?}", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut format) = this.format {
|
let res = if let Some(ref mut format) = this.format {
|
||||||
|
// to avoid polluting all the Logger types with the body parameter we swap the body
|
||||||
|
// out temporarily since it's not usable in custom response functions anyway
|
||||||
|
|
||||||
|
let (req, res) = res.into_parts();
|
||||||
|
let (res, body) = res.into_parts();
|
||||||
|
|
||||||
|
let temp_res = ServiceResponse::new(req, res.map_into_boxed_body());
|
||||||
|
|
||||||
for unit in &mut format.0 {
|
for unit in &mut format.0 {
|
||||||
unit.render_response(res.response());
|
unit.render_response(&temp_res);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// re-construct original service response
|
||||||
|
let (req, res) = temp_res.into_parts();
|
||||||
|
ServiceResponse::new(req, res.set_body(body))
|
||||||
|
} else {
|
||||||
|
res
|
||||||
|
};
|
||||||
|
|
||||||
let time = *this.time;
|
let time = *this.time;
|
||||||
let format = this.format.take();
|
let format = this.format.take();
|
||||||
|
@ -399,7 +469,7 @@ impl Format {
|
||||||
/// Returns `None` if the format string syntax is incorrect.
|
/// Returns `None` if the format string syntax is incorrect.
|
||||||
pub fn new(s: &str) -> Format {
|
pub fn new(s: &str) -> Format {
|
||||||
log::trace!("Access log format: {}", s);
|
log::trace!("Access log format: {}", s);
|
||||||
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[%atPrUsbTD]?)").unwrap();
|
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|x[io])|[%atPrUsbTD]?)").unwrap();
|
||||||
|
|
||||||
let mut idx = 0;
|
let mut idx = 0;
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
|
@ -417,7 +487,7 @@ impl Format {
|
||||||
if key.as_str() == "r" {
|
if key.as_str() == "r" {
|
||||||
FormatText::RealIpRemoteAddr
|
FormatText::RealIpRemoteAddr
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unreachable!("regex and code mismatch")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"i" => {
|
"i" => {
|
||||||
|
@ -428,6 +498,7 @@ impl Format {
|
||||||
}
|
}
|
||||||
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
|
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
|
||||||
"xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
|
"xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
|
||||||
|
"xo" => FormatText::CustomResponse(key.as_str().to_owned(), None),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -475,6 +546,7 @@ enum FormatText {
|
||||||
ResponseHeader(HeaderName),
|
ResponseHeader(HeaderName),
|
||||||
EnvironHeader(String),
|
EnvironHeader(String),
|
||||||
CustomRequest(String, Option<CustomRequestFn>),
|
CustomRequest(String, Option<CustomRequestFn>),
|
||||||
|
CustomResponse(String, Option<CustomResponseFn>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -494,6 +566,23 @@ impl fmt::Debug for CustomRequestFn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct CustomResponseFn {
|
||||||
|
inner_fn: Rc<dyn Fn(&ServiceResponse) -> String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomResponseFn {
|
||||||
|
fn call(&self, res: &ServiceResponse) -> String {
|
||||||
|
(self.inner_fn)(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for CustomResponseFn {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("custom_response_fn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FormatText {
|
impl FormatText {
|
||||||
fn render(
|
fn render(
|
||||||
&self,
|
&self,
|
||||||
|
@ -526,11 +615,12 @@ impl FormatText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_response<B>(&mut self, res: &HttpResponse<B>) {
|
fn render_response(&mut self, res: &ServiceResponse) {
|
||||||
match self {
|
match self {
|
||||||
FormatText::ResponseStatus => {
|
FormatText::ResponseStatus => {
|
||||||
*self = FormatText::Str(format!("{}", res.status().as_u16()))
|
*self = FormatText::Str(format!("{}", res.status().as_u16()))
|
||||||
}
|
}
|
||||||
|
|
||||||
FormatText::ResponseHeader(ref name) => {
|
FormatText::ResponseHeader(ref name) => {
|
||||||
let s = if let Some(val) = res.headers().get(name) {
|
let s = if let Some(val) = res.headers().get(name) {
|
||||||
if let Ok(s) = val.to_str() {
|
if let Ok(s) = val.to_str() {
|
||||||
|
@ -543,6 +633,16 @@ impl FormatText {
|
||||||
};
|
};
|
||||||
*self = FormatText::Str(s.to_string())
|
*self = FormatText::Str(s.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FormatText::CustomResponse(_, res_fn) => {
|
||||||
|
let text = match res_fn {
|
||||||
|
Some(res_fn) => FormatText::Str(res_fn.call(res)),
|
||||||
|
None => FormatText::Str("-".to_owned()),
|
||||||
|
};
|
||||||
|
|
||||||
|
*self = text;
|
||||||
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -627,8 +727,11 @@ mod tests {
|
||||||
use actix_utils::future::ok;
|
use actix_utils::future::ok;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::http::{header, StatusCode};
|
use crate::{
|
||||||
use crate::test::{self, TestRequest};
|
http::{header, StatusCode},
|
||||||
|
test::{self, TestRequest},
|
||||||
|
HttpResponse,
|
||||||
|
};
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_logger() {
|
async fn test_logger() {
|
||||||
|
@ -691,9 +794,10 @@ mod tests {
|
||||||
unit.render_request(now, &req);
|
unit.render_request(now, &req);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
|
||||||
for unit in &mut format.0 {
|
for unit in &mut format.0 {
|
||||||
unit.render_response(&resp);
|
unit.render_response(&res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry_time = OffsetDateTime::now_utc();
|
let entry_time = OffsetDateTime::now_utc();
|
||||||
|
@ -723,9 +827,10 @@ mod tests {
|
||||||
unit.render_request(now, &req);
|
unit.render_request(now, &req);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
|
||||||
for unit in &mut format.0 {
|
for unit in &mut format.0 {
|
||||||
unit.render_response(&resp);
|
unit.render_response(&res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let render = |fmt: &mut fmt::Formatter<'_>| {
|
let render = |fmt: &mut fmt::Formatter<'_>| {
|
||||||
|
@ -755,9 +860,10 @@ mod tests {
|
||||||
unit.render_request(now, &req);
|
unit.render_request(now, &req);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
|
||||||
for unit in &mut format.0 {
|
for unit in &mut format.0 {
|
||||||
unit.render_response(&resp);
|
unit.render_response(&res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry_time = OffsetDateTime::now_utc();
|
let entry_time = OffsetDateTime::now_utc();
|
||||||
|
@ -784,9 +890,10 @@ mod tests {
|
||||||
unit.render_request(now, &req);
|
unit.render_request(now, &req);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
|
||||||
for unit in &mut format.0 {
|
for unit in &mut format.0 {
|
||||||
unit.render_response(&resp);
|
unit.render_response(&res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let render = |fmt: &mut fmt::Formatter<'_>| {
|
let render = |fmt: &mut fmt::Formatter<'_>| {
|
||||||
|
@ -815,9 +922,10 @@ mod tests {
|
||||||
unit.render_request(now, &req);
|
unit.render_request(now, &req);
|
||||||
}
|
}
|
||||||
|
|
||||||
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
|
||||||
for unit in &mut format.0 {
|
for unit in &mut format.0 {
|
||||||
unit.render_response(&resp);
|
unit.render_response(&res);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry_time = OffsetDateTime::now_utc();
|
let entry_time = OffsetDateTime::now_utc();
|
||||||
|
@ -832,7 +940,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_custom_closure_log() {
|
async fn test_custom_closure_req_log() {
|
||||||
let mut logger = Logger::new("test %{CUSTOM}xi")
|
let mut logger = Logger::new("test %{CUSTOM}xi")
|
||||||
.custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
|
.custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
|
||||||
String::from("custom_log")
|
String::from("custom_log")
|
||||||
|
@ -857,6 +965,38 @@ mod tests {
|
||||||
assert_eq!(log_output, "custom_log");
|
assert_eq!(log_output, "custom_log");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_custom_closure_response_log() {
|
||||||
|
let mut logger = Logger::new("test %{CUSTOM}xo").custom_response_replace(
|
||||||
|
"CUSTOM",
|
||||||
|
|res: &ServiceResponse| -> String {
|
||||||
|
if res.status().as_u16() == 200 {
|
||||||
|
String::from("custom_log")
|
||||||
|
} else {
|
||||||
|
String::from("-")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
|
||||||
|
|
||||||
|
let label = match &unit {
|
||||||
|
FormatText::CustomResponse(label, _) => label,
|
||||||
|
ft => panic!("expected CustomResponse, found {:?}", ft),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(label, "CUSTOM");
|
||||||
|
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let resp_ok = ServiceResponse::new(req, HttpResponse::Ok().finish());
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
unit.render_response(&resp_ok);
|
||||||
|
|
||||||
|
let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
|
||||||
|
|
||||||
|
let log_output = FormatDisplay(&render).to_string();
|
||||||
|
assert_eq!(log_output, "custom_log");
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_closure_logger_in_middleware() {
|
async fn test_closure_logger_in_middleware() {
|
||||||
let captured = "custom log replacement";
|
let captured = "custom log replacement";
|
||||||
|
|
|
@ -219,7 +219,7 @@ impl HttpRequest {
|
||||||
/// for urls that do not contain variable parts.
|
/// for urls that do not contain variable parts.
|
||||||
pub fn url_for_static(&self, name: &str) -> Result<url::Url, UrlGenerationError> {
|
pub fn url_for_static(&self, name: &str) -> Result<url::Url, UrlGenerationError> {
|
||||||
const NO_PARAMS: [&str; 0] = [];
|
const NO_PARAMS: [&str; 0] = [];
|
||||||
self.url_for(name, &NO_PARAMS)
|
self.url_for(name, NO_PARAMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a `ResourceMap` of current application.
|
/// Get a reference to a `ResourceMap` of current application.
|
||||||
|
@ -306,7 +306,7 @@ impl HttpRequest {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn app_state(&self) -> &AppInitServiceState {
|
fn app_state(&self) -> &AppInitServiceState {
|
||||||
&*self.inner.app_state
|
&self.inner.app_state
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load request cookies.
|
/// Load request cookies.
|
||||||
|
@ -583,14 +583,14 @@ mod tests {
|
||||||
.to_http_request();
|
.to_http_request();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
req.url_for("unknown", &["test"]),
|
req.url_for("unknown", ["test"]),
|
||||||
Err(UrlGenerationError::ResourceNotFound)
|
Err(UrlGenerationError::ResourceNotFound)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
req.url_for("index", &["test"]),
|
req.url_for("index", ["test"]),
|
||||||
Err(UrlGenerationError::NotEnoughElements)
|
Err(UrlGenerationError::NotEnoughElements)
|
||||||
);
|
);
|
||||||
let url = req.url_for("index", &["test", "html"]);
|
let url = req.url_for("index", ["test", "html"]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
url.ok().unwrap().as_str(),
|
url.ok().unwrap().as_str(),
|
||||||
"http://www.rust-lang.org/user/test.html"
|
"http://www.rust-lang.org/user/test.html"
|
||||||
|
@ -646,7 +646,7 @@ mod tests {
|
||||||
rmap.add(&mut rdef, None);
|
rmap.add(&mut rdef, None);
|
||||||
|
|
||||||
let req = TestRequest::default().rmap(rmap).to_http_request();
|
let req = TestRequest::default().rmap(rmap).to_http_request();
|
||||||
let url = req.url_for("youtube", &["oHg5SJYRHA0"]);
|
let url = req.url_for("youtube", ["oHg5SJYRHA0"]);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
url.ok().unwrap().as_str(),
|
url.ok().unwrap().as_str(),
|
||||||
"https://youtube.com/watch/oHg5SJYRHA0"
|
"https://youtube.com/watch/oHg5SJYRHA0"
|
||||||
|
|
|
@ -457,7 +457,7 @@ mod tests {
|
||||||
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
||||||
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||||
|
|
||||||
let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]);
|
let res = HttpResponse::Ok().json(["v1", "v2", "v3"]);
|
||||||
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
let ct = res.headers().get(CONTENT_TYPE).unwrap();
|
||||||
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
assert_eq!(ct, HeaderValue::from_static("application/json"));
|
||||||
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
|
||||||
|
|
|
@ -449,12 +449,12 @@ mod tests {
|
||||||
let req = req.to_http_request();
|
let req = req.to_http_request();
|
||||||
|
|
||||||
let url = rmap
|
let url = rmap
|
||||||
.url_for(&req, "post", &["u123", "foobar"])
|
.url_for(&req, "post", ["u123", "foobar"])
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string();
|
.to_string();
|
||||||
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
|
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
|
||||||
|
|
||||||
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
|
assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -490,7 +490,7 @@ mod tests {
|
||||||
assert_eq!(url.path(), OUTPUT);
|
assert_eq!(url.path(), OUTPUT);
|
||||||
|
|
||||||
assert!(rmap.url_for(&req, "external.2", INPUT).is_err());
|
assert!(rmap.url_for(&req, "external.2", INPUT).is_err());
|
||||||
assert!(rmap.url_for(&req, "external.2", &[""]).is_err());
|
assert!(rmap.url_for(&req, "external.2", [""]).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -524,7 +524,7 @@ mod tests {
|
||||||
let req = req.to_http_request();
|
let req = req.to_http_request();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(),
|
rmap.url_for(&req, "duck", ["abcd"]).unwrap().to_string(),
|
||||||
"https://duck.com/abcd"
|
"https://duck.com/abcd"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -552,9 +552,9 @@ mod tests {
|
||||||
|
|
||||||
let req = crate::test::TestRequest::default().to_http_request();
|
let req = crate::test::TestRequest::default().to_http_request();
|
||||||
|
|
||||||
let url = rmap.url_for(&req, "nested", &[""; 0]).unwrap().to_string();
|
let url = rmap.url_for(&req, "nested", [""; 0]).unwrap().to_string();
|
||||||
assert_eq!(url, "http://localhost:8080/bar/nested");
|
assert_eq!(url, "http://localhost:8080/bar/nested");
|
||||||
|
|
||||||
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
|
assert!(rmap.url_for(&req, "missing", ["u123"]).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Running Actix Web Using `#[tokio::main]`
|
//! # Running Actix Web Using `#[tokio::main]`
|
||||||
//! If you need to run something alongside Actix Web that uses Tokio's work stealing functionality,
|
//! If you need to run something that uses Tokio's work stealing functionality alongside Actix Web,
|
||||||
//! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned
|
//! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned
|
||||||
//! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred.
|
//! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred.
|
||||||
//!
|
//!
|
||||||
|
|
|
@ -1133,7 +1133,7 @@ mod tests {
|
||||||
"/",
|
"/",
|
||||||
web::get().to(|req: HttpRequest| {
|
web::get().to(|req: HttpRequest| {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
.body(req.url_for("youtube", &["xxxxxx"]).unwrap().to_string())
|
.body(req.url_for("youtube", ["xxxxxx"]).unwrap().to_string())
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
|
@ -1152,8 +1152,7 @@ mod tests {
|
||||||
let srv = init_service(App::new().service(web::scope("/a").service(
|
let srv = init_service(App::new().service(web::scope("/a").service(
|
||||||
web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(
|
web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(
|
||||||
web::get().to(|req: HttpRequest| {
|
web::get().to(|req: HttpRequest| {
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok().body(format!("{}", req.url_for("c", ["12345"]).unwrap()))
|
||||||
.body(format!("{}", req.url_for("c", &["12345"]).unwrap()))
|
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
)))
|
)))
|
||||||
|
|
|
@ -327,9 +327,7 @@ impl ServiceRequest {
|
||||||
.push(extensions);
|
.push(extensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a context object for use with a [guard](crate::guard).
|
/// Creates a context object for use with a routing [guard](crate::guard).
|
||||||
///
|
|
||||||
/// Useful if you are implementing
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn guard_ctx(&self) -> GuardContext<'_> {
|
pub fn guard_ctx(&self) -> GuardContext<'_> {
|
||||||
GuardContext { req: self }
|
GuardContext { req: self }
|
||||||
|
|
|
@ -290,7 +290,7 @@ const DEFAULT_CONFIG: JsonConfig = JsonConfig {
|
||||||
|
|
||||||
impl Default for JsonConfig {
|
impl Default for JsonConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DEFAULT_CONFIG.clone()
|
DEFAULT_CONFIG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -271,7 +271,7 @@ const DEFAULT_CONFIG: PayloadConfig = PayloadConfig {
|
||||||
|
|
||||||
impl Default for PayloadConfig {
|
impl Default for PayloadConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DEFAULT_CONFIG.clone()
|
DEFAULT_CONFIG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "awc"
|
name = "awc"
|
||||||
version = "3.0.1"
|
version = "3.0.1"
|
||||||
authors = [
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
|
||||||
"fakeshadow <24548779@qq.com>",
|
|
||||||
]
|
|
||||||
description = "Async HTTP and WebSocket client library"
|
description = "Async HTTP and WebSocket client library"
|
||||||
keywords = ["actix", "http", "framework", "async", "web"]
|
keywords = ["actix", "http", "framework", "async", "web"]
|
||||||
categories = [
|
categories = [
|
||||||
|
|
|
@ -97,7 +97,7 @@ where
|
||||||
type Target = ConnectionPoolInnerPriv<Io>;
|
type Target = ConnectionPoolInnerPriv<Io>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&*self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -321,7 +321,7 @@ impl WebsocketsRequest {
|
||||||
// Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded
|
// Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded
|
||||||
// (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3).
|
// (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3).
|
||||||
let sec_key: [u8; 16] = rand::random();
|
let sec_key: [u8; 16] = rand::random();
|
||||||
let key = base64::encode(&sec_key);
|
let key = base64::encode(sec_key);
|
||||||
|
|
||||||
self.head.headers.insert(
|
self.head.headers.insert(
|
||||||
header::SEC_WEBSOCKET_KEY,
|
header::SEC_WEBSOCKET_KEY,
|
||||||
|
@ -513,7 +513,7 @@ mod tests {
|
||||||
.origin("test-origin")
|
.origin("test-origin")
|
||||||
.max_frame_size(100)
|
.max_frame_size(100)
|
||||||
.server_mode()
|
.server_mode()
|
||||||
.protocols(&["v1", "v2"])
|
.protocols(["v1", "v2"])
|
||||||
.set_header_if_none(header::CONTENT_TYPE, "json")
|
.set_header_if_none(header::CONTENT_TYPE, "json")
|
||||||
.set_header_if_none(header::CONTENT_TYPE, "text")
|
.set_header_if_none(header::CONTENT_TYPE, "text")
|
||||||
.cookie(Cookie::build("cookie1", "value1").finish());
|
.cookie(Cookie::build("cookie1", "value1").finish());
|
||||||
|
|
|
@ -707,8 +707,7 @@ async fn client_cookie_handling() {
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
// Check cookies were sent correctly
|
// Check cookies were sent correctly
|
||||||
let res: Result<(), Error> = req
|
req.cookie("cookie1")
|
||||||
.cookie("cookie1")
|
|
||||||
.ok_or(())
|
.ok_or(())
|
||||||
.and_then(|c1| {
|
.and_then(|c1| {
|
||||||
if c1.value() == "value1" {
|
if c1.value() == "value1" {
|
||||||
|
@ -725,16 +724,10 @@ async fn client_cookie_handling() {
|
||||||
Err(())
|
Err(())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map_err(|_| Error::from(IoError::from(ErrorKind::NotFound)));
|
.map_err(|_| Error::from(IoError::from(ErrorKind::NotFound)))?;
|
||||||
|
|
||||||
if let Err(e) = res {
|
// Send some cookies back
|
||||||
Err(e)
|
Ok::<_, Error>(HttpResponse::Ok().cookie(cookie1).cookie(cookie2).finish())
|
||||||
} else {
|
|
||||||
// Send some cookies back
|
|
||||||
Ok::<_, Error>(
|
|
||||||
HttpResponse::Ok().cookie(cookie1).cookie(cookie2).finish(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue