Skip to content

perf(upload): Preserve compressed TUS uploads when forwarding - #6572

Open
vjeranc wants to merge 37 commits into
masterfrom
perf/ingest-1222-upload-passthrough
Open

vjeranc wants to merge 37 commits into
masterfrom
perf/ingest-1222-upload-passthrough

Conversation

@vjeranc

@vjeranc vjeranc commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Preserve compressed TUS upload bytes and Content-Encoding when forwarding. Decode and enforce decoded-size limits in the Upload service’s objectstore branch, reusing the existing decompressor and byte counters.

Bound incoming request bytes at every hop. Record the decompression ratio when the input body finishes or is dropped.

Refs INGEST-1222

Follow-up: INGEST-1348 removes the stream-validation workaround after upgrading to a fixed Objectstore client release.

Compressed TUS uploads currently get decompressed and recompressed at forwarding hops. Preserve their bytes and Content-Encoding until they reach the Relay writing to objectstore.

Select decompression during route initialization based on objectstore availability. Keep decoded-size validation, upload offsets, signed attachment lengths, and attachment-byte accounting at the storage Relay. Other routes retain decompression.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
@vjeranc
vjeranc requested a review from a team as a code owner October 27, 2026 12:42
@linear-code

linear-code Bot commented Oct 27, 2026

Copy link
Copy Markdown

INGEST-1222

@vjeranc
vjeranc marked this pull request as draft October 27, 2026 12:44
vjeranc and others added 9 commits October 27, 2026 13:52
Remove the duplicated test router and codec helper. Cover routing, encoding rejection, normalization, and malformed passthrough through actual Relay instances; retain direct classifier and Tower response parity tests.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Preserve both changelog entries and add the upload PR reference.

Co-authored-by: Codex <noreply@openai.com>
Keep HEAD, PATCH, and the decompressed method fallback together. Register the complete upload route after the other public routes receive decompression, removing the trailing router merge.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Select passthrough or configured encoding before constructing the upstream request. Read the encoding config only for decoded bodies, and pass the prepared stream and encoding to the request constructor.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Supply the decoded-body marker directly at route initialization, matching the forwarding adapter and removing the redundant handler wrapper.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Clarify that compression overhead can increase payload size and that the decoded-size limit is enforced upstream, with no separate compressed-byte cap.

Co-authored-by: Codex <noreply@openai.com>
Keep passthrough bytes, headers, and retry-state assertions local. Leave multi-hop forwarding and final decompression to the existing integration tests.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
@vjeranc
vjeranc marked this pull request as ready for review October 27, 2026 13:58
Comment thread relay-server/src/endpoints/upload.rs Resolved
Convert delivery and upload failures in a small helper using await??. Keep the PATCH handler focused on the operation and preserve logging for both error paths.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Comment thread relay-server/src/endpoints/upload.rs Resolved
Apply max_upload_size to incoming bytes at forwarding hops and retain decoded-size checks at the objectstore Relay. Remove the unlimited compressed-body branch and cover wire and decoded rejection for fixed-length and chunked requests.

BREAKING CHANGE: Compressed TUS requests exceeding max_upload_size are rejected by forwarding Relays even when their decoded bodies fit.

Refs INGEST-1222

Co-authored-by: Codex <noreply@openai.com>
Comment thread relay-config/src/config.rs Resolved
Comment thread relay-server/src/services/upload.rs Resolved
Comment thread relay-config/src/config.rs Resolved
Comment thread relay-server/src/endpoints/mod.rs Resolved
Comment thread relay-config/src/config.rs Resolved
Comment thread relay-server/src/endpoints/upload.rs Resolved
Comment thread relay-server/src/services/upload.rs Resolved
Comment thread relay-server/src/services/upload.rs Resolved
Comment thread relay-server/src/endpoints/upload.rs Resolved
Use an explicit match for strict Content-Encoding parsing and explain whether upload limits count compressed or decoded bytes.

Co-authored-by: Codex <noreply@openai.com>
Comment thread relay-server/src/endpoints/upload.rs Resolved
Comment thread relay-server/src/endpoints/upload.rs Resolved
Comment thread relay-server/src/endpoints/upload.rs Resolved
Co-authored-by: Codex <noreply@openai.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0d7c0e5. Configure here.

Comment thread relay-server/src/services/upload.rs Resolved
vjeranc and others added 3 commits October 28, 2026 14:18
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
vjeranc and others added 2 commits October 28, 2026 14:29
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>

@jjbayer jjbayer left a comment

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.

Thanks for making the change, this looks like it's almost good to go!

Comment thread tests/integration/test_upload.py Resolved
Comment thread relay-server/src/services/upload.rs Resolved
vjeranc and others added 2 commits October 28, 2026 16:03
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
@vjeranc
vjeranc force-pushed the perf/ingest-1222-upload-passthrough branch from 8bc8011 to d8220b1 Compare October 28, 2026 15:03
Comment thread relay-server/src/services/upload.rs Resolved
let stream = BoundedStream::new(
MeteredStream::new(stream, referrer),
0,
config.max_upload_size(),

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.

If the mode has a length / chunk_length, we could still use that as an upper bound instead of max_upload_size, wdyt? In practice a compressed stream should always be smaller than its decoded length.

Not a blocker because the objectstore relay enforces the bounds in the end.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, should be smaller, not sure if that's always the case, maybe someone compresses twice or sends an encrypted blob ? even a short string due to overhead compresses to more bytes...

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.

Fair enough, double compression is a realistic scenario 👍

Let's keep max_upload_size then. Might be worth a code comment explaining why we cannot apply the declared length as an upper bound.

Comment thread relay-server/src/services/upload.rs Outdated
Comment on lines +1344 to +1347
let compression = match HttpCompression::from_encoding(encoding) {
None => return Either::Right(stream),
Some(compression) => compression,
};

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.

nit

Suggested change
let compression = match HttpCompression::from_encoding(encoding) {
None => return Either::Right(stream),
Some(compression) => compression,
};
let Some(compression) = HttpCompression::from_encoding(encoding) else {
return Either::Right(stream);
};

Comment thread relay-server/src/services/upload.rs Outdated
Comment on lines +1366 to +1384
use http_body_util::BodyExt;

let body =
http_body_util::StreamBody::new(stream.map(|result| result.map(hyper::body::Frame::data)));
let mut request = http::Request::new(body);
if let Some(codec) = compression {
request.headers_mut().insert(
http::header::CONTENT_ENCODING,
HeaderValue::from_static(codec.encoding().name().unwrap()),
);
}
let request = crate::middlewares::decompress_request(request, UPLOAD_PATCH_PATH.into())
.await
.expect("the upload encoding has already been validated");
request
.into_body()
.into_data_stream()
.map(|result| result.map_err(std::io::Error::other))
.boxed()

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.

Creating an artificial request here seems a bit awkward. I get the idea of using the same decompression mechanism as for other endpoints, but I think it would be better here to do the reverse of encode_body, i.e. match on compression and call a DeflateDecoder, GzipDecoder, etc.

That would simplify the code and get rid of the .expect().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

yes, i agree, i stopped thinking after we stopped using tower when everything moved to upload.rs

will make sure to support the same behavior.

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.

Nice!

vjeranc and others added 6 commits October 30, 2026 11:04
Select codecs directly in the objectstore upload path and share byte
counters between HTTP bodies and streams. Preserve zlib-wrapped deflate,
concatenated zstd frames, and reporting on EOF, error, or drop.

Co-authored-by: Codex <noreply@openai.com>
Keep the reporter alive with the decoded body or stream instead of
emitting at EOF or error. Replace optional reporters with typed guards
and verify reporting when retained, partial, and unread streams drop.

Co-authored-by: Codex <noreply@openai.com>
HTTP deflate requires a zlib wrapper. Replace the raw encoder and add a
regression that decodes the emitted bytes with ZlibDecoder.

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Move the upload changelog entry back to Unreleased Internal after the
26.10.1 release.

Co-authored-by: Codex <noreply@openai.com>
let stream = BoundedStream::new(
MeteredStream::new(stream, referrer),
0,
config.max_upload_size(),

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.

Fair enough, double compression is a realistic scenario 👍

Let's keep max_upload_size then. Might be worth a code comment explaining why we cannot apply the declared length as an upper bound.

Comment thread relay-server/src/services/upload.rs Outdated
Comment on lines +1366 to +1384
use http_body_util::BodyExt;

let body =
http_body_util::StreamBody::new(stream.map(|result| result.map(hyper::body::Frame::data)));
let mut request = http::Request::new(body);
if let Some(codec) = compression {
request.headers_mut().insert(
http::header::CONTENT_ENCODING,
HeaderValue::from_static(codec.encoding().name().unwrap()),
);
}
let request = crate::middlewares::decompress_request(request, UPLOAD_PATCH_PATH.into())
.await
.expect("the upload encoding has already been validated");
request
.into_body()
.into_data_stream()
.map(|result| result.map_err(std::io::Error::other))
.boxed()

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.

Nice!

Comment thread relay-server/src/services/upload.rs Resolved
vjeranc and others added 7 commits October 30, 2026 12:49
Co-authored-by: Codex <noreply@openai.com>
Keep both upload forwarding and Unreal integration entries under
Unreleased Internal.

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Apply CORS to the retained method fallback so OPTIONS requests receive CORS responses. Add coverage for preflight requests without a content encoding.

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Hold back the final decoded chunk until the input stream reaches EOF, so checksum and length errors cannot be skipped by the Objectstore client. Validate zero-length bodies before sending them.

Co-authored-by: Codex <noreply@openai.com>
vjeranc and others added 3 commits November 3, 2026 14:52
Compile the stream-completion helper only for processing builds and tests. Track its removal after the fixed Objectstore client release with INGEST-1348.

Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
A codec can finish before its encoded source. Check the underlying reader before reporting EOF to reject trailing input and propagate late source errors.

Co-authored-by: Codex <noreply@openai.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants