Skip to content

Commit 0f0c490

Browse files
committed
mirror_worker: map malformed-gzip bodies to 400, not 500
Gzip decode/trailer failures were surfaced as worker::Error, which the handler's blanket From<worker::Error> mapped to 500. A malformed or truncated gzip request body is a client fault. Introduce a typed BodyError (Decode vs Transport) on the body stream so decode faults map to 400 while genuine transport failures stay 500; parse_next_package now returns ApiResult so the distinction survives to the handler.
1 parent b8f0273 commit 0f0c490

3 files changed

Lines changed: 62 additions & 18 deletions

File tree

crates/mirror_worker/src/add_entries.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ use worker::*;
4848
use generic_log_worker::{ObjectBackend, util::now_millis};
4949

5050
use crate::{
51-
body, commit,
51+
body::{self, BodyError},
52+
commit,
5253
frontend_worker::{ApiResult, AppError},
5354
load_mirror_signer, load_ticket_sealer, log_verifiers,
5455
mirror_state_do::{
@@ -237,7 +238,7 @@ async fn stream_and_commit<S>(
237238
first_prefix: &[Vec<u8>],
238239
) -> ApiResult<axum::response::Response>
239240
where
240-
S: futures_util::Stream<Item = Result<Vec<u8>>> + Unpin,
241+
S: futures_util::Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin,
241242
{
242243
let bucket = load_origin_bucket(env, &header.log_origin)?;
243244

@@ -336,7 +337,7 @@ async fn persist_packages<S, O>(
336337
start: &NextEntry,
337338
) -> ApiResult<StreamResult>
338339
where
339-
S: futures_util::Stream<Item = Result<Vec<u8>>> + Unpin,
340+
S: futures_util::Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin,
340341
O: ObjectBackend,
341342
{
342343
// config.schema.json caps commit_packages (max 1024), enforced by the
@@ -728,7 +729,7 @@ enum ParseOutcome {
728729
/// retry-on-`UnexpectedEof` loop terminates.
729730
async fn parse_header<S>(buf: &mut StreamBuffer<S>) -> ApiResult<AddEntriesRequestHeader>
730731
where
731-
S: futures_util::Stream<Item = Result<Vec<u8>>> + Unpin,
732+
S: futures_util::Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin,
732733
{
733734
loop {
734735
let mut cursor = Cursor::new(buf.buffered());
@@ -765,9 +766,12 @@ where
765766
/// Read the next entry package from `buf`, pulling more chunks from
766767
/// the underlying stream until the package parses or the stream ends.
767768
/// See [`ParseOutcome`] for the four cases.
768-
async fn parse_next_package<S>(buf: &mut StreamBuffer<S>, num_entries: u64) -> Result<ParseOutcome>
769+
async fn parse_next_package<S>(
770+
buf: &mut StreamBuffer<S>,
771+
num_entries: u64,
772+
) -> ApiResult<ParseOutcome>
769773
where
770-
S: futures_util::Stream<Item = Result<Vec<u8>>> + Unpin,
774+
S: futures_util::Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin,
771775
{
772776
// EOF with an empty buffer: clean truncation between packages.
773777
if buf.is_eof() && buf.len() == 0 {

crates/mirror_worker/src/body.rs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,31 @@ use worker::*;
3131

3232
use crate::frontend_worker::{ApiResult, AppError};
3333

34+
/// An error surfaced by a [`BodyStream`], distinguishing a client-side
35+
/// decode fault from a transport failure so the `add-entries` handler can
36+
/// map each to the right HTTP status (see `From<BodyError> for AppError`).
37+
#[derive(Debug)]
38+
pub(crate) enum BodyError {
39+
/// Malformed or truncated gzip body: a client fault, mapped to 400.
40+
Decode(String),
41+
/// Transport failure reading the underlying request body: mapped to
42+
/// 500.
43+
Transport(Error),
44+
}
45+
46+
impl std::fmt::Display for BodyError {
47+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48+
match self {
49+
BodyError::Decode(e) => write!(f, "{e}"),
50+
BodyError::Transport(e) => write!(f, "{e}"),
51+
}
52+
}
53+
}
54+
3455
/// A boxed, `Unpin` body stream, the common type the `add-entries`
3556
/// handler feeds to [`crate::stream_buffer::StreamBuffer`] regardless of
3657
/// whether the request body was identity- or gzip-encoded.
37-
pub(crate) type BodyStream = Pin<Box<dyn Stream<Item = Result<Vec<u8>>>>>;
58+
pub(crate) type BodyStream = Pin<Box<dyn Stream<Item = std::result::Result<Vec<u8>, BodyError>>>>;
3859

3960
/// Open the request body as a decoded chunk stream, honoring
4061
/// `Content-Encoding`.
@@ -62,7 +83,7 @@ pub(crate) fn decoded_stream(
6283
// `Result<Vec<u8>>` chunk contract the buffer/gunzip pipeline expects.
6384
let raw = body.into_data_stream().map(|r| {
6485
r.map(|b| b.to_vec())
65-
.map_err(|e| Error::from(e.to_string()))
86+
.map_err(|e| BodyError::Transport(Error::from(e.to_string())))
6687
});
6788
let stream: BodyStream = match encoding.as_str() {
6889
"" | "identity" => Box::pin(raw),
@@ -125,7 +146,7 @@ impl GzipInflater {
125146
/// terminal `Err` item, after which the stream ends.
126147
pub(crate) fn gunzip<S>(inner: S) -> BodyStream
127148
where
128-
S: Stream<Item = Result<Vec<u8>>> + Unpin + 'static,
149+
S: Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin + 'static,
129150
{
130151
struct DecodeState<S> {
131152
inner: S,
@@ -151,7 +172,7 @@ where
151172
Ok(out) => out,
152173
Err(e) => {
153174
st.done = true;
154-
return Some((Err(e), st));
175+
return Some((Err(BodyError::Decode(e.to_string())), st));
155176
}
156177
};
157178
// A chunk may not yet yield any plaintext (partial
@@ -172,7 +193,7 @@ where
172193
return match inflater.finish() {
173194
Ok(tail) if !tail.is_empty() => Some((Ok(tail), st)),
174195
Ok(_) => None,
175-
Err(e) => Some((Err(e), st)),
196+
Err(e) => Some((Err(BodyError::Decode(e.to_string())), st)),
176197
};
177198
}
178199
}
@@ -199,13 +220,15 @@ mod tests {
199220
fn chunked_stream(
200221
bytes: &[u8],
201222
size: usize,
202-
) -> impl Stream<Item = Result<Vec<u8>>> + Unpin + 'static {
203-
let chunks: Vec<Result<Vec<u8>>> =
223+
) -> impl Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin + 'static {
224+
let chunks: Vec<std::result::Result<Vec<u8>, BodyError>> =
204225
bytes.chunks(size.max(1)).map(|c| Ok(c.to_vec())).collect();
205226
stream::iter(chunks)
206227
}
207228

208-
async fn collect(mut s: impl Stream<Item = Result<Vec<u8>>> + Unpin) -> Result<Vec<u8>> {
229+
async fn collect(
230+
mut s: impl Stream<Item = std::result::Result<Vec<u8>, BodyError>> + Unpin,
231+
) -> std::result::Result<Vec<u8>, BodyError> {
209232
let mut out = Vec::new();
210233
while let Some(item) = s.next().await {
211234
out.extend_from_slice(&item?);
@@ -249,7 +272,10 @@ mod tests {
249272
// ends mid-member; finish() must report the truncation.
250273
compressed.truncate(compressed.len() - 6);
251274
let err = collect(gunzip(chunked_stream(&compressed, 4))).await;
252-
assert!(err.is_err(), "truncated gzip must surface an error");
275+
assert!(
276+
matches!(err, Err(BodyError::Decode(_))),
277+
"truncated gzip must surface a client decode error"
278+
);
253279
}
254280

255281
#[tokio::test]
@@ -259,14 +285,18 @@ mod tests {
259285
let mid = compressed.len() / 2;
260286
compressed[mid] ^= 0xff;
261287
let err = collect(gunzip(chunked_stream(&compressed, 5))).await;
262-
assert!(err.is_err(), "corrupt gzip must surface an error");
288+
assert!(
289+
matches!(err, Err(BodyError::Decode(_))),
290+
"corrupt gzip must surface a client decode error"
291+
);
263292
}
264293

265294
#[tokio::test]
266295
async fn upstream_error_propagates() {
267296
let compressed = gzip(b"partial");
268-
let mut chunks: Vec<Result<Vec<u8>>> = vec![Ok(compressed[..4].to_vec())];
269-
chunks.push(Err(Error::from("boom")));
297+
let mut chunks: Vec<std::result::Result<Vec<u8>, BodyError>> =
298+
vec![Ok(compressed[..4].to_vec())];
299+
chunks.push(Err(BodyError::Transport(Error::from("boom"))));
270300
let err = collect(gunzip(stream::iter(chunks))).await;
271301
assert!(err.is_err(), "upstream stream error must propagate");
272302
}

crates/mirror_worker/src/frontend_worker.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,16 @@ impl From<worker::Error> for AppError {
140140
}
141141
}
142142

143+
impl From<crate::body::BodyError> for AppError {
144+
fn from(err: crate::body::BodyError) -> Self {
145+
match err {
146+
// Malformed/truncated gzip is a client fault.
147+
crate::body::BodyError::Decode(msg) => Self::BadRequest(msg),
148+
crate::body::BodyError::Transport(e) => Self::InternalServerError(e.to_string()),
149+
}
150+
}
151+
}
152+
143153
impl IntoResponse for AppError {
144154
fn into_response(self) -> axum::response::Response {
145155
match self {

0 commit comments

Comments
 (0)