Skip to content
Open
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
69 changes: 62 additions & 7 deletions src/gax-internal/src/grpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub mod status;
pub mod tonic;

use crate::observability::attributes::{self, keys::*, otel_status_codes};
use crate::universe_domain::DEFAULT_UNIVERSE_DOMAIN;
use ::tonic::client::Grpc;
use ::tonic::transport::Channel;
use from_status::to_gax_error;
Expand Down Expand Up @@ -102,9 +103,18 @@ impl Client {
) -> ClientBuilderResult<Self> {
let credentials = Self::make_credentials(&config).await?;
let tracing_enabled = crate::options::tracing_enabled(&config);
let universe_domain =
crate::universe_domain::resolve(config.universe_domain.as_deref(), &credentials)
.await?;

let (inner, tracing_attributes) =
Self::make_inner(&config, default_endpoint, tracing_enabled, instrumentation).await?;
let (inner, tracing_attributes) = Self::make_inner(
&config,
default_endpoint,
tracing_enabled,
&universe_domain,
instrumentation,
)
.await?;

Ok(Self {
inner,
Expand Down Expand Up @@ -428,12 +438,14 @@ impl Client {
config: &crate::options::ClientConfig,
default_endpoint: &str,
tracing_enabled: bool,
universe_domain: &str,
instrumentation: Option<&'static crate::options::InstrumentationClientInfo>,
) -> ClientBuilderResult<(InnerClient, Option<TracingAttributes>)> {
use ::tonic::transport::{Channel, channel::Change};
let endpoint = Self::make_endpoint(
config.endpoint.clone(),
default_endpoint,
universe_domain,
config.grpc_max_header_list_size,
)
.await?;
Expand Down Expand Up @@ -478,15 +490,16 @@ impl Client {
async fn make_endpoint(
endpoint: Option<String>,
default_endpoint: &str,
universe_domain: &str,
grpc_max_header_list_size: Option<u32>,
) -> ClientBuilderResult<::tonic::transport::Endpoint> {
use ::tonic::transport::{ClientTlsConfig, Endpoint};

let origin = crate::host::origin(endpoint.as_deref(), default_endpoint)
let service_endpoint = default_endpoint.replace(DEFAULT_UNIVERSE_DOMAIN, universe_domain);
let origin = crate::host::origin(endpoint.as_deref(), default_endpoint, universe_domain)
.map_err(|e| e.client_builder())?;
let endpoint =
Endpoint::from_shared(endpoint.unwrap_or_else(|| default_endpoint.to_string()))
.map_err(BuilderError::transport)?;
let target_endpoint = endpoint.unwrap_or(service_endpoint);
let endpoint = Endpoint::from_shared(target_endpoint).map_err(BuilderError::transport)?;
let endpoint = if endpoint
.uri()
.scheme()
Expand Down Expand Up @@ -619,8 +632,50 @@ where

#[cfg(test)]
mod tests {
use super::Client;
use super::*;
use crate::options::InstrumentationClientInfo;
use test_case::test_case;

type TestResult = anyhow::Result<()>;

#[tokio::test]
#[test_case(None, "my-universe-domain.com", "https://language.my-universe-domain.com/"; "default endpoint")]
#[test_case(Some("https://yet-another-universe-domain.com/"), "yet-another-universe-domain.com", "https://yet-another-universe-domain.com/"; "custom endpoint override")]
#[test_case(Some("https://rep.language.googleapis.com/"), "my-universe-domain.com", "https://rep.language.googleapis.com/"; "regional endpoint with universe domain")]
#[test_case(Some("https://us-central1-language.googleapis.com/"), "my-universe-domain.com", "https://us-central1-language.googleapis.com/"; "locational endpoint with universe domain")]
async fn make_endpoint_with_universe_domain(
endpoint_override: Option<&str>,
universe_domain: &str,
expected_uri: &str,
) -> TestResult {
let default_endpoint = "https://language.googleapis.com";
let endpoint = Client::make_endpoint(
endpoint_override.map(String::from),
default_endpoint,
universe_domain,
None,
)
.await?;

assert_eq!(endpoint.uri().to_string(), expected_uri);

Ok(())
}

#[tokio::test]
async fn make_endpoint_with_universe_domain_mismatch() -> TestResult {
let mut config = crate::options::ClientConfig::default();
config.universe_domain = Some("my-universe-domain.com".to_string());
config.cred = Some(google_cloud_auth::credentials::anonymous::Builder::new().build());

let err = Client::new(config, "https://language.googleapis.com")
.await
.unwrap_err();

assert!(err.is_universe_domain_mismatch(), "{err:?}");

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_new_with_instrumentation() {
Expand Down
90 changes: 76 additions & 14 deletions src/gax-internal/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::universe_domain::DEFAULT_UNIVERSE_DOMAIN;
use google_cloud_gax::client_builder::Error as BuilderError;
#[cfg(any(test, feature = "_internal-http-client"))]
use google_cloud_gax::error::Error;
Expand All @@ -23,8 +24,12 @@ use std::str::FromStr;
/// Notably, locational and regional endpoints are detected and used as the
/// host. For VIPs and private networks, we need to use the default host.
#[cfg(any(test, feature = "_internal-http-client"))]
pub(crate) fn header(endpoint: Option<&str>, default_endpoint: &str) -> Result<String, HostError> {
origin_and_header(endpoint, default_endpoint).map(|(_, header)| header)
pub(crate) fn header(
endpoint: Option<&str>,
default_endpoint: &str,
universe_domain: &str,
) -> Result<String, HostError> {
origin_and_header(endpoint, default_endpoint, universe_domain).map(|(_, header)| header)
}

/// Calculate the gRPC authority given the endpoint and default endpoint.
Expand All @@ -34,15 +39,21 @@ pub(crate) fn header(endpoint: Option<&str>, default_endpoint: &str) -> Result<S
///
/// Tonic consumes the authority as a [http::Uri].
#[cfg(any(test, feature = "_internal-grpc-client"))]
pub(crate) fn origin(endpoint: Option<&str>, default_endpoint: &str) -> Result<Uri, HostError> {
origin_and_header(endpoint, default_endpoint).map(|(origin, _)| origin)
pub(crate) fn origin(
endpoint: Option<&str>,
default_endpoint: &str,
universe_domain: &str,
) -> Result<Uri, HostError> {
origin_and_header(endpoint, default_endpoint, universe_domain).map(|(origin, _)| origin)
}

fn origin_and_header(
endpoint: Option<&str>,
default_endpoint: &str,
universe_domain: &str,
) -> Result<(Uri, String), HostError> {
let default_origin = Uri::from_str(default_endpoint).map_err(HostError::Uri)?;
let service_endpoint = default_endpoint.replace(DEFAULT_UNIVERSE_DOMAIN, &universe_domain);
let default_origin = Uri::from_str(&service_endpoint).map_err(HostError::Uri)?;
let default_host = default_origin
.authority()
.expect("missing authority in default endpoint")
Expand All @@ -58,9 +69,11 @@ fn origin_and_header(
.ok_or_else(|| HostError::MissingAuthority(endpoint.to_string()))?
.host()
.to_string();

let universe_suffix = format!(".{universe_domain}");
let (Some(prefix), Some(service)) = (
custom_host.strip_suffix(".googleapis.com"),
default_host.strip_suffix(".googleapis.com"),
custom_host.strip_suffix(&universe_suffix),
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 think this fails to strip the suffix on the .with_endpoint("foo.googleapis.com").with_universe_domain("my-custom.ud") case and then it returns "foo.custom.ud", when it should return "foo.googleapis.com".

Assuming I know what the caller is supplying to this function.

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.

yeah, that's the conflicting thing that I'm stuck on this feature. You commented that before on #5302 (comment).

I'm trying to make the code look good and pass the test cases for localhost and the scenario that you described.

#[test_case("localhost:5678", "test.googleapis.com"; "emulator")]
#[test_case("https://localhost:5678", "test.googleapis.com"; "emulator with scheme")]
#[test_case("https://test.googleapis.com", "test.googleapis.com"; "GDU endpoint with universe domain") ]
#[test_case("https://test.another-universe.com", "test.another-universe.com"; "endpoint priority") ]
fn header_universe_domain(input: &str, want: &str) -> anyhow::Result<()> {
    let got = header(
        Some(input),
        "https://test.googleapis.com",
        "my-custom-universe.com",
    )?;
    assert_eq!(got, want, "input={input:?}");
    Ok(())
}

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.

FWIW, I think these tests are wrong. Not sure why I wrote them.

#[test_case("https://test.my-universe-domain.com", "https://test.googleapis.com"; "universe domain")]
#[test_case("localhost:5678", "https://test.googleapis.com"; "emulator")]
#[test_case("http://localhost:5678", "https://test.googleapis.com"; "emulator with scheme")]

Gemini tells me we should use "localhost" as the host when talking to an emulator, not the default service endpoint.

Copy link
Copy Markdown
Contributor Author

@alvarowolfx alvarowolfx Apr 18, 2026

Choose a reason for hiding this comment

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

yup, I'm also finding that some previous assertions for universe domains might be incorrect, like

#[test_case("https://test.my-universe-domain.com", "https://test.googleapis.com"; "universe domain")]
, it should prioritize the endpoint override, so host should be test.my-universe-domain.com

default_host.strip_suffix(&universe_suffix),
) else {
return Ok((default_origin, default_host));
};
Expand Down Expand Up @@ -115,6 +128,7 @@ impl HostError {
#[cfg(test)]
mod tests {
use super::*;
use crate::universe_domain::DEFAULT_UNIVERSE_DOMAIN;
use std::error::Error as _;
use test_case::test_case;

Expand All @@ -131,7 +145,11 @@ mod tests {
#[test_case("localhost:5678", "test.googleapis.com"; "emulator")]
#[test_case("https://localhost:5678", "test.googleapis.com"; "emulator with scheme")]
fn header_success(input: &str, want: &str) -> anyhow::Result<()> {
let got = header(Some(input), "https://test.googleapis.com")?;
let got = header(
Some(input),
"https://test.googleapis.com",
DEFAULT_UNIVERSE_DOMAIN,
)?;
assert_eq!(got, want, "input={input:?}");
Ok(())
}
Expand All @@ -144,7 +162,23 @@ mod tests {
#[test_case("localhost:5678", "localhost"; "emulator")]
#[test_case("https://localhost:5678", "localhost"; "emulator with scheme")]
fn header_default(input: &str, want: &str) -> anyhow::Result<()> {
let got = header(None, input)?;
let got = header(None, input, DEFAULT_UNIVERSE_DOMAIN)?;
assert_eq!(got, want, "input={input:?}");
Ok(())
}

#[test_case("http://www.my-custom-universe.com", "test.my-custom-universe.com"; "global")]
#[test_case("http://private.my-custom-universe.com", "test.my-custom-universe.com"; "VPC-SC private")]
#[test_case("http://restricted.my-custom-universe.com", "test.my-custom-universe.com"; "VPC-SC restricted")]
#[test_case("http://test-my-private-ep.p.my-custom-universe.com", "test.my-custom-universe.com"; "PSC custom endpoint")]
#[test_case("https://us-central1-test.my-custom-universe.com", "us-central1-test.my-custom-universe.com"; "locational endpoint")]
#[test_case("https://test.us-central1.rep.my-custom-universe.com", "test.us-central1.rep.my-custom-universe.com"; "regional endpoint")]
fn header_universe_domain(input: &str, want: &str) -> anyhow::Result<()> {
let got = header(
Some(input),
"https://test.googleapis.com",
"my-custom-universe.com",
)?;
assert_eq!(got, want, "input={input:?}");
Ok(())
}
Expand All @@ -162,7 +196,11 @@ mod tests {
#[test_case("localhost:5678", "https://test.googleapis.com"; "emulator")]
#[test_case("http://localhost:5678", "https://test.googleapis.com"; "emulator with scheme")]
fn origin_success(input: &str, want: &str) -> anyhow::Result<()> {
let got = origin(Some(input), "https://test.googleapis.com")?;
let got = origin(
Some(input),
"https://test.googleapis.com",
DEFAULT_UNIVERSE_DOMAIN,
)?;
assert_eq!(got, want, "input={input:?}");
Ok(())
}
Expand All @@ -175,21 +213,45 @@ mod tests {
#[test_case("https://localhost:5678", "https://localhost:5678")]
#[test_case("http://localhost:5678", "http://localhost:5678")]
fn origin_default(input: &str, want: &str) -> anyhow::Result<()> {
let got = origin(None, input)?;
let got = origin(None, input, DEFAULT_UNIVERSE_DOMAIN)?;
assert_eq!(got, want, "input={input:?}");
Ok(())
}

#[test_case("http://www.my-custom-universe.com", "https://test.my-custom-universe.com"; "global")]
#[test_case("http://private.my-custom-universe.com", "https://test.my-custom-universe.com"; "VPC-SC private")]
#[test_case("http://restricted.my-custom-universe.com", "https://test.my-custom-universe.com"; "VPC-SC restricted")]
#[test_case("http://test-my-private-ep.p.my-custom-universe.com", "https://test.my-custom-universe.com"; "PSC custom endpoint")]
#[test_case("https://us-central1-test.my-custom-universe.com", "https://us-central1-test.my-custom-universe.com"; "locational endpoint")]
#[test_case("https://test.us-central1.rep.my-custom-universe.com", "https://test.us-central1.rep.my-custom-universe.com"; "regional endpoint")]
fn origin_universe_domain(input: &str, want: &str) -> anyhow::Result<()> {
let got = origin(
Some(input),
"https://test.googleapis.com",
"my-custom-universe.com",
)?;
assert_eq!(got, want, "input={input:?}");
Ok(())
}

#[test]
fn errors() {
let got = origin_and_header(Some("https:///a/b/c"), "https://test.googleapis.com");
let got = origin_and_header(
Some("https:///a/b/c"),
"https://test.googleapis.com",
DEFAULT_UNIVERSE_DOMAIN,
);
assert!(matches!(got, Err(HostError::Uri(_))), "{got:?}");
let got = origin_and_header(Some("/a/b/c"), "https://test.googleapis.com");
let got = origin_and_header(
Some("/a/b/c"),
"https://test.googleapis.com",
DEFAULT_UNIVERSE_DOMAIN,
);
assert!(
matches!(got, Err(HostError::MissingAuthority(ref e)) if e == "/a/b/c"),
"{got:?}"
);
let got = origin_and_header(None, "https:///");
let got = origin_and_header(None, "https:///", DEFAULT_UNIVERSE_DOMAIN);
assert!(matches!(got, Err(HostError::Uri(_))), "{got:?}");
}

Expand Down
Loading
Loading