-
Notifications
You must be signed in to change notification settings - Fork 171
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add layer that limits body size (#271)
* feat: add layer that limits body size * chore: rename `LengthLimited` to `RequestBodyLimit` * refactor: remove request body witness from layer * refactor: remove need to box wrapped service error * fix: impl bounds to work as a service * feat: handle `Content-Length` case, improve docs * docs: add example and recommendation for without 413 handling * refactor: split out body and data * refactor: get rid of custom data type * refactor: stop interpreting service error This removes the `StdError + 'static` bound on the service error. Also sets the read limit for a given message to the lesser of `Content-Length` or the configured limit. * refactor: remove accessory function for finding nested source * docs: improve limit module documentation * misc docs changes * Derive `Debug` * Order functions like we normally do * Add `RequestBodyLimit::layer` * fix nit picks * changelog * add note about hyper Co-authored-by: David Pedersen <[email protected]>
- Loading branch information
1 parent
960c83b
commit 5468fc8
Showing
9 changed files
with
431 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
use bytes::Bytes; | ||
use http::{HeaderMap, HeaderValue, Response, StatusCode}; | ||
use http_body::{Body, Full, SizeHint}; | ||
use pin_project_lite::pin_project; | ||
use std::pin::Pin; | ||
use std::task::{Context, Poll}; | ||
|
||
pin_project! { | ||
/// Response body for [`RequestBodyLimit`]. | ||
/// | ||
/// [`RequestBodyLimit`]: super::RequestBodyLimit | ||
pub struct ResponseBody<B> { | ||
#[pin] | ||
inner: ResponseBodyInner<B> | ||
} | ||
} | ||
|
||
impl<B> ResponseBody<B> { | ||
fn payload_too_large() -> Self { | ||
Self { | ||
inner: ResponseBodyInner::PayloadTooLarge { | ||
body: Full::from(BODY), | ||
}, | ||
} | ||
} | ||
|
||
pub(crate) fn new(body: B) -> Self { | ||
Self { | ||
inner: ResponseBodyInner::Body { body }, | ||
} | ||
} | ||
} | ||
|
||
pin_project! { | ||
#[project = BodyProj] | ||
enum ResponseBodyInner<B> { | ||
PayloadTooLarge { | ||
#[pin] | ||
body: Full<Bytes>, | ||
}, | ||
Body { | ||
#[pin] | ||
body: B | ||
} | ||
} | ||
} | ||
|
||
impl<B> Body for ResponseBody<B> | ||
where | ||
B: Body<Data = Bytes>, | ||
{ | ||
type Data = Bytes; | ||
type Error = B::Error; | ||
|
||
fn poll_data( | ||
self: Pin<&mut Self>, | ||
cx: &mut Context<'_>, | ||
) -> Poll<Option<Result<Self::Data, Self::Error>>> { | ||
match self.project().inner.project() { | ||
BodyProj::PayloadTooLarge { body } => body.poll_data(cx).map_err(|err| match err {}), | ||
BodyProj::Body { body } => body.poll_data(cx), | ||
} | ||
} | ||
|
||
fn poll_trailers( | ||
self: Pin<&mut Self>, | ||
cx: &mut Context<'_>, | ||
) -> Poll<Result<Option<HeaderMap>, Self::Error>> { | ||
match self.project().inner.project() { | ||
BodyProj::PayloadTooLarge { body } => { | ||
body.poll_trailers(cx).map_err(|err| match err {}) | ||
} | ||
BodyProj::Body { body } => body.poll_trailers(cx), | ||
} | ||
} | ||
|
||
fn is_end_stream(&self) -> bool { | ||
match &self.inner { | ||
ResponseBodyInner::PayloadTooLarge { body } => body.is_end_stream(), | ||
ResponseBodyInner::Body { body } => body.is_end_stream(), | ||
} | ||
} | ||
|
||
fn size_hint(&self) -> SizeHint { | ||
match &self.inner { | ||
ResponseBodyInner::PayloadTooLarge { body } => body.size_hint(), | ||
ResponseBodyInner::Body { body } => body.size_hint(), | ||
} | ||
} | ||
} | ||
|
||
const BODY: &[u8] = b"length limit exceeded"; | ||
|
||
pub(crate) fn create_error_response<B>() -> Response<ResponseBody<B>> | ||
where | ||
B: Body, | ||
{ | ||
let mut res = Response::new(ResponseBody::payload_too_large()); | ||
*res.status_mut() = StatusCode::PAYLOAD_TOO_LARGE; | ||
|
||
#[allow(clippy::declare_interior_mutable_const)] | ||
const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8"); | ||
res.headers_mut() | ||
.insert(http::header::CONTENT_TYPE, TEXT_PLAIN); | ||
|
||
res | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
use super::body::create_error_response; | ||
use super::ResponseBody; | ||
use futures_core::ready; | ||
use http::Response; | ||
use http_body::Body; | ||
use pin_project_lite::pin_project; | ||
use std::future::Future; | ||
use std::pin::Pin; | ||
use std::task::{Context, Poll}; | ||
|
||
pin_project! { | ||
/// Response future for [`RequestBodyLimit`]. | ||
/// | ||
/// [`RequestBodyLimit`]: super::RequestBodyLimit | ||
pub struct ResponseFuture<F> { | ||
#[pin] | ||
inner: ResponseFutureInner<F>, | ||
} | ||
} | ||
|
||
impl<F> ResponseFuture<F> { | ||
pub(crate) fn payload_too_large() -> Self { | ||
Self { | ||
inner: ResponseFutureInner::PayloadTooLarge, | ||
} | ||
} | ||
|
||
pub(crate) fn new(future: F) -> Self { | ||
Self { | ||
inner: ResponseFutureInner::Future { future }, | ||
} | ||
} | ||
} | ||
|
||
pin_project! { | ||
#[project = ResFutProj] | ||
enum ResponseFutureInner<F> { | ||
PayloadTooLarge, | ||
Future { | ||
#[pin] | ||
future: F, | ||
} | ||
} | ||
} | ||
|
||
impl<ResBody, F, E> Future for ResponseFuture<F> | ||
where | ||
ResBody: Body, | ||
F: Future<Output = Result<Response<ResBody>, E>>, | ||
{ | ||
type Output = Result<Response<ResponseBody<ResBody>>, E>; | ||
|
||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
let res = match self.project().inner.project() { | ||
ResFutProj::PayloadTooLarge => create_error_response(), | ||
ResFutProj::Future { future } => ready!(future.poll(cx))?.map(ResponseBody::new), | ||
}; | ||
|
||
Poll::Ready(Ok(res)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
use super::RequestBodyLimit; | ||
use tower_layer::Layer; | ||
|
||
/// Layer that applies the [`RequestBodyLimit`] middleware that intercepts requests | ||
/// with body lengths greater than the configured limit and converts them into | ||
/// `413 Payload Too Large` responses. | ||
/// | ||
/// See the [module docs](crate::limit) for an example. | ||
/// | ||
/// [`RequestBodyLimit`]: super::RequestBodyLimit | ||
#[derive(Clone, Copy, Debug)] | ||
pub struct RequestBodyLimitLayer { | ||
limit: usize, | ||
} | ||
|
||
impl RequestBodyLimitLayer { | ||
/// Create a new `RequestBodyLimitLayer` with the given body length limit. | ||
pub fn new(limit: usize) -> Self { | ||
Self { limit } | ||
} | ||
} | ||
|
||
impl<S> Layer<S> for RequestBodyLimitLayer { | ||
type Service = RequestBodyLimit<S>; | ||
|
||
fn layer(&self, inner: S) -> Self::Service { | ||
RequestBodyLimit { | ||
inner, | ||
limit: self.limit, | ||
} | ||
} | ||
} |
Oops, something went wrong.