Skip to content

feat(storage): add user_project option#5490

Open
joshuatants wants to merge 2 commits intogoogleapis:mainfrom
joshuatants:upload_project
Open

feat(storage): add user_project option#5490
joshuatants wants to merge 2 commits intogoogleapis:mainfrom
joshuatants:upload_project

Conversation

@joshuatants
Copy link
Copy Markdown
Contributor

@joshuatants joshuatants commented Apr 22, 2026

Adds a with_user_project method to Storage and StorageControl request builders to support Requester Pays buckets. When set, the internal gRPC and HTTP transports emit an x-goog-user-project header which takes precedence over any quota_project_id configured in the client credentials.

For #3189 and #3306.

@product-auto-label product-auto-label Bot added the api: storage Issues related to the Cloud Storage API. label Apr 22, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.73%. Comparing base (e519784) to head (3a8efbc).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5490   +/-   ##
=======================================
  Coverage   97.73%   97.73%           
=======================================
  Files         218      218           
  Lines       49043    49070   +27     
=======================================
+ Hits        47932    47961   +29     
+ Misses       1111     1109    -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joshuatants
Copy link
Copy Markdown
Contributor Author

Hi @googleapis/cloud-sdk-rust-team in this PR I add functionality to gax to support the request level setting of the UserProject. This seems like it should require a bump in the minor number (hence the failing minimal-versions checks). Looking around it seems that this is handled automatically by the tooling. I'm wondering what I should do here?

  1. Split this PR into two: one for the gax change which goes in first, another for the storage change.
  2. Keep the PR as in but add another feat(storage) line to cover storage.

Thank you for the advice.

@joshuatants joshuatants marked this pull request as ready for review April 22, 2026 07:00
@joshuatants joshuatants requested review from a team as code owners April 22, 2026 07:00
@dbolduc
Copy link
Copy Markdown
Member

dbolduc commented Apr 22, 2026

I'm wondering what I should do here?

The tests are failing because there is new logic in gax-internal that is not present in the latest released gax-internal.

The tests should pass if you bump the gax-internal version for the crate:

... as well as the minimum version of gax-internal set in the workspace (which is being used by storage in the failing tests):

gaxi = { default-features = false, version = "0.7.12", path = "src/gax-internal", package = "google-cloud-gax-internal" }

The change is purely additive, so make the version 0.7.13.

(The same would have been true for adding a public API to gax but note that we already did one of these version bumps since the latest release in #5421)

@coryan
Copy link
Copy Markdown
Contributor

coryan commented Apr 22, 2026

The change is purely additive, so make the version 0.7.13.

(The same would have been true for adding a public API to gax but note that we already did one of these version bumps since the latest release in #5421)

I would prefer if we split these changes to gax and gax-internal to separate PRs. Purely because it will make it easier to grok the history of the changes, not because it would fix any builds or anything.

Copy link
Copy Markdown
Member

@dbolduc dbolduc left a comment

Choose a reason for hiding this comment

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

Split this PR into two

This is typically a good idea. I would be grateful if you did this.

I did not look at the storage changes because I have enough feedback on the gax half.

Comment thread src/gax/src/options.rs Outdated
/// provider would have emitted from its configured `quota_project_id`,
/// so the wire carries exactly one `x-goog-user-project`.
#[derive(Debug, Clone, PartialEq)]
pub struct UserProject(http::HeaderValue);
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 don't want to plumb this via the extensions. Can you just add a field + setters/getters similar to user_agent, user_agent(), set_user_agent(), with_user_agent() ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread src/gax/src/options.rs Outdated
Comment on lines +288 to +290
/// # Panics
///
/// Panics if the project ID contains non-visible ASCII characters.
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.

This is valid, but not customary for us. I think we can avoid it if we treat it like a user agent. The error may be less helpful, but eh.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread src/gax-internal/src/grpc.rs Outdated
unreachable!("headers are not cached");
};

if let Some(up) = options.get_extension::<UserProject>() {
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.

Meh, don't love that this is here. But it's fine.

If we keep this, can you add a comment like:

// The client-supplied user project must override the credential's user project. So we insert it after setting the credential's headers.

But I think I would prefer is to add this logic to Self::make_headers()

And then we just flip this to:

// Note that client headers override credential headers (e.g. for the `x-goog-user-project`).
data.extend(headers);
Ok(data)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Comment thread src/gax-internal/src/http.rs Outdated
if let Some(up) = options.get_extension::<UserProject>() {
data.insert(X_GOOG_USER_PROJECT, up.as_value().clone());
}
builder.headers(data)
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.

The logic doesn't belong in the match statement. Add a block like the user agent block right after this.

Copy link
Copy Markdown
Member

@dbolduc dbolduc Apr 22, 2026

Choose a reason for hiding this comment

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

Note that you need to use the builder.headers() method to insert the key and overwrite the previous entry. builder.header() is an append 🫨


optional: at this point, we might want to construct a http::HeaderMap and call builder.headers() once. 🤷

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

}
Ok(CacheableResource::NotModified) => unreachable!("headers are not cached"),
};
builder.build().map_err(map_send_error)
Copy link
Copy Markdown
Contributor

@alvarowolfx alvarowolfx Apr 22, 2026

Choose a reason for hiding this comment

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

same as @dbolduc, I think we should add a method to the builder to allow overriding request level quota/user project. And maybe some similar code can be used in the gRPC part.

builder = quota_project(options)
            .into_iter()
            .fold(builder, |b, qp| b.header(X_GOOG_USER_PROJECT, qp));

fn quota_project(options: &crate::options::RequestOptions){
   options.get_extension::<UserProject>()
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

@joshuatants
Copy link
Copy Markdown
Contributor Author

Thank you all for the feedback. Will split the PR and address the comments. ETA for the gax changes is early next week.

Adds a `with_user_project` method to `Storage` and `StorageControl`
request builders to support Requester Pays buckets. When set, the
internal gRPC and HTTP transports emit an `x-goog-user-project` header
which takes precedence over any `quota_project_id` configured in the
client credentials.
@joshuatants joshuatants force-pushed the upload_project branch 2 times, most recently from 142e614 to 26d351c Compare April 24, 2026 07:03
@joshuatants
Copy link
Copy Markdown
Contributor Author

@dbolduc @alvarowolfx I've split the PR into two and kept only the gax changes in this PR.

gcb-pr-semver-checks is failing but I'm not sure how to fix it. By my understanding the default implementation the semver checker is complaining about is provided in impl<T> RequestOptionsBuilder for T where T: internal::RequestBuilder. Is that correct?

@dbolduc
Copy link
Copy Markdown
Member

dbolduc commented Apr 24, 2026

@joshuatants - Sorry, you're right. I offered a bad suggestion.

gcb-pr-semver-checks is failing but I'm not sure how to fix it.

It is going to be a tough fix. Let me think about it for a bit and get back to you with a plan.

By my understanding the default implementation the semver checker is complaining about is provided in impl<T> RequestOptionsBuilder for T where T: internal::RequestBuilder. Is that correct?

Yep. This was my mistake. At the time I thought I could make "breaking changes" such as adding a new trait function to public types marked as internal. Since then, we have learned that if a type is public, it is public. It doesn't matter if we hide a module from the documentation and tell external crates not to use it -- breaking the interface costs us a major version bump.

@dbolduc
Copy link
Copy Markdown
Member

dbolduc commented Apr 24, 2026

It is going to be a tough fix.

Actually, I found an easy fix, and wrote it up in go/cloud-rust:adding-request-options

Basically, I think we can provide a default implementation for any new fns we add to RequestOptionsBuilder and specialize them.

So we would say like:

pub trait RequestOptionsBuilder : internal::RequestBuilder {
    // <current fns omitted...>

    fn with_user_project<V: Into<String>>(self, _v: V) -> Self
    where Self: Sized { self }
}

instead of:

pub trait RequestOptionsBuilder : internal::RequestBuilder {
    // <current fns omitted...>

    fn with_user_project<V: Into<String>>(self, v: V) -> Self;
}

... but maybe hold off on making changes in this PR until that doc gets approved. My team often thinks of things that I missed.

@joshuatants
Copy link
Copy Markdown
Contributor Author

joshuatants commented Apr 27, 2026

Thanks for the help, will wait for updates.

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

Labels

api: storage Issues related to the Cloud Storage API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants