Skip to content
Merged
Show file tree
Hide file tree
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
23 changes: 20 additions & 3 deletions src/gax-internal/src/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ pub struct Client {
credentials: Credentials,
transport_policies: TransportPolicies,
attempt_interceptor: Option<Arc<dyn AttemptInterceptor>>,
extensions: crate::options::Extensions,
}

impl Client {
Expand Down Expand Up @@ -122,6 +123,7 @@ impl Client {
credentials,
transport_policies: TransportPolicies::from_config(&config),
attempt_interceptor: None,
extensions: config.extensions,
})
}

Expand All @@ -144,7 +146,12 @@ impl Client {
Request: prost::Message + Clone + 'static,
Response: prost::Message + Default + 'static,
{
let headers = make_headers(api_client_header, request_params, &options)?;
let headers = make_headers(
&self.extensions,
api_client_header,
request_params,
&options,
)?;
self.retry_loop::<Request, Response>(extensions, path, request, options, headers)
.await
}
Expand Down Expand Up @@ -194,7 +201,12 @@ impl Client {
Response: prost::Message + Default + 'static,
{
use ::tonic::IntoStreamingRequest;
let headers = make_headers(api_client_header, request_params, &options)?;
let headers = make_headers(
&self.extensions,
api_client_header,
request_params,
&options,
)?;
let mut headers = add_auth_headers(headers, &self.credentials).await?;
self.intercept(&mut headers, 1);
let metadata = tonic::MetadataMap::from_headers(headers);
Expand Down Expand Up @@ -334,7 +346,12 @@ impl Client {
Response: prost::Message + Default + 'static,
{
use ::tonic::IntoRequest;
let headers = make_headers(api_client_header, request_params, &options)?;
let headers = make_headers(
&self.extensions,
api_client_header,
request_params,
&options,
)?;
let mut headers = add_auth_headers(headers, &self.credentials).await?;
self.intercept(&mut headers, 1);
let metadata = tonic::MetadataMap::from_headers(headers);
Expand Down
168 changes: 141 additions & 27 deletions src/gax-internal/src/grpc/grpc_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::headers::{
X_GOOG_API_CLIENT, X_GOOG_REQUEST_PARAMS, X_GOOG_USER_PROJECT, sanitize_custom_headers,
};
use crate::observability::attributes::{self, keys::*, otel_status_codes};
use crate::observability::{WithTransportLogging, WithTransportMetric, WithTransportSpan};
use crate::options::ClientConfig;
Expand All @@ -23,15 +26,10 @@ use google_cloud_gax::client_builder::{Error as BuilderError, Result as ClientBu
use google_cloud_gax::error::Error;
use google_cloud_gax::options::RequestOptions;
use google_cloud_gax::options::internal::RequestOptionsExt as _;
use http::{HeaderMap, header::HeaderName};
use http::HeaderMap;
use opentelemetry_semantic_conventions::{attribute as otel_attr, trace as otel_trace};
use std::future::Future;

const X_GOOG_API_CLIENT: HeaderName = HeaderName::from_static("x-goog-api-client");
const X_GOOG_REQUEST_PARAMS: HeaderName = HeaderName::from_static("x-goog-request-params");
const X_GOOG_USER_PROJECT: HeaderName = HeaderName::from_static("x-goog-user-project");
const X_GOOG_API_KEY: HeaderName = HeaderName::from_static("x-goog-api-key");

/// Extends the supplied `headers` map with authentication headers from a
/// `Credentials` object. For entries with the same header name, the one in
/// `headers` takes precedence.
Expand Down Expand Up @@ -66,30 +64,33 @@ pub(crate) fn make_credentials(config: &ClientConfig) -> ClientBuilderResult<Cre
}

/// Constructs the headers required for Google Cloud API requests.
/// Custom headers can be provided through `RequestOptions`.
///
/// Custom headers can be provided through global `extensions` (client-level) as well as
/// per-call `options` ([`RequestOptions`]). Request-level custom headers override global-level
/// headers when key conflicts occur.
///
/// Returns an error if any of the header values fail to parse.
pub(crate) fn make_headers(
extensions: &crate::options::Extensions,
api_client_header: &'static str,
request_params: &str,
options: &RequestOptions,
) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();

if let Some(global_headers) = extensions.get::<HeaderMap>() {
headers.extend(global_headers.clone());
}

if let Some(custom_headers) = options.get_extension::<HeaderMap>() {
for k in custom_headers.keys() {
headers.remove(k);
}
headers.extend(custom_headers.clone());
}
Comment thread
xlai20 marked this conversation as resolved.

// Sanitize user custom headers by stripping away any keys conflicting with system headers.
for key in [
http::header::USER_AGENT,
http::header::AUTHORIZATION,
X_GOOG_API_KEY,
X_GOOG_USER_PROJECT,
X_GOOG_REQUEST_PARAMS,
X_GOOG_API_CLIENT,
] {
headers.remove(key);
}
sanitize_custom_headers(&mut headers);

if let Some(user_agent) = options.user_agent() {
headers.insert(
Expand Down Expand Up @@ -215,6 +216,7 @@ where
#[cfg(test)]
mod tests {
use super::*;
use crate::headers::X_GOOG_API_KEY;
use google_cloud_auth::credentials::{CacheableResource, CredentialsProvider, EntityTag};
use google_cloud_auth::errors::CredentialsError;
use http::{Extensions, header::HeaderName, header::HeaderValue};
Expand Down Expand Up @@ -338,7 +340,12 @@ mod tests {
options.set_quota_project(QUOTA_PROJECT);

// Act
let headers = make_headers(API_CLIENT_HEADER, REQUEST_PARAMS, &options)?;
let headers = make_headers(
&crate::options::Extensions::new(),
API_CLIENT_HEADER,
REQUEST_PARAMS,
&options,
)?;

// Assert
assert_eq!(headers.get(X_GOOG_API_CLIENT).unwrap(), API_CLIENT_HEADER);
Expand All @@ -351,7 +358,12 @@ mod tests {
#[test]
fn make_headers_omits_unset_params() -> TestResult {
// Act
let headers = make_headers(API_CLIENT_HEADER, "", &RequestOptions::default())?;
let headers = make_headers(
&crate::options::Extensions::new(),
API_CLIENT_HEADER,
"",
&RequestOptions::default(),
)?;

// Assert
assert_eq!(headers.get(X_GOOG_API_CLIENT).unwrap(), API_CLIENT_HEADER);
Expand All @@ -364,6 +376,52 @@ mod tests {
Ok(())
}

#[test]
fn make_headers_with_global_headers() -> TestResult {
// Arrange
const GLOBAL_HEADER_NAME: &str = "x-global-header";
const GLOBAL_HEADER_VAL: &str = "global-val";
let mut global_headers = HeaderMap::new();
global_headers.insert(
GLOBAL_HEADER_NAME,
HeaderValue::from_static(GLOBAL_HEADER_VAL),
);
let mut extensions = crate::options::Extensions::new();
extensions.insert(global_headers);

// Act
let headers = make_headers(
&extensions,
API_CLIENT_HEADER,
"",
&RequestOptions::default(),
)?;

// Assert
assert_eq!(headers.get(GLOBAL_HEADER_NAME).unwrap(), GLOBAL_HEADER_VAL);
Ok(())
}

#[test]
fn make_headers_precedence_request_over_global() -> TestResult {
// Arrange
const KEY: &str = "x-test-precedence";
let mut global_headers = HeaderMap::new();
global_headers.insert(KEY, HeaderValue::from_static("global-wins")); // Initial global
let mut extensions = crate::options::Extensions::new();
extensions.insert(global_headers);
let mut request_headers = HeaderMap::new();
request_headers.insert(KEY, HeaderValue::from_static("request-wins")); // Request overrides global
let options = RequestOptions::default().insert_extension(request_headers);

// Act
let headers = make_headers(&extensions, API_CLIENT_HEADER, "", &options)?;

// Assert
assert_eq!(headers.get(KEY).unwrap(), "request-wins"); // Request merged on top of global
Ok(())
}

#[test]
fn make_headers_with_custom_headers() -> TestResult {
// Arrange
Expand All @@ -377,7 +435,12 @@ mod tests {
let options = RequestOptions::default().insert_extension(custom_headers);

// Act
let headers = make_headers(API_CLIENT_HEADER, CUSTOM_REQUEST_PARAMS, &options)?;
let headers = make_headers(
&crate::options::Extensions::new(),
API_CLIENT_HEADER,
CUSTOM_REQUEST_PARAMS,
&options,
)?;

// Assert
assert_eq!(headers.get(X_GOOG_API_CLIENT).unwrap(), API_CLIENT_HEADER);
Expand All @@ -394,18 +457,33 @@ mod tests {
// Invalid user agent
let mut options = RequestOptions::default();
options.set_user_agent("invalid\nagent");
let res = make_headers(API_CLIENT_HEADER, "param=1", &options);
let res = make_headers(
&crate::options::Extensions::new(),
API_CLIENT_HEADER,
"param=1",
&options,
);
assert!(res.is_err(), "{res:?}");

// Invalid quota project
let mut options = RequestOptions::default();
options.set_quota_project("invalid\nproject");
let res = make_headers(API_CLIENT_HEADER, "param=1", &options);
let res = make_headers(
&crate::options::Extensions::new(),
API_CLIENT_HEADER,
"param=1",
&options,
);
assert!(res.is_err(), "{res:?}");

// Invalid request params
let options = RequestOptions::default();
let res = make_headers(API_CLIENT_HEADER, "invalid\nparams", &options);
let res = make_headers(
&crate::options::Extensions::new(),
API_CLIENT_HEADER,
"invalid\nparams",
&options,
);
assert!(res.is_err(), "{res:?}");
}

Expand Down Expand Up @@ -438,6 +516,35 @@ mod tests {
custom_headers
}

fn test_global_custom_headers() -> HeaderMap {
let mut custom_headers = HeaderMap::new();
// Try to override system headers with conflicting global values
custom_headers.insert(
http::header::USER_AGENT,
HeaderValue::from_static("global-agent"),
);
custom_headers.insert(
X_GOOG_USER_PROJECT,
HeaderValue::from_static("global-project"),
);
custom_headers.insert(X_GOOG_API_CLIENT, HeaderValue::from_static("global-client"));
custom_headers.insert(
X_GOOG_REQUEST_PARAMS,
HeaderValue::from_static("global-params"),
);
custom_headers.insert(
http::header::AUTHORIZATION,
HeaderValue::from_static("global-authorization"),
);
custom_headers.insert(X_GOOG_API_KEY, HeaderValue::from_static("global-api-key"));
// A legitimate custom header, distinct from the request-level one
custom_headers.insert(
"x-legitimate-header",
HeaderValue::from_static("global-legitimate-value"),
);
custom_headers
}

#[test]
fn make_headers_enforces_system_precedence_with_values() -> TestResult {
// Arrange
Expand All @@ -449,9 +556,15 @@ mod tests {
options.set_user_agent(TEST_USER_AGENT);
options.set_quota_project(TEST_QUOTA_PROJECT);
let options = options.insert_extension(test_custom_headers());

let mut extensions = crate::options::Extensions::new();
extensions.insert(test_global_custom_headers());
// Act
let headers = make_headers(API_CLIENT_HEADER, TEST_REQUEST_PARAMS, &options)?;
let headers = make_headers(
&extensions,
API_CLIENT_HEADER,
TEST_REQUEST_PARAMS,
&options,
)?;

// Assert
let expected = HeaderMap::from_iter([
Expand Down Expand Up @@ -485,9 +598,10 @@ mod tests {
fn make_headers_enforces_system_precedence_without_values() -> TestResult {
// Arrange
let options = RequestOptions::default().insert_extension(test_custom_headers());

let mut extensions = crate::options::Extensions::new();
extensions.insert(test_global_custom_headers());
// Act (pass empty request params, empty user agent, empty quota project)
let headers = make_headers(API_CLIENT_HEADER, "", &options)?;
let headers = make_headers(&extensions, API_CLIENT_HEADER, "", &options)?;

// Assert
let expected = HeaderMap::from_iter([
Expand Down
38 changes: 38 additions & 0 deletions src/gax-internal/src/headers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use http::header::{HeaderMap, HeaderName};

pub(crate) const X_GOOG_USER_PROJECT: HeaderName = HeaderName::from_static("x-goog-user-project");
pub(crate) const X_GOOG_API_KEY: HeaderName = HeaderName::from_static("x-goog-api-key");
pub(crate) const X_GOOG_REQUEST_PARAMS: HeaderName =
HeaderName::from_static("x-goog-request-params");
pub(crate) const X_GOOG_API_CLIENT: HeaderName = HeaderName::from_static("x-goog-api-client");

/// Strips all reserved system and telemetry keys from the provided custom headers map.
///
/// This is a shared utility across both HTTP and gRPC transport layers, with end-to-end
/// sanitization behavior covered by transport test suites on both sides.
pub(crate) fn sanitize_custom_headers(headers: &mut HeaderMap) {
for key in [
http::header::USER_AGENT,
http::header::AUTHORIZATION,
X_GOOG_API_KEY,
X_GOOG_USER_PROJECT,
X_GOOG_REQUEST_PARAMS,
X_GOOG_API_CLIENT,
] {
headers.remove(key);
}
}
Loading
Loading