Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/sources/util/http/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,12 @@ pub(crate) fn limited_body(max_body_size: usize) -> BoxedFilter<(Bytes,)> {
max_body_size,
)))
} else {
Ok(())
Ok(declared)
}
})
.untuple_one()
.and(warp::body::stream())
.and_then(move |body| async move {
collect_body_with_limit(body, max_body_size)
.and_then(move |declared: Option<u64>, body| async move {
collect_body_with_limit(body, max_body_size, declared)
.await
.map_err(warp::reject::custom)
})
Expand Down Expand Up @@ -183,14 +182,21 @@ fn decompress_snappy(
feature = "sources-opentelemetry",
test
))]
async fn collect_body_with_limit<S, B>(body: S, max_body_size: usize) -> Result<Bytes, ErrorMessage>
async fn collect_body_with_limit<S, B>(
body: S,
max_body_size: usize,
declared_len: Option<u64>,
) -> Result<Bytes, ErrorMessage>
where
S: futures_util::Stream<Item = Result<B, warp::Error>>,
B: Buf,
{
futures_util::pin_mut!(body);

let mut bytes = BytesMut::new();
let capacity = declared_len
.and_then(|len| usize::try_from(len).ok())
.map_or(0, |len| len.min(max_body_size));
let mut bytes = BytesMut::with_capacity(capacity);
Comment on lines +196 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid allocating the full declared body before reads

When an unauthenticated HTTP client sends a Content-Length near the configured limit (100 MiB by default via max_decompressed_size_bytes()) and then stalls or disconnects, this allocates that entire buffer before any body bytes are received. I checked the HTTP source call sites in src/sources/util/http/prelude.rs and src/sources/opentelemetry/http.rs; both use this helper for incoming requests, so many concurrent header-only/slow requests can now reserve large amounts of memory where the previous streaming collector only grew after chunks arrived. Consider capping the initial preallocation to a smaller threshold or growing after the first chunk instead of trusting the header up to the full body limit.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. IMO, it's acceptable given the performance regression that the alternative causes.
What do you guys think about this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd agree with its suggestion to cap the initial capacity at some suitably large but smaller-than-100MiB number. That would minimize still reallocations, just not eliminate them.

while let Some(chunk) = body.next().await {
let chunk = chunk.map_err(|error| {
ErrorMessage::new(
Expand Down Expand Up @@ -368,7 +374,7 @@ mod tests {
Ok::<_, warp::Error>(Bytes::from_static(b" world")),
]);

let collected = collect_body_with_limit(body, 11).await.unwrap();
let collected = collect_body_with_limit(body, 11, Some(11)).await.unwrap();
assert_eq!(collected, Bytes::from_static(b"hello world"));
}

Expand All @@ -379,7 +385,7 @@ mod tests {
Ok::<_, warp::Error>(Bytes::from_static(b" world")),
]);

let err = collect_body_with_limit(body, 5)
let err = collect_body_with_limit(body, 5, None)
.await
.expect_err("should reject");
assert_eq!(err.status_code(), StatusCode::PAYLOAD_TOO_LARGE);
Expand Down
Loading