mirror of
https://github.com/actix/actix-extras.git
synced 2024-11-30 18:34:36 +01:00
Initial config for static files (#405)
This commit is contained in:
parent
f6e35a04f0
commit
a751df2589
@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
* Add `fs::StaticFileConfig` to provide means of customizing static file services. It allows to map
|
||||||
|
`mime` to `Content-Disposition`, specify whether to use `ETag` and `Last-Modified` and allowed methods.
|
||||||
|
|
||||||
* Add `.has_prefixed_resource()` method to `router::ResourceInfo` for route matching with prefix awareness
|
* Add `.has_prefixed_resource()` method to `router::ResourceInfo` for route matching with prefix awareness
|
||||||
|
|
||||||
* Add `HttpMessage::readlines()` for reading line by line.
|
* Add `HttpMessage::readlines()` for reading line by line.
|
||||||
|
231
src/fs.rs
231
src/fs.rs
@ -6,6 +6,7 @@ use std::ops::{Deref, DerefMut};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use std::{cmp, io};
|
use std::{cmp, io};
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
@ -27,6 +28,73 @@ use httprequest::HttpRequest;
|
|||||||
use httpresponse::HttpResponse;
|
use httpresponse::HttpResponse;
|
||||||
use param::FromParam;
|
use param::FromParam;
|
||||||
use server::settings::DEFAULT_CPUPOOL;
|
use server::settings::DEFAULT_CPUPOOL;
|
||||||
|
use header::{ContentDisposition, DispositionParam, DispositionType};
|
||||||
|
|
||||||
|
///Describes `StaticFiles` configiration
|
||||||
|
///
|
||||||
|
///To configure actix's static resources you need
|
||||||
|
///to define own configiration type and implement any method
|
||||||
|
///you wish to customize.
|
||||||
|
///As trait implements reasonable defaults for Actix.
|
||||||
|
///
|
||||||
|
///## Example
|
||||||
|
///
|
||||||
|
///```rust
|
||||||
|
/// extern crate mime;
|
||||||
|
/// extern crate actix_web;
|
||||||
|
/// use actix_web::http::header::DispositionType;
|
||||||
|
/// use actix_web::fs::{StaticFileConfig, NamedFile};
|
||||||
|
///
|
||||||
|
/// #[derive(Default)]
|
||||||
|
/// struct MyConfig;
|
||||||
|
///
|
||||||
|
/// impl StaticFileConfig for MyConfig {
|
||||||
|
/// fn content_disposition_map(typ: mime::Name) -> DispositionType {
|
||||||
|
/// DispositionType::Attachment
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let file = NamedFile::open_with_config("foo.txt", MyConfig);
|
||||||
|
///```
|
||||||
|
pub trait StaticFileConfig: Default {
|
||||||
|
///Describes mapping for mime type to content disposition header
|
||||||
|
///
|
||||||
|
///By default `IMAGE`, `TEXT` and `VIDEO` are mapped to Inline.
|
||||||
|
///Others are mapped to Attachment
|
||||||
|
fn content_disposition_map(typ: mime::Name) -> DispositionType {
|
||||||
|
match typ {
|
||||||
|
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||||
|
_ => DispositionType::Attachment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Describes whether Actix should attempt to calculate `ETag`
|
||||||
|
///
|
||||||
|
///Defaults to `true`
|
||||||
|
fn is_use_etag() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
///Describes whether Actix should use last modified date of file.
|
||||||
|
///
|
||||||
|
///Defaults to `true`
|
||||||
|
fn is_use_last_modifier() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
///Describes allowed methods to access static resources.
|
||||||
|
///
|
||||||
|
///By default all methods are allowed
|
||||||
|
fn is_method_allowed(_method: &Method) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///Default content disposition as described in
|
||||||
|
///[StaticFileConfig](trait.StaticFileConfig.html)
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DefaultConfig;
|
||||||
|
impl StaticFileConfig for DefaultConfig {}
|
||||||
|
|
||||||
/// Return the MIME type associated with a filename extension (case-insensitive).
|
/// Return the MIME type associated with a filename extension (case-insensitive).
|
||||||
/// If `ext` is empty or no associated type for the extension was found, returns
|
/// If `ext` is empty or no associated type for the extension was found, returns
|
||||||
@ -38,7 +106,7 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
|||||||
|
|
||||||
/// A file with an associated name.
|
/// A file with an associated name.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NamedFile {
|
pub struct NamedFile<C=DefaultConfig> {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
file: File,
|
file: File,
|
||||||
content_type: mime::Mime,
|
content_type: mime::Mime,
|
||||||
@ -47,8 +115,8 @@ pub struct NamedFile {
|
|||||||
modified: Option<SystemTime>,
|
modified: Option<SystemTime>,
|
||||||
cpu_pool: Option<CpuPool>,
|
cpu_pool: Option<CpuPool>,
|
||||||
encoding: Option<ContentEncoding>,
|
encoding: Option<ContentEncoding>,
|
||||||
only_get: bool,
|
|
||||||
status_code: StatusCode,
|
status_code: StatusCode,
|
||||||
|
_cd_map: PhantomData<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NamedFile {
|
impl NamedFile {
|
||||||
@ -62,7 +130,21 @@ impl NamedFile {
|
|||||||
/// let file = NamedFile::open("foo.txt");
|
/// let file = NamedFile::open("foo.txt");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||||
use header::{ContentDisposition, DispositionParam, DispositionType};
|
Self::open_with_config(path, DefaultConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: StaticFileConfig> NamedFile<C> {
|
||||||
|
/// Attempts to open a file in read-only mode using provided configiration.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use actix_web::fs::{DefaultConfig, NamedFile};
|
||||||
|
///
|
||||||
|
/// let file = NamedFile::open_with_config("foo.txt", DefaultConfig);
|
||||||
|
/// ```
|
||||||
|
pub fn open_with_config<P: AsRef<Path>>(path: P, _: C) -> io::Result<NamedFile<C>> {
|
||||||
let path = path.as_ref().to_path_buf();
|
let path = path.as_ref().to_path_buf();
|
||||||
|
|
||||||
// Get the name of the file and use it to construct default Content-Type
|
// Get the name of the file and use it to construct default Content-Type
|
||||||
@ -79,10 +161,7 @@ impl NamedFile {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let ct = guess_mime_type(&path);
|
let ct = guess_mime_type(&path);
|
||||||
let disposition_type = match ct.type_() {
|
let disposition_type = C::content_disposition_map(ct.type_());
|
||||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
|
||||||
_ => DispositionType::Attachment,
|
|
||||||
};
|
|
||||||
let cd = ContentDisposition {
|
let cd = ContentDisposition {
|
||||||
disposition: disposition_type,
|
disposition: disposition_type,
|
||||||
parameters: vec![DispositionParam::Filename(
|
parameters: vec![DispositionParam::Filename(
|
||||||
@ -108,18 +187,11 @@ impl NamedFile {
|
|||||||
modified,
|
modified,
|
||||||
cpu_pool,
|
cpu_pool,
|
||||||
encoding,
|
encoding,
|
||||||
only_get: false,
|
|
||||||
status_code: StatusCode::OK,
|
status_code: StatusCode::OK,
|
||||||
|
_cd_map: PhantomData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allow only GET and HEAD methods
|
|
||||||
#[inline]
|
|
||||||
pub fn only_get(mut self) -> Self {
|
|
||||||
self.only_get = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns reference to the underlying `File` object.
|
/// Returns reference to the underlying `File` object.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn file(&self) -> &File {
|
pub fn file(&self) -> &File {
|
||||||
@ -218,7 +290,7 @@ impl NamedFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for NamedFile {
|
impl<C> Deref for NamedFile<C> {
|
||||||
type Target = File;
|
type Target = File;
|
||||||
|
|
||||||
fn deref(&self) -> &File {
|
fn deref(&self) -> &File {
|
||||||
@ -226,7 +298,7 @@ impl Deref for NamedFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DerefMut for NamedFile {
|
impl<C> DerefMut for NamedFile<C> {
|
||||||
fn deref_mut(&mut self) -> &mut File {
|
fn deref_mut(&mut self) -> &mut File {
|
||||||
&mut self.file
|
&mut self.file
|
||||||
}
|
}
|
||||||
@ -267,7 +339,7 @@ fn none_match<S>(etag: Option<&header::EntityTag>, req: &HttpRequest<S>) -> bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Responder for NamedFile {
|
impl<C: StaticFileConfig> Responder for NamedFile<C> {
|
||||||
type Item = HttpResponse;
|
type Item = HttpResponse;
|
||||||
type Error = io::Error;
|
type Error = io::Error;
|
||||||
|
|
||||||
@ -294,7 +366,7 @@ impl Responder for NamedFile {
|
|||||||
return Ok(resp.streaming(reader));
|
return Ok(resp.streaming(reader));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.only_get && *req.method() != Method::GET && *req.method() != Method::HEAD
|
if !C::is_method_allowed(req.method())
|
||||||
{
|
{
|
||||||
return Ok(HttpResponse::MethodNotAllowed()
|
return Ok(HttpResponse::MethodNotAllowed()
|
||||||
.header(header::CONTENT_TYPE, "text/plain")
|
.header(header::CONTENT_TYPE, "text/plain")
|
||||||
@ -302,8 +374,14 @@ impl Responder for NamedFile {
|
|||||||
.body("This resource only supports GET and HEAD."));
|
.body("This resource only supports GET and HEAD."));
|
||||||
}
|
}
|
||||||
|
|
||||||
let etag = self.etag();
|
let etag = match C::is_use_etag() {
|
||||||
let last_modified = self.last_modified();
|
true => self.etag(),
|
||||||
|
false => None,
|
||||||
|
};
|
||||||
|
let last_modified = match C::is_use_last_modifier() {
|
||||||
|
true => self.last_modified(),
|
||||||
|
false => None,
|
||||||
|
};
|
||||||
|
|
||||||
// check preconditions
|
// check preconditions
|
||||||
let precondition_failed = if !any_match(etag.as_ref(), req) {
|
let precondition_failed = if !any_match(etag.as_ref(), req) {
|
||||||
@ -559,7 +637,7 @@ fn directory_listing<S>(
|
|||||||
/// .finish();
|
/// .finish();
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct StaticFiles<S> {
|
pub struct StaticFiles<S, C=DefaultConfig> {
|
||||||
directory: PathBuf,
|
directory: PathBuf,
|
||||||
index: Option<String>,
|
index: Option<String>,
|
||||||
show_index: bool,
|
show_index: bool,
|
||||||
@ -568,6 +646,7 @@ pub struct StaticFiles<S> {
|
|||||||
renderer: Box<DirectoryRenderer<S>>,
|
renderer: Box<DirectoryRenderer<S>>,
|
||||||
_chunk_size: usize,
|
_chunk_size: usize,
|
||||||
_follow_symlinks: bool,
|
_follow_symlinks: bool,
|
||||||
|
_cd_map: PhantomData<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: 'static> StaticFiles<S> {
|
impl<S: 'static> StaticFiles<S> {
|
||||||
@ -577,10 +656,7 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
/// By default pool with 20 threads is used.
|
/// By default pool with 20 threads is used.
|
||||||
/// Pool size can be changed by setting ACTIX_CPU_POOL environment variable.
|
/// Pool size can be changed by setting ACTIX_CPU_POOL environment variable.
|
||||||
pub fn new<T: Into<PathBuf>>(dir: T) -> Result<StaticFiles<S>, Error> {
|
pub fn new<T: Into<PathBuf>>(dir: T) -> Result<StaticFiles<S>, Error> {
|
||||||
// use default CpuPool
|
Self::with_config(dir, DefaultConfig)
|
||||||
let pool = { DEFAULT_CPUPOOL.lock().clone() };
|
|
||||||
|
|
||||||
StaticFiles::with_pool(dir, pool)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new `StaticFiles` instance for specified base directory and
|
/// Create new `StaticFiles` instance for specified base directory and
|
||||||
@ -588,6 +664,26 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
pub fn with_pool<T: Into<PathBuf>>(
|
pub fn with_pool<T: Into<PathBuf>>(
|
||||||
dir: T, pool: CpuPool,
|
dir: T, pool: CpuPool,
|
||||||
) -> Result<StaticFiles<S>, Error> {
|
) -> Result<StaticFiles<S>, Error> {
|
||||||
|
Self::with_config_pool(dir, pool, DefaultConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: 'static, C: StaticFileConfig> StaticFiles<S, C> {
|
||||||
|
/// Create new `StaticFiles` instance for specified base directory.
|
||||||
|
///
|
||||||
|
/// Identical with `new` but allows to specify configiration to use.
|
||||||
|
pub fn with_config<T: Into<PathBuf>>(dir: T, config: C) -> Result<StaticFiles<S, C>, Error> {
|
||||||
|
// use default CpuPool
|
||||||
|
let pool = { DEFAULT_CPUPOOL.lock().clone() };
|
||||||
|
|
||||||
|
StaticFiles::with_config_pool(dir, pool, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new `StaticFiles` instance for specified base directory with config and
|
||||||
|
/// `CpuPool`.
|
||||||
|
pub fn with_config_pool<T: Into<PathBuf>>(
|
||||||
|
dir: T, pool: CpuPool, _: C
|
||||||
|
) -> Result<StaticFiles<S, C>, Error> {
|
||||||
let dir = dir.into().canonicalize()?;
|
let dir = dir.into().canonicalize()?;
|
||||||
|
|
||||||
if !dir.is_dir() {
|
if !dir.is_dir() {
|
||||||
@ -605,6 +701,7 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
renderer: Box::new(directory_listing),
|
renderer: Box::new(directory_listing),
|
||||||
_chunk_size: 0,
|
_chunk_size: 0,
|
||||||
_follow_symlinks: false,
|
_follow_symlinks: false,
|
||||||
|
_cd_map: PhantomData
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -631,13 +728,13 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
///
|
///
|
||||||
/// Redirects to specific index file for directory "/" instead of
|
/// Redirects to specific index file for directory "/" instead of
|
||||||
/// showing files listing.
|
/// showing files listing.
|
||||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S> {
|
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles<S, C> {
|
||||||
self.index = Some(index.into());
|
self.index = Some(index.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets default handler which is used when no matched file could be found.
|
/// Sets default handler which is used when no matched file could be found.
|
||||||
pub fn default_handler<H: Handler<S>>(mut self, handler: H) -> StaticFiles<S> {
|
pub fn default_handler<H: Handler<S>>(mut self, handler: H) -> StaticFiles<S, C> {
|
||||||
self.default = Box::new(WrapHandler::new(handler));
|
self.default = Box::new(WrapHandler::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -672,7 +769,7 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
Err(StaticFileError::IsDirectory.into())
|
Err(StaticFileError::IsDirectory.into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
NamedFile::open(path)?
|
NamedFile::open_with_config(path, C::default())?
|
||||||
.set_cpu_pool(self.cpu_pool.clone())
|
.set_cpu_pool(self.cpu_pool.clone())
|
||||||
.respond_to(&req)?
|
.respond_to(&req)?
|
||||||
.respond_to(&req)
|
.respond_to(&req)
|
||||||
@ -680,7 +777,7 @@ impl<S: 'static> StaticFiles<S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: 'static> Handler<S> for StaticFiles<S> {
|
impl<S: 'static, C: 'static + StaticFileConfig> Handler<S> for StaticFiles<S, C> {
|
||||||
type Result = Result<AsyncResult<HttpResponse>, Error>;
|
type Result = Result<AsyncResult<HttpResponse>, Error>;
|
||||||
|
|
||||||
fn handle(&self, req: &HttpRequest<S>) -> Self::Result {
|
fn handle(&self, req: &HttpRequest<S>) -> Self::Result {
|
||||||
@ -920,6 +1017,56 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AllAttachmentConfig;
|
||||||
|
impl StaticFileConfig for AllAttachmentConfig {
|
||||||
|
fn content_disposition_map(_typ: mime::Name) -> DispositionType {
|
||||||
|
DispositionType::Attachment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AllInlineConfig;
|
||||||
|
impl StaticFileConfig for AllInlineConfig {
|
||||||
|
fn content_disposition_map(_typ: mime::Name) -> DispositionType {
|
||||||
|
DispositionType::Inline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_named_file_image_attachment_and_custom_config() {
|
||||||
|
let file = NamedFile::open_with_config("tests/test.png", AllAttachmentConfig)
|
||||||
|
.unwrap()
|
||||||
|
.set_cpu_pool(CpuPool::new(1));
|
||||||
|
|
||||||
|
let req = TestRequest::default().finish();
|
||||||
|
let resp = file.respond_to(&req).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
|
"image/png"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
|
"attachment; filename=\"test.png\""
|
||||||
|
);
|
||||||
|
|
||||||
|
let file = NamedFile::open_with_config("tests/test.png", AllInlineConfig)
|
||||||
|
.unwrap()
|
||||||
|
.set_cpu_pool(CpuPool::new(1));
|
||||||
|
|
||||||
|
let req = TestRequest::default().finish();
|
||||||
|
let resp = file.respond_to(&req).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||||
|
"image/png"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||||
|
"inline; filename=\"test.png\""
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_named_file_binary() {
|
fn test_named_file_binary() {
|
||||||
let mut file = NamedFile::open("tests/test.binary")
|
let mut file = NamedFile::open("tests/test.binary")
|
||||||
@ -1143,12 +1290,32 @@ mod tests {
|
|||||||
assert_eq!(bytes, data);
|
assert_eq!(bytes, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct OnlyMethodHeadConfig;
|
||||||
|
impl StaticFileConfig for OnlyMethodHeadConfig {
|
||||||
|
fn is_method_allowed(method: &Method) -> bool {
|
||||||
|
match *method {
|
||||||
|
Method::HEAD => true,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_named_file_not_allowed() {
|
fn test_named_file_not_allowed() {
|
||||||
|
let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap();
|
||||||
let req = TestRequest::default().method(Method::POST).finish();
|
let req = TestRequest::default().method(Method::POST).finish();
|
||||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
let resp = file.respond_to(&req).unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
|
||||||
let resp = file.only_get().respond_to(&req).unwrap();
|
let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap();
|
||||||
|
let req = TestRequest::default().method(Method::PUT).finish();
|
||||||
|
let resp = file.respond_to(&req).unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||||
|
|
||||||
|
let file = NamedFile::open_with_config("Cargo.toml", OnlyMethodHeadConfig).unwrap();
|
||||||
|
let req = TestRequest::default().method(Method::GET).finish();
|
||||||
|
let resp = file.respond_to(&req).unwrap();
|
||||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user