Skip to content

feat(upload): Create before upload#5685

Open
jjbayer wants to merge 25 commits intomasterfrom
feat/upload-create
Open

feat(upload): Create before upload#5685
jjbayer wants to merge 25 commits intomasterfrom
feat/upload-create

Conversation

@jjbayer
Copy link
Member

@jjbayer jjbayer commented Mar 3, 2026

This PR adds a new route to upload, so a user can now

  1. Either "create" or "create-with-upload" in the POST request.
  2. Additionally, an existing upload can be PATCHed.
  3. The service now only accepts Create or Upload messages, so the upstream will always receive two requests.

Once all experimental clients have switched to "create", we can remove the "create-with-upload" branch.

fixes: INGEST-802

@linear
Copy link

linear bot commented Mar 3, 2026

@jjbayer jjbayer force-pushed the feat/upload-create branch from 0329614 to 8f5c41c Compare March 10, 2026 08:47
jjbayer and others added 11 commits March 10, 2026 10:07
Move PatchPath/PatchQueryParams from endpoints to services/upload as
LocationPath/LocationQueryParams and reuse them in both the endpoint
handler and SignedLocation::try_from_str. The matchit router is kept
in a static LazyLock so it is only built once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jjbayer jjbayer force-pushed the feat/upload-create branch from ec8d5ae to c47baa6 Compare March 12, 2026 07:09
@jjbayer jjbayer marked this pull request as ready for review March 12, 2026 07:19
@jjbayer jjbayer requested a review from a team as a code owner March 12, 2026 07:19
Comment on lines +176 to +194
if content_length.is_none_or(|v| v > 0) {
let stream = body
.into_data_stream()
.map(|result| result.map_err(io::Error::other))
.boxed();
let (lower_bound, upper_bound) = match upload_length {
None => (1, config.max_upload_size()),
Some(u) => (u, u),
};
let stream = BoundedStream::new(stream, lower_bound, upper_bound);
let byte_counter = stream.byte_counter();

relay_log::trace!("Uploading");
let result = upload(&state, scoping, location, stream).await;
location = result.inspect_err(|e| {
relay_log::warn!(error = e as &dyn std::error::Error, "upload failed");
})?;
upload_offset = Some(byte_counter.get());
}
Copy link
Member Author

Choose a reason for hiding this comment

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

This block will be removed once we remove "Creation-With-Upload".

public_key
.iter()
.any(|p| self.verify(p, start_time, max_age))
.any(|p| self.verify(&[], p, start_time, max_age))
Copy link
Member Author

Choose a reason for hiding this comment

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

I changed the signature of verify so we can verify the integrity of the data, not just the signature itself.

Comment on lines +212 to +213
Path(upload::LocationPath { project_id, key }): Path<upload::LocationPath>,
Query(upload::LocationQueryParams { length, signature }): Query<upload::LocationQueryParams>,
Copy link
Member Author

Choose a reason for hiding this comment

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

There's a potential forward-compatibility issue here: Outdated external relays may parse the wrong set of query parameters, i.e. upstream Relays cannot transparently put new query parameters in its signed location.

There's an argument to be made that we should forward all query parameters, but I didn't like how that interacted with for example sentry_key=.

json-forensics = { workspace = true }
libc = { workspace = true }
liblzma = { workspace = true }
matchit = { workspace = true }
Copy link
Member Author

Choose a reason for hiding this comment

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

Not a new dependency, axum already uses it.

serde_bytes = { workspace = true }
serde_json = { workspace = true }
serde_path_to_error = { workspace = true }
serde_urlencoded = { workspace = true }
Copy link
Member Author

Choose a reason for hiding this comment

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

Not a new dependency, axum already uses it.

Poll::Pending => {
relay_log::trace!("still pending");
Poll::Pending
}
Copy link

Choose a reason for hiding this comment

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

Verbose trace logging in hot streaming path

Low Severity

Eight relay_log::trace!() calls were added to BoundedStream::poll_next, which fires on every single poll of the stream — including Pending wakeups. For a large upload with many chunks, this generates thousands of log entries per upload even at trace level. This looks like development debugging output that was left in. Contrast with the per-request traces in the endpoint handlers, which fire once; these per-poll traces are orders of magnitude more voluminous.

Fix in Cursor Fix in Web

Copy link

@cursor cursor bot left a comment

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.

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

builder.header("X-Sentry-Auth", format!("Sentry sentry_key={project_key}"));
let upload_length = (body.lower_bound == body.upper_bound).then_some(body.lower_bound);
for (key, value) in tus::request_headers(upload_length) {
for (key, value) in tus::request_headers(self.length()) {
Copy link

Choose a reason for hiding this comment

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

Upload length lost because stream taken before length() call

Medium Severity

In build(), the stream is consumed via stream.take() before self.length() is called. The length() method for RequestKind::Upload reads from stream.as_ref().and_then(BoundedStream::length), which returns None since the stream was already taken. This causes request_headers(None) to emit Upload-Defer-Length: 1 instead of the correct Upload-Length header on forwarded PATCH requests.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Member

@Dav1dde Dav1dde left a comment

Choose a reason for hiding this comment

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

As we discussed: For the forward compat we can make the middleware remove sentry_key

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.

2 participants