diff --git a/.bazelrc b/.bazelrc
index 554440cfe3d..453acedb593 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1 +1,3 @@
-build --cxxopt=-std=c++14 --host_cxxopt=-std=c++14
+build --cxxopt=-std=c++17 --host_cxxopt=-std=c++17
+
+common:skip_android --deleted_packages=android,binder
\ No newline at end of file
diff --git a/.gemini/config.yaml b/.gemini/config.yaml
new file mode 100644
index 00000000000..71adf793964
--- /dev/null
+++ b/.gemini/config.yaml
@@ -0,0 +1,13 @@
+have_fun: false
+memory_config:
+ disabled: false
+code_review:
+ disable: false
+ comment_severity_threshold: MEDIUM
+ max_review_comments: -1
+ pull_request_opened:
+ help: false
+ summary: false
+ code_review: false
+ include_drafts: false
+ignore_patterns: []
diff --git a/.github/workflows/branch-testing.yml b/.github/workflows/branch-testing.yml
index ece8ec4cd58..2f462242f38 100644
--- a/.github/workflows/branch-testing.yml
+++ b/.github/workflows/branch-testing.yml
@@ -20,14 +20,14 @@ jobs:
fail-fast: false # Should swap to true if we grow a large matrix
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-java@v4
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
with:
java-version: ${{ matrix.jre }}
distribution: 'temurin'
- name: Gradle cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.gradle/caches
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index da1e2fed114..91eada5371d 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -9,5 +9,5 @@ jobs:
name: "Gradle wrapper validation"
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: gradle/actions/wrapper-validation@v4
+ - uses: actions/checkout@v6
+ - uses: gradle/actions/wrapper-validation@v6
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index 3070a1a2f7c..b29a5573512 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -13,7 +13,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- - uses: dessant/lock-threads@v5
+ - uses: dessant/lock-threads@v6
with:
github-token: ${{ github.token }}
issue-inactive-days: 90
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 4fe75b0be78..0292e6602f9 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -21,14 +21,14 @@ jobs:
fail-fast: false # Should swap to true if we grow a large matrix
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-java@v4
+ - uses: actions/checkout@v6
+ - uses: actions/setup-java@v5
with:
java-version: ${{ matrix.jre }}
distribution: 'temurin'
- name: Gradle cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.gradle/caches
@@ -37,7 +37,7 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-
- name: Maven cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.m2/repository
@@ -46,7 +46,7 @@ jobs:
restore-keys: |
${{ runner.os }}-maven-
- name: Protobuf cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: /tmp/protobuf-cache
key: ${{ runner.os }}-maven-${{ hashFiles('buildscripts/make_dependencies.sh') }}
@@ -55,7 +55,7 @@ jobs:
run: buildscripts/kokoro/unix.sh
- name: Post Failure Upload Test Reports to Artifacts
if: ${{ failure() }}
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: Test Reports (JRE ${{ matrix.jre }})
path: |
@@ -71,7 +71,7 @@ jobs:
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
run: ./gradlew :grpc-all:coveralls -PskipAndroid=true -x compileJava
- name: Codecov
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -80,11 +80,15 @@ jobs:
strategy:
matrix:
bzlmod: [true, false]
+ bazel_version: [8.7.0, 9.1.0]
+ exclude:
+ - bazel_version: 9.1.0
+ bzlmod: false
env:
- USE_BAZEL_VERSION: 7.0.0
+ USE_BAZEL_VERSION: ${{ matrix.bazel_version }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Check versions match in MODULE.bazel and repositories.bzl
run: |
@@ -92,7 +96,7 @@ jobs:
<(sed -n '/GRPC_DEPS_START/,/GRPC_DEPS_END/ {/GRPC_DEPS_/! p}' repositories.bzl)
- name: Bazel cache
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
~/.cache/bazel/*/cache
@@ -100,11 +104,11 @@ jobs:
key: ${{ runner.os }}-bazel-${{ env.USE_BAZEL_VERSION }}-${{ hashFiles('WORKSPACE', 'repositories.bzl') }}
- name: Run bazel build
- run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }}
+ run: bazelisk build //... --config=skip_android --enable_bzlmod=${{ matrix.bzlmod }} --enable_workspace=${{ !matrix.bzlmod }}
- name: Run bazel test
- run: bazelisk test //... --enable_bzlmod=${{ matrix.bzlmod }}
+ run: bazelisk test //... --config=skip_android --enable_bzlmod=${{ matrix.bzlmod }} --enable_workspace=${{ !matrix.bzlmod }}
- name: Run example bazel build
- run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }}
+ run: bazelisk build //... --enable_bzlmod=${{ matrix.bzlmod }} --enable_workspace=${{ !matrix.bzlmod }}
working-directory: ./examples
diff --git a/.gitignore b/.gitignore
index 92a0e3d6d3a..b078d891adf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,6 +31,9 @@ MODULE.bazel.lock
.gitignore
bin
+# VsCode
+.vscode
+
# OS X
.DS_Store
diff --git a/BUILD.bazel b/BUILD.bazel
index fee2c0bd12d..27a99fb62eb 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -12,7 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-load("@rules_java//java:defs.bzl", "java_library", "java_plugin", "java_proto_library")
+load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library")
+load("@rules_java//java:defs.bzl", "java_library", "java_plugin")
load("@rules_jvm_external//:defs.bzl", "artifact")
load(":java_grpc_library.bzl", "java_grpc_library")
diff --git a/COMPILING.md b/COMPILING.md
index b7df1319beb..78247394cee 100644
--- a/COMPILING.md
+++ b/COMPILING.md
@@ -150,3 +150,26 @@ $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses
# Add 'export ANDROID_HOME=$HOME/Android/Sdk' to your .bashrc or equivalent
```
+
+Building with Bazel
+===================
+
+grpc-java can also be built using [Bazel](https://bazel.build/). We support
+the two most recent major versions of Bazel.
+[Install bazelisk](https://github.com/bazelbuild/bazelisk#installation)
+to ensure you're always building using the latest supported version, then try:
+
+```
+$ bazelisk build //...
+```
+
+Some parts of grpc-java depend on Android. Bazel can build these parts too but,
+for size, licensing and maintenance reasons, it requires a locally installed
+Android SDK. If you don't have the SDK and/or don't care about Android, use the
+`skip_android` configuration to skip building the Android parts:
+
+```sh
+$ bazelisk build //... --config=skip_android
+```
+
+You cannot run the tests from Bazel at this time.
diff --git a/MODULE.bazel b/MODULE.bazel
index 83ff9eaa539..9f0de3eb789 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -1,64 +1,75 @@
module(
name = "grpc-java",
- compatibility_level = 0,
+ version = "1.83.0-SNAPSHOT", # CURRENT_GRPC_VERSION
repo_name = "io_grpc_grpc_java",
- version = "1.77.0-SNAPSHOT", # CURRENT_GRPC_VERSION
)
# GRPC_DEPS_START
IO_GRPC_GRPC_JAVA_ARTIFACTS = [
"com.google.android:annotations:4.1.1.4",
- "com.google.api.grpc:proto-google-common-protos:2.59.2",
- "com.google.auth:google-auth-library-credentials:1.24.1",
- "com.google.auth:google-auth-library-oauth2-http:1.24.1",
+ "com.google.api.grpc:proto-google-common-protos:2.64.1",
+ "com.google.auth:google-auth-library-credentials:1.42.1",
+ "com.google.auth:google-auth-library-oauth2-http:1.42.1",
"com.google.auto.value:auto-value-annotations:1.11.0",
"com.google.auto.value:auto-value:1.11.0",
"com.google.code.findbugs:jsr305:3.0.2",
- "com.google.code.gson:gson:2.11.0",
- "com.google.errorprone:error_prone_annotations:2.36.0",
+ "com.google.code.gson:gson:2.14.0",
+ "com.google.errorprone:error_prone_annotations:2.50.0",
"com.google.guava:failureaccess:1.0.1",
- "com.google.guava:guava:33.4.8-android",
+ "com.google.guava:guava:33.6.0-android",
"com.google.re2j:re2j:1.8",
- "com.google.s2a.proto.v2:s2a-proto:0.1.2",
- "com.google.truth:truth:1.4.2",
+ "com.google.s2a.proto.v2:s2a-proto:0.1.3",
+ "com.google.truth:truth:1.4.5",
+ "dev.cel:runtime:0.13.0",
+ "dev.cel:protobuf:0.13.0",
+ "dev.cel:common:0.13.0",
"com.squareup.okhttp:okhttp:2.7.5",
"com.squareup.okio:okio:2.10.0", # 3.0+ needs swapping to -jvm; need work to avoid flag-day
- "io.netty:netty-buffer:4.1.124.Final",
- "io.netty:netty-codec-http2:4.1.124.Final",
- "io.netty:netty-codec-http:4.1.124.Final",
- "io.netty:netty-codec-socks:4.1.124.Final",
- "io.netty:netty-codec:4.1.124.Final",
- "io.netty:netty-common:4.1.124.Final",
- "io.netty:netty-handler-proxy:4.1.124.Final",
- "io.netty:netty-handler:4.1.124.Final",
- "io.netty:netty-resolver:4.1.124.Final",
- "io.netty:netty-tcnative-boringssl-static:2.0.70.Final",
- "io.netty:netty-tcnative-classes:2.0.70.Final",
- "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.1.124.Final",
- "io.netty:netty-transport-native-unix-common:4.1.124.Final",
- "io.netty:netty-transport:4.1.124.Final",
+ "io.netty:netty-buffer:4.2.15.Final",
+ "io.netty:netty-codec-base:4.2.15.Final",
+ "io.netty:netty-codec-http2:4.2.15.Final",
+ "io.netty:netty-codec-http:4.2.15.Final",
+ "io.netty:netty-codec-socks:4.2.15.Final",
+ "io.netty:netty-common:4.2.15.Final",
+ "io.netty:netty-handler-proxy:4.2.15.Final",
+ "io.netty:netty-handler:4.2.15.Final",
+ "io.netty:netty-resolver:4.2.15.Final",
+ "io.netty:netty-tcnative-boringssl-static:2.0.75.Final",
+ "io.netty:netty-tcnative-classes:2.0.75.Final",
+ "io.netty:netty-transport-native-epoll:jar:linux-x86_64:4.2.15.Final",
+ "io.netty:netty-transport-native-unix-common:4.2.15.Final",
+ "io.netty:netty-transport:4.2.15.Final",
"io.opencensus:opencensus-api:0.31.0",
"io.opencensus:opencensus-contrib-grpc-metrics:0.31.0",
"io.perfmark:perfmark-api:0.27.0",
"junit:junit:4.13.2",
+ "org.mockito:mockito-core:4.4.0",
"org.checkerframework:checker-qual:3.49.5",
- "org.codehaus.mojo:animal-sniffer-annotations:1.24",
+ "org.codehaus.mojo:animal-sniffer-annotations:1.27",
]
# GRPC_DEPS_END
-bazel_dep(name = "bazel_jar_jar", version = "0.1.7")
+bazel_dep(name = "abseil-cpp", version = "20250512.1")
+bazel_dep(name = "bazel_jar_jar", version = "0.1.11.bcr.1")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
-bazel_dep(name = "googleapis", repo_name = "com_google_googleapis", version = "0.0.0-20240326-1c8d509c5")
-bazel_dep(name = "grpc-proto", repo_name = "io_grpc_grpc_proto", version = "0.0.0-20240627-ec30f58")
-# Protobuf 25.5+ is incompatible with Bazel 7 with bzlmod
-bazel_dep(name = "protobuf", repo_name = "com_google_protobuf", version = "24.4")
+bazel_dep(name = "googleapis", version = "0.0.0-20260514-1dbb1a14", repo_name = "com_google_googleapis")
+bazel_dep(name = "grpc-proto", version = "0.0.0-20240627-ec30f58.bcr.1", repo_name = "io_grpc_grpc_proto")
+bazel_dep(name = "protobuf", version = "35.1", repo_name = "com_google_protobuf")
+bazel_dep(name = "rules_proto", version = "7.1.0")
+bazel_dep(name = "rules_android", version = "0.7.3")
bazel_dep(name = "rules_cc", version = "0.0.9")
-bazel_dep(name = "rules_java", version = "5.3.5")
+bazel_dep(name = "rules_java", version = "9.1.0")
bazel_dep(name = "rules_jvm_external", version = "6.0")
-bazel_dep(name = "rules_proto", version = "5.3.0-21.7")
-maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
+android_sdk_repository_extension = use_extension(
+ "@rules_android//rules/android_sdk_repository:rule.bzl",
+ "android_sdk_repository_extension",
+)
+use_repo(android_sdk_repository_extension, "androidsdk")
+
+register_toolchains("@androidsdk//:sdk-toolchain", "@androidsdk//:all")
+maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
artifacts = IO_GRPC_GRPC_JAVA_ARTIFACTS,
repositories = [
@@ -66,119 +77,120 @@ maven.install(
],
strict_visibility = True,
)
-
use_repo(maven, "maven")
+# Define a separate, dev-only extension import for Android deps.
+# This prevents downstream non-Android users from having to resolve
+# Google Maven (which is required for androidx.*) or rules_android transitively.
+grpc_android_maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven", dev_dependency = True)
+grpc_android_maven.install(
+ name = "grpc_android_maven",
+ # Set this explicitly since the default guess is incorrect under Bzlmod.
+ aar_import_bzl_label = "@rules_android//rules:rules.bzl",
+ artifacts = [
+ "androidx.annotation:annotation:1.6.0",
+ "androidx.annotation:annotation-jvm:1.6.0",
+ "androidx.core:core:1.13.1",
+ "androidx.lifecycle:lifecycle-common:2.6.2",
+ ],
+ repositories = [
+ "https://repo.maven.apache.org/maven2/",
+ "https://maven.google.com",
+ ],
+ strict_visibility = True,
+ # For Bazel 8+ compatibility.
+ use_starlark_android_rules = True,
+)
+use_repo(grpc_android_maven, "grpc_android_maven")
+
maven.override(
coordinates = "com.google.protobuf:protobuf-java",
target = "@com_google_protobuf//:protobuf_java",
)
-
maven.override(
coordinates = "com.google.protobuf:protobuf-java-util",
target = "@com_google_protobuf//:protobuf_java_util",
)
-
maven.override(
coordinates = "com.google.protobuf:protobuf-javalite",
target = "@com_google_protobuf//:protobuf_javalite",
)
-
maven.override(
coordinates = "io.grpc:grpc-alts",
target = "@io_grpc_grpc_java//alts",
)
-
maven.override(
coordinates = "io.grpc:grpc-api",
target = "@io_grpc_grpc_java//api",
)
-
maven.override(
coordinates = "io.grpc:grpc-auth",
target = "@io_grpc_grpc_java//auth",
)
-
maven.override(
coordinates = "io.grpc:grpc-census",
target = "@io_grpc_grpc_java//census",
)
-
maven.override(
coordinates = "io.grpc:grpc-context",
target = "@io_grpc_grpc_java//context",
)
-
maven.override(
coordinates = "io.grpc:grpc-core",
target = "@io_grpc_grpc_java//core:core_maven",
)
-
maven.override(
coordinates = "io.grpc:grpc-googleapis",
target = "@io_grpc_grpc_java//googleapis",
)
-
maven.override(
coordinates = "io.grpc:grpc-grpclb",
target = "@io_grpc_grpc_java//grpclb",
)
-
maven.override(
coordinates = "io.grpc:grpc-inprocess",
target = "@io_grpc_grpc_java//inprocess",
)
-
maven.override(
coordinates = "io.grpc:grpc-netty",
target = "@io_grpc_grpc_java//netty",
)
-
maven.override(
coordinates = "io.grpc:grpc-netty-shaded",
target = "@io_grpc_grpc_java//netty:shaded_maven",
)
-
maven.override(
coordinates = "io.grpc:grpc-okhttp",
target = "@io_grpc_grpc_java//okhttp",
)
-
maven.override(
coordinates = "io.grpc:grpc-protobuf",
target = "@io_grpc_grpc_java//protobuf",
)
-
maven.override(
coordinates = "io.grpc:grpc-protobuf-lite",
target = "@io_grpc_grpc_java//protobuf-lite",
)
-
maven.override(
coordinates = "io.grpc:grpc-rls",
target = "@io_grpc_grpc_java//rls",
)
-
maven.override(
coordinates = "io.grpc:grpc-services",
target = "@io_grpc_grpc_java//services:services_maven",
)
-
maven.override(
coordinates = "io.grpc:grpc-stub",
target = "@io_grpc_grpc_java//stub",
)
-
maven.override(
coordinates = "io.grpc:grpc-testing",
target = "@io_grpc_grpc_java//testing",
)
-
maven.override(
coordinates = "io.grpc:grpc-xds",
target = "@io_grpc_grpc_java//xds:xds_maven",
)
-
maven.override(
coordinates = "io.grpc:grpc-util",
target = "@io_grpc_grpc_java//util",
diff --git a/README.md b/README.md
index d696164ee32..f12fa0e1986 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ gRPC-Java - An RPC library and framework
Supported Platforms
-------------------
-gRPC-Java supports Java 8 and later. Android minSdkVersion 21 (Lollipop) and
+gRPC-Java supports Java 8 and later. Android minSdkVersion 23 (Marshmallow) and
later are supported with [Java 8 language desugaring][android-java-8].
TLS usage on Android typically requires Play Services Dynamic Security Provider.
@@ -44,8 +44,8 @@ For a guided tour, take a look at the [quick start
guide](https://grpc.io/docs/languages/java/quickstart) or the more explanatory [gRPC
basics](https://grpc.io/docs/languages/java/basics).
-The [examples](https://github.com/grpc/grpc-java/tree/v1.75.0/examples) and the
-[Android example](https://github.com/grpc/grpc-java/tree/v1.75.0/examples/android)
+The [examples](https://github.com/grpc/grpc-java/tree/v1.82.1/examples) and the
+[Android example](https://github.com/grpc/grpc-java/tree/v1.82.1/examples/android)
are standalone projects that showcase the usage of gRPC.
Download
@@ -56,34 +56,34 @@ Download [the JARs][]. Or for Maven with non-Android, add to your `pom.xml`:
io.grpcgrpc-netty-shaded
- 1.75.0
+ 1.82.1runtimeio.grpcgrpc-protobuf
- 1.75.0
+ 1.82.1io.grpcgrpc-stub
- 1.75.0
+ 1.82.1
```
Or for Gradle with non-Android, add to your dependencies:
```gradle
-runtimeOnly 'io.grpc:grpc-netty-shaded:1.75.0'
-implementation 'io.grpc:grpc-protobuf:1.75.0'
-implementation 'io.grpc:grpc-stub:1.75.0'
+runtimeOnly 'io.grpc:grpc-netty-shaded:1.82.1'
+implementation 'io.grpc:grpc-protobuf:1.82.1'
+implementation 'io.grpc:grpc-stub:1.82.1'
```
For Android client, use `grpc-okhttp` instead of `grpc-netty-shaded` and
`grpc-protobuf-lite` instead of `grpc-protobuf`:
```gradle
-implementation 'io.grpc:grpc-okhttp:1.75.0'
-implementation 'io.grpc:grpc-protobuf-lite:1.75.0'
-implementation 'io.grpc:grpc-stub:1.75.0'
+implementation 'io.grpc:grpc-okhttp:1.82.1'
+implementation 'io.grpc:grpc-protobuf-lite:1.82.1'
+implementation 'io.grpc:grpc-stub:1.82.1'
```
For [Bazel](https://bazel.build), you can either
@@ -91,7 +91,7 @@ For [Bazel](https://bazel.build), you can either
(with the GAVs from above), or use `@io_grpc_grpc_java//api` et al (see below).
[the JARs]:
-https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.75.0
+https://search.maven.org/search?q=g:io.grpc%20AND%20v:1.82.1
Development snapshots are available in [Sonatypes's snapshot
repository](https://central.sonatype.com/repository/maven-snapshots/).
@@ -121,9 +121,9 @@ For protobuf-based codegen integrated with the Maven build system, you can use
protobuf-maven-plugin0.6.1
- com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}
+ com.google.protobuf:protoc:3.25.8:exe:${os.detected.classifier}grpc-java
- io.grpc:protoc-gen-grpc-java:1.75.0:exe:${os.detected.classifier}
+ io.grpc:protoc-gen-grpc-java:1.82.1:exe:${os.detected.classifier}
@@ -149,11 +149,11 @@ plugins {
protobuf {
protoc {
- artifact = "com.google.protobuf:protoc:3.25.5"
+ artifact = "com.google.protobuf:protoc:3.25.8"
}
plugins {
grpc {
- artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0'
+ artifact = 'io.grpc:protoc-gen-grpc-java:1.82.1'
}
}
generateProtoTasks {
@@ -182,11 +182,11 @@ plugins {
protobuf {
protoc {
- artifact = "com.google.protobuf:protoc:3.25.5"
+ artifact = "com.google.protobuf:protoc:3.25.8"
}
plugins {
grpc {
- artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0'
+ artifact = 'io.grpc:protoc-gen-grpc-java:1.82.1'
}
}
generateProtoTasks {
diff --git a/RELEASING.md b/RELEASING.md
index c57829b8c25..14b8fa6f769 100644
--- a/RELEASING.md
+++ b/RELEASING.md
@@ -250,3 +250,7 @@ gRPC for things that will need a migration effort.
When happy with the dependency upgrades, update the versions in `MODULE.bazel`,
`repositories.bzl`, and the various `pom.xml` and `build.gradle` files in
`examples/`.
+
+Upgrade the `uses:` for actions in `.github/workflows` to newer versions. Make
+sure to see what changed in each new major version, but it is most often just
+requiring a newer Node.js version.
diff --git a/SECURITY.md b/SECURITY.md
index 1555882c3c6..e710ceaabe1 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -397,7 +397,10 @@ grpc-netty version | netty-handler version | netty-tcnative-boringssl-static ver
1.60.x-1.66.x | 4.1.100.Final | 2.0.61.Final
1.67.x-1.70.x | 4.1.110.Final | 2.0.65.Final
1.71.x-1.74.x | 4.1.110.Final | 2.0.70.Final
-1.75.x- | 4.1.124.Final | 2.0.72.Final
+1.75.x-1.76.x | 4.1.124.Final | 2.0.72.Final
+1.77.x-1.78.x | 4.1.127.Final | 2.0.74.Final
+1.79.x-1.80.x | 4.1.130.Final | 2.0.74.Final
+1.81.x- | 4.1.132.Final | 2.0.75.Final
_(grpc-netty-shaded avoids issues with keeping these versions in sync.)_
diff --git a/WORKSPACE b/WORKSPACE
index 227ef332757..1efdf2793a8 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,35 +1,37 @@
workspace(name = "io_grpc_grpc_java")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS", "grpc_java_repositories")
+
+grpc_java_repositories()
http_archive(
name = "rules_java",
- url = "https://github.com/bazelbuild/rules_java/releases/download/5.3.5/rules_java-5.3.5.tar.gz",
- sha256 = "c73336802d0b4882e40770666ad055212df4ea62cfa6edf9cb0f9d29828a0934",
+ sha256 = "47632cc506c858011853073449801d648e10483d4b50e080ec2549a4b2398960",
+ urls = [
+ "https://github.com/bazelbuild/rules_java/releases/download/8.15.2/rules_java-8.15.2.tar.gz",
+ ],
)
-http_archive(
- name = "rules_jvm_external",
- sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac",
- strip_prefix = "rules_jvm_external-5.3",
- url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz",
-)
+load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS", "protobuf_deps")
-load("@rules_jvm_external//:defs.bzl", "maven_install")
-load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_ARTIFACTS")
-load("//:repositories.bzl", "IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS")
-load("//:repositories.bzl", "grpc_java_repositories")
+protobuf_deps()
-grpc_java_repositories()
+load("@rules_java//java:rules_java_deps.bzl", "rules_java_dependencies")
+
+rules_java_dependencies()
+
+load("@bazel_features//:deps.bzl", "bazel_features_deps")
+
+bazel_features_deps()
load("@bazel_jar_jar//:jar_jar.bzl", "jar_jar_repositories")
jar_jar_repositories()
-load("@com_google_protobuf//:protobuf_deps.bzl", "PROTOBUF_MAVEN_ARTIFACTS")
-load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
+load("@rules_python//python:repositories.bzl", "py_repositories")
-protobuf_deps()
+py_repositories()
load("@com_google_googleapis//:repository_rules.bzl", "switched_rules_by_language")
@@ -37,6 +39,15 @@ switched_rules_by_language(
name = "com_google_googleapis_imports",
)
+http_archive(
+ name = "rules_jvm_external",
+ sha256 = "d31e369b854322ca5098ea12c69d7175ded971435e55c18dd9dd5f29cc5249ac",
+ strip_prefix = "rules_jvm_external-5.3",
+ url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/5.3/rules_jvm_external-5.3.tar.gz",
+)
+
+load("@rules_jvm_external//:defs.bzl", "maven_install")
+
maven_install(
artifacts = IO_GRPC_GRPC_JAVA_ARTIFACTS + PROTOBUF_MAVEN_ARTIFACTS,
override_targets = IO_GRPC_GRPC_JAVA_OVERRIDE_TARGETS,
diff --git a/alts/BUILD.bazel b/alts/BUILD.bazel
index 2d9d3508461..f02aa511ff5 100644
--- a/alts/BUILD.bazel
+++ b/alts/BUILD.bazel
@@ -1,6 +1,7 @@
-load("@rules_java//java:defs.bzl", "java_library", "java_proto_library")
+load("@com_google_protobuf//bazel:java_proto_library.bzl", "java_proto_library")
+load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library")
+load("@rules_java//java:defs.bzl", "java_library")
load("@rules_jvm_external//:defs.bzl", "artifact")
-load("@rules_proto//proto:defs.bzl", "proto_library")
load("//:java_grpc_library.bzl", "java_grpc_library")
java_library(
@@ -13,7 +14,6 @@ java_library(
":handshaker_java_proto",
"//api",
"//core:internal",
- "//grpclb",
"//netty",
"//stub",
"@com_google_protobuf//:protobuf_java",
@@ -22,7 +22,7 @@ java_library(
artifact("com.google.errorprone:error_prone_annotations"),
artifact("com.google.guava:guava"),
artifact("io.netty:netty-buffer"),
- artifact("io.netty:netty-codec"),
+ artifact("io.netty:netty-codec-base"),
artifact("io.netty:netty-common"),
artifact("io.netty:netty-handler"),
artifact("io.netty:netty-transport"),
diff --git a/alts/build.gradle b/alts/build.gradle
index fe2e27784fc..c206a37bcef 100644
--- a/alts/build.gradle
+++ b/alts/build.gradle
@@ -14,12 +14,10 @@ dependencies {
implementation project(':grpc-auth'),
project(':grpc-core'),
project(":grpc-context"), // Override google-auth dependency with our newer version
- project(':grpc-grpclb'),
project(':grpc-protobuf'),
project(':grpc-stub'),
libraries.protobuf.java,
libraries.conscrypt,
- libraries.guava.jre, // JRE required by protobuf-java-util from grpclb
libraries.google.auth.oauth2Http
def nettyDependency = implementation project(':grpc-netty')
diff --git a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java
index fa1d4f7fa1c..c914385a451 100644
--- a/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java
+++ b/alts/src/main/java/io/grpc/alts/HandshakerServiceChannel.java
@@ -25,7 +25,6 @@
import io.grpc.internal.SharedResourceHolder.Resource;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.channel.EventLoopGroup;
-import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;
import java.net.InetSocketAddress;
@@ -37,10 +36,29 @@
* application will have at most one connection to the handshaker service.
*/
final class HandshakerServiceChannel {
+ // Port 8080 is necessary for ALTS handshake.
+ private static final int ALTS_PORT = 8080;
+ private static final String DEFAULT_TARGET = "metadata.google.internal.:8080";
static final Resource SHARED_HANDSHAKER_CHANNEL =
- new ChannelResource("metadata.google.internal.:8080");
-
+ new ChannelResource(getHandshakerTarget(System.getenv("GCE_METADATA_HOST")));
+
+ /**
+ * Returns handshaker target. When GCE_METADATA_HOST is provided, it might contain port which we
+ * will discard and use ALTS_PORT instead.
+ */
+ static String getHandshakerTarget(String envValue) {
+ if (envValue == null || envValue.isEmpty()) {
+ return DEFAULT_TARGET;
+ }
+ String host = envValue;
+ int portIndex = host.lastIndexOf(':');
+ if (portIndex != -1) {
+ host = host.substring(0, portIndex); // Discard port if specified
+ }
+ return host + ":" + ALTS_PORT; // Utilize ALTS port in all cases
+ }
+
/** Returns a resource of handshaker service channel for testing only. */
static Resource getHandshakerChannelForTesting(String handshakerAddress) {
return new ChannelResource(handshakerAddress);
@@ -59,8 +77,10 @@ public ChannelResource(String target) {
@Override
public Channel create() {
/* Use its own event loop thread pool to avoid blocking. */
+ @SuppressWarnings("deprecation") // Wait a bit before migrating to the Netty 4.2 API
EventLoopGroup eventGroup =
- new NioEventLoopGroup(1, new DefaultThreadFactory("handshaker pool", true));
+ new io.netty.channel.nio.NioEventLoopGroup(
+ 1, new DefaultThreadFactory("handshaker pool", true));
NettyChannelBuilder channelBuilder =
NettyChannelBuilder.forTarget(target)
.channelType(NioSocketChannel.class, InetSocketAddress.class)
diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java b/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java
index e0343f83c51..9c51cf6a053 100644
--- a/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java
+++ b/alts/src/main/java/io/grpc/alts/internal/AltsProtocolNegotiator.java
@@ -30,7 +30,6 @@
import io.grpc.SecurityLevel;
import io.grpc.Status;
import io.grpc.alts.internal.RpcProtocolVersionsUtil.RpcVersionsCheckResult;
-import io.grpc.grpclb.GrpclbConstants;
import io.grpc.internal.ObjectPool;
import io.grpc.netty.GrpcHttp2ConnectionHandler;
import io.grpc.netty.InternalProtocolNegotiator;
@@ -299,9 +298,7 @@ public ChannelHandler newHandler(GrpcHttp2ConnectionHandler grpcHandler) {
isXdsDirectPath = isDirectPathCluster(
grpcHandler.getEagAttributes().get(clusterNameAttrKey));
}
- if (grpcHandler.getEagAttributes().get(GrpclbConstants.ATTR_LB_ADDR_AUTHORITY) != null
- || grpcHandler.getEagAttributes().get(GrpclbConstants.ATTR_LB_PROVIDED_BACKEND) != null
- || isXdsDirectPath) {
+ if (isXdsDirectPath) {
TsiHandshaker handshaker =
handshakerFactory.newHandshaker(grpcHandler.getAuthority(), negotiationLogger);
NettyTsiHandshaker nettyHandshaker = new NettyTsiHandshaker(handshaker);
diff --git a/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java b/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java
index 007db9e1eed..2d6c322c1b1 100644
--- a/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java
+++ b/alts/src/main/java/io/grpc/alts/internal/AltsTsiHandshaker.java
@@ -80,7 +80,7 @@ public boolean processBytesFromPeer(ByteBuffer bytes) throws GeneralSecurityExce
return true;
}
int remaining = bytes.remaining();
- // Call handshaker service to proceess the bytes.
+ // Call handshaker service to process the bytes.
if (outputFrame == null) {
checkState(!isClient, "Client handshaker should not process any frame at the beginning.");
outputFrame = handshaker.startServerHandshake(bytes);
diff --git a/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java b/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java
index a3937904cd7..221001157f1 100644
--- a/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java
+++ b/alts/src/test/java/io/grpc/alts/HandshakerServiceChannelTest.java
@@ -67,6 +67,24 @@ public void sharedChannel_authority() {
}
}
+ @Test
+ public void getHandshakerTarget_nullEnvVar() {
+ assertThat(HandshakerServiceChannel.getHandshakerTarget(null))
+ .isEqualTo("metadata.google.internal.:8080");
+ }
+
+ @Test
+ public void getHandshakerTarget_envVarWithPort() {
+ assertThat(HandshakerServiceChannel.getHandshakerTarget("169.254.169.254:80"))
+ .isEqualTo("169.254.169.254:8080");
+ }
+
+ @Test
+ public void getHandshakerTarget_envVarWithHostOnly() {
+ assertThat(HandshakerServiceChannel.getHandshakerTarget("169.254.169.254"))
+ .isEqualTo("169.254.169.254:8080");
+ }
+
@Test
public void resource_works() {
Channel channel = resource.create();
diff --git a/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java b/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java
index 24392af75fd..d47607ed90f 100644
--- a/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java
+++ b/alts/src/test/java/io/grpc/alts/internal/AltsProtocolNegotiatorTest.java
@@ -202,8 +202,11 @@ public void operationComplete(ChannelFuture future) throws Exception {
channel.flush();
// Capture the protected data written to the wire.
- assertEquals(1, channel.outboundMessages().size());
- ByteBuf protectedData = channel.readOutbound();
+ assertThat(channel.outboundMessages()).isNotEmpty();
+ ByteBuf protectedData = channel.alloc().buffer();
+ while (!channel.outboundMessages().isEmpty()) {
+ protectedData.writeBytes((ByteBuf) channel.readOutbound());
+ }
assertEquals(message.length(), writeCount.get());
// Read the protected message at the server and verify it matches the original message.
@@ -327,16 +330,18 @@ public void doNotFlushEmptyBuffer() throws Exception {
String message = "hello";
ByteBuf in = Unpooled.copiedBuffer(message, UTF_8);
- assertEquals(0, protector.flushes.get());
+ int flushes = protector.flushes.get();
Future> done = channel.write(in);
channel.flush();
+ flushes++;
done.get(5, TimeUnit.SECONDS);
- assertEquals(1, protector.flushes.get());
+ assertEquals(flushes, protector.flushes.get());
+ // Flush does not propagate
done = channel.write(Unpooled.EMPTY_BUFFER);
channel.flush();
done.get(5, TimeUnit.SECONDS);
- assertEquals(1, protector.flushes.get());
+ assertEquals(flushes, protector.flushes.get());
}
@Test
diff --git a/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java
index 9a520720beb..14c19e554ae 100644
--- a/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java
+++ b/alts/src/test/java/io/grpc/alts/internal/GoogleDefaultProtocolNegotiatorTest.java
@@ -29,7 +29,6 @@
import io.grpc.ChannelLogger;
import io.grpc.ChannelLogger.ChannelLogLevel;
import io.grpc.ManagedChannel;
-import io.grpc.grpclb.GrpclbConstants;
import io.grpc.inprocess.InProcessChannelBuilder;
import io.grpc.internal.ObjectPool;
import io.grpc.netty.GrpcHttp2ConnectionHandler;
@@ -95,13 +94,6 @@ public void tearDown() {
@Nullable
abstract Attributes.Key getClusterNameAttrKey();
- @Test
- public void altsHandler_lbProvidedBackend() {
- Attributes attrs =
- Attributes.newBuilder().set(GrpclbConstants.ATTR_LB_PROVIDED_BACKEND, true).build();
- subtest_altsHandler(attrs);
- }
-
@Test
public void tlsHandler_emptyAttributes() {
subtest_tlsHandler(Attributes.EMPTY);
diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle
index 17551465f05..b61d50a6763 100644
--- a/android-interop-testing/build.gradle
+++ b/android-interop-testing/build.gradle
@@ -33,15 +33,11 @@ android {
defaultConfig {
applicationId "io.grpc.android.integrationtest"
- // Held back to 20 as Gradle fails to build at the 21 level. This is
- // presumably a Gradle bug that can be revisited later.
- // Maybe this issue: https://github.com/gradle/gradle/issues/20778
- minSdkVersion 20
+ minSdkVersion 23
targetSdkVersion 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- multiDexEnabled = true
}
buildTypes {
debug { minifyEnabled false }
@@ -62,11 +58,11 @@ android {
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
- implementation 'androidx.multidex:multidex:2.0.0'
implementation libraries.androidx.annotation
implementation 'com.google.android.gms:play-services-base:18.0.1'
implementation project(':grpc-android'),
+ project(':grpc-api'),
project(':grpc-core'),
project(':grpc-census'),
project(':grpc-okhttp'),
@@ -76,6 +72,7 @@ dependencies {
libraries.junit,
libraries.truth,
libraries.androidx.test.rules,
+ libraries.androidx.test.core,
libraries.opencensus.contrib.grpc.metrics
implementation (project(':grpc-services')) {
@@ -83,8 +80,8 @@ dependencies {
exclude group: 'com.google.guava'
}
- androidTestImplementation 'androidx.test.ext:junit:1.1.3',
- 'androidx.test:runner:1.4.0'
+ androidTestImplementation libraries.androidx.test.ext.junit,
+ libraries.androidx.test.runner
}
// Checkstyle doesn't run automatically with android
@@ -115,6 +112,25 @@ tasks.withType(JavaCompile).configureEach {
"|")
}
+// Workaround error seen with Gradle 8.14.3 and AGP 7.4.1 when building:
+// ./gradlew clean :grpc-android-interop-testing:build -PskipAndroid=false \
+// -Pandroid.useAndroidX=true --no-build-cache
+//
+// Error message:
+//
+// Execution failed for task ':grpc-android-interop-testing:mergeExtDexDebug'.
+// > Could not resolve all files for configuration ':grpc-android-interop-testing:debugRuntimeClasspath'.
+// > Failed to transform opencensus-contrib-grpc-metrics-0.31.1.jar (io.opencensus:opencensus-contrib-grpc-metrics:0.31.1) to match attributes {artifactType=android-dex, asm-transformed-variant=NONE, dexing-enable-desugaring=true, dexing-enable-jacoco-instrumentation=false, dexing-is-debuggable=true, dexing-min-sdk=23, org.gradle.category=library, org.gradle.libraryelements=jar, org.gradle.status=release, org.gradle.usage=java-runtime}.
+// > Could not resolve all files for configuration ':grpc-android-interop-testing:debugRuntimeClasspath'.
+// > Failed to transform grpc-api-1.81.0-SNAPSHOT.jar (project :grpc-api) to match attributes {artifactType=android-classes-jar, org.gradle.category=library, org.gradle.dependency.bundling=external, org.gradle.jvm.version=8, org.gradle.libraryelements=jar, org.gradle.usage=java-runtime}.
+// > Execution failed for IdentityTransform: grpc-java/api/build/libs/grpc-api-1.81.0-SNAPSHOT.jar.
+// > File/directory does not exist: grpc-java/api/build/libs/grpc-api-1.81.0-SNAPSHOT.jar
+tasks.configureEach { task ->
+ if (task.name.equals("mergeExtDexDebug")) {
+ dependsOn(':grpc-api:jar')
+ }
+}
+
afterEvaluate {
// Hack to workaround "Task ':grpc-android-interop-testing:extractIncludeDebugProto' uses this
// output of task ':grpc-context:jar' without declaring an explicit or implicit dependency." The
diff --git a/android-interop-testing/src/androidTest/AndroidManifest.xml b/android-interop-testing/src/androidTest/AndroidManifest.xml
index b0507f10ab9..3cc0a29a85f 100644
--- a/android-interop-testing/src/androidTest/AndroidManifest.xml
+++ b/android-interop-testing/src/androidTest/AndroidManifest.xml
@@ -5,8 +5,7 @@
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="io.grpc.android.integrationtest" />
-
+
diff --git a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java
index f5e54da5d4e..5b98665ba29 100644
--- a/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java
+++ b/android-interop-testing/src/androidTest/java/io/grpc/android/integrationtest/UdsChannelInteropTest.java
@@ -19,9 +19,9 @@
import static org.junit.Assert.assertEquals;
import android.net.LocalSocketAddress.Namespace;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.rule.ActivityTestRule;
+import androidx.test.platform.app.InstrumentationRegistry;
import io.grpc.Grpc;
import io.grpc.InsecureServerCredentials;
import io.grpc.Server;
@@ -60,8 +60,8 @@ public class UdsChannelInteropTest {
// Ensures Looper is initialized for tests running on API level 15. Otherwise instantiating an
// AsyncTask throws an exception.
@Rule
- public ActivityTestRule activityRule =
- new ActivityTestRule(TesterActivity.class);
+ public ActivityScenarioRule activityRule =
+ new ActivityScenarioRule<>(TesterActivity.class);
@Before
public void setUp() throws IOException {
diff --git a/android-interop-testing/src/main/AndroidManifest.xml b/android-interop-testing/src/main/AndroidManifest.xml
index da7ccef5b1d..08c139e5880 100644
--- a/android-interop-testing/src/main/AndroidManifest.xml
+++ b/android-interop-testing/src/main/AndroidManifest.xml
@@ -14,8 +14,7 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
- android:theme="@style/Base.V7.Theme.AppCompat.Light"
- android:name="androidx.multidex.MultiDexApplication">
+ android:theme="@style/Base.V7.Theme.AppCompat.Light">
diff --git a/android/BUILD.bazel b/android/BUILD.bazel
new file mode 100644
index 00000000000..04b60f05eed
--- /dev/null
+++ b/android/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@rules_android//rules:rules.bzl", "android_library")
+load("@rules_jvm_external//:defs.bzl", "artifact")
+
+licenses(["notice"])
+
+android_library(
+ name = "android",
+ srcs = glob([
+ "src/main/java/**/*.java",
+ ]),
+ manifest = "src/main/AndroidManifest.xml",
+ custom_package = "io.grpc.android",
+ # TODO(jdcormie): Figure out how to expose this publicly without making
+ # existing non-android users configure rules_android and maven.google.com.
+ visibility = ["//:__subpackages__"],
+ deps = [
+ "//api",
+ "//core:internal",
+ artifact("com.google.code.findbugs:jsr305"),
+ artifact("com.google.errorprone:error_prone_annotations"),
+ artifact("com.google.guava:guava"),
+ ],
+)
diff --git a/android/build.gradle b/android/build.gradle
index d0ae3b9c886..e94bf03ff37 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -14,7 +14,7 @@ android {
}
compileSdkVersion 34
defaultConfig {
- minSdkVersion 22
+ minSdkVersion 23
targetSdkVersion 33
versionCode 1
versionName "1.0"
diff --git a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java
index 7d41301704c..6f03aa0ee5e 100644
--- a/android/src/main/java/io/grpc/android/UdsChannelBuilder.java
+++ b/android/src/main/java/io/grpc/android/UdsChannelBuilder.java
@@ -21,6 +21,7 @@
import io.grpc.ExperimentalApi;
import io.grpc.InsecureChannelCredentials;
import io.grpc.ManagedChannelBuilder;
+import io.grpc.internal.GrpcUtil;
import java.lang.reflect.InvocationTargetException;
import javax.annotation.Nullable;
import javax.net.SocketFactory;
@@ -81,7 +82,7 @@ public static ManagedChannelBuilder> forPath(String path, Namespace namespace)
OKHTTP_CHANNEL_BUILDER_CLASS
.getMethod("socketFactory", SocketFactory.class)
.invoke(builder, new UdsSocketFactory(path, namespace));
- return builder;
+ return builder.proxyDetector(GrpcUtil.NOOP_PROXY_DETECTOR);
} catch (IllegalAccessException e) {
throw new RuntimeException("Failed to create OkHttpChannelBuilder", e);
} catch (NoSuchMethodException e) {
diff --git a/api/BUILD.bazel b/api/BUILD.bazel
index 4c5e750b5e4..6de00d6272d 100644
--- a/api/BUILD.bazel
+++ b/api/BUILD.bazel
@@ -15,3 +15,21 @@ java_library(
artifact("com.google.guava:guava"),
],
)
+
+java_library(
+ name = "test_fixtures",
+ testonly = 1,
+ srcs = glob([
+ "src/testFixtures/java/io/grpc/**/*.java",
+ ]),
+ visibility = ["//xds:__pkg__"],
+ deps = [
+ "//core",
+ artifact("com.google.code.findbugs:jsr305"),
+ artifact("com.google.errorprone:error_prone_annotations"),
+ artifact("com.google.guava:guava"),
+ artifact("com.google.truth:truth"),
+ artifact("junit:junit"),
+ artifact("org.mockito:mockito-core"),
+ ],
+)
diff --git a/api/build.gradle b/api/build.gradle
index 415a17f61f8..745fa00b3f1 100644
--- a/api/build.gradle
+++ b/api/build.gradle
@@ -73,6 +73,7 @@ tasks.named("javadoc").configure {
exclude 'io/grpc/Internal?*.java'
exclude 'io/grpc/MetricRecorder.java'
exclude 'io/grpc/MetricSink.java'
+ exclude 'io/grpc/Uri.java'
}
tasks.named("sourcesJar").configure {
diff --git a/api/src/main/java/io/grpc/Channel.java b/api/src/main/java/io/grpc/Channel.java
index 60ff76ff082..e2787eb2f26 100644
--- a/api/src/main/java/io/grpc/Channel.java
+++ b/api/src/main/java/io/grpc/Channel.java
@@ -16,7 +16,6 @@
package io.grpc;
-import javax.annotation.concurrent.ThreadSafe;
/**
* A virtual connection to a conceptual endpoint, to perform RPCs. A channel is free to have zero or
@@ -29,8 +28,9 @@
* implementations using {@link ClientInterceptor}. It is expected that most application
* code will not use this class directly but rather work with stubs that have been bound to a
* Channel that was decorated during application initialization.
+ *
+ *
This class is thread-safe.
*/
-@ThreadSafe
public abstract class Channel {
/**
* Create a {@link ClientCall} to the remote operation specified by the given
diff --git a/api/src/main/java/io/grpc/ChannelLogger.java b/api/src/main/java/io/grpc/ChannelLogger.java
index ce654ec9d5b..2cdf4c84724 100644
--- a/api/src/main/java/io/grpc/ChannelLogger.java
+++ b/api/src/main/java/io/grpc/ChannelLogger.java
@@ -16,15 +16,15 @@
package io.grpc;
-import javax.annotation.concurrent.ThreadSafe;
/**
* A Channel-specific logger provided by GRPC library to {@link LoadBalancer} implementations.
* Information logged here goes to Channelz, and to the Java logger of this class
* as well.
+ *
+ *
This class is thread-safe.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/5029")
-@ThreadSafe
public abstract class ChannelLogger {
/**
* Log levels. See the table below for the mapping from the ChannelLogger levels to Channelz
diff --git a/api/src/main/java/io/grpc/ClientInterceptor.java b/api/src/main/java/io/grpc/ClientInterceptor.java
index c27c31c8474..d6c8cd7e6fb 100644
--- a/api/src/main/java/io/grpc/ClientInterceptor.java
+++ b/api/src/main/java/io/grpc/ClientInterceptor.java
@@ -16,7 +16,6 @@
package io.grpc;
-import javax.annotation.concurrent.ThreadSafe;
/**
* Interface for intercepting outgoing calls before they are dispatched by a {@link Channel}.
@@ -37,8 +36,10 @@
* without completing the previous ones first. Refer to the
* {@link io.grpc.ClientCall.Listener ClientCall.Listener} docs for more details regarding thread
* safety of the returned listener.
+ *
+ *
This is thread-safe and should be considered
+ * for the errorprone ThreadSafe annotation in the future.
*/
-@ThreadSafe
public interface ClientInterceptor {
/**
* Intercept {@link ClientCall} creation by the {@code next} {@link Channel}.
diff --git a/api/src/main/java/io/grpc/ClientStreamTracer.java b/api/src/main/java/io/grpc/ClientStreamTracer.java
index 42e1fdfebea..8e11e781e7c 100644
--- a/api/src/main/java/io/grpc/ClientStreamTracer.java
+++ b/api/src/main/java/io/grpc/ClientStreamTracer.java
@@ -19,13 +19,13 @@
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.MoreObjects;
-import javax.annotation.concurrent.ThreadSafe;
/**
* {@link StreamTracer} for the client-side.
+ *
+ *
This class is thread-safe.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
-@ThreadSafe
public abstract class ClientStreamTracer extends StreamTracer {
/**
* Indicates how long the call was delayed, in nanoseconds, due to waiting for name resolution
diff --git a/api/src/main/java/io/grpc/ConnectivityState.java b/api/src/main/java/io/grpc/ConnectivityState.java
index 677039b2517..a7407efb2e9 100644
--- a/api/src/main/java/io/grpc/ConnectivityState.java
+++ b/api/src/main/java/io/grpc/ConnectivityState.java
@@ -20,7 +20,7 @@
* The connectivity states.
*
* @see
- * more information
+ * more information
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/4359")
public enum ConnectivityState {
diff --git a/api/src/main/java/io/grpc/EquivalentAddressGroup.java b/api/src/main/java/io/grpc/EquivalentAddressGroup.java
index bf8a864902c..2dd52fe7f21 100644
--- a/api/src/main/java/io/grpc/EquivalentAddressGroup.java
+++ b/api/src/main/java/io/grpc/EquivalentAddressGroup.java
@@ -55,6 +55,21 @@ public final class EquivalentAddressGroup {
*/
public static final Attributes.Key ATTR_LOCALITY_NAME =
Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY");
+ /**
+ * The backend service associated with this EquivalentAddressGroup.
+ */
+ @Attr
+ static final Attributes.Key ATTR_BACKEND_SERVICE =
+ Attributes.Key.create("io.grpc.EquivalentAddressGroup.BACKEND_SERVICE");
+ /**
+ * Endpoint weight for load balancing purposes. While the type is Long, it must be a valid uint32.
+ * Must not be zero. The weight is proportional to the other endpoints; if an endpoint's weight is
+ * twice that of another endpoint, it is intended to receive twice the load.
+ */
+ @Attr
+ static final Attributes.Key ATTR_WEIGHT =
+ Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_WEIGHT");
+
private final List addrs;
private final Attributes attrs;
@@ -113,7 +128,9 @@ public Attributes getAttributes() {
@Override
public String toString() {
- // TODO(zpencer): Summarize return value if addr is very large
+ // EquivalentAddressGroup is intended to contain a small number of addresses for the same
+ // endpoint(e.g., IPv4/IPv6). Aggregating many groups into a single EquivalentAddressGroup
+ // is no longer done, so this no longer needs summarization.
return "[" + addrs + "/" + attrs + "]";
}
diff --git a/api/src/main/java/io/grpc/FeatureFlags.java b/api/src/main/java/io/grpc/FeatureFlags.java
new file mode 100644
index 00000000000..36fec1b9a59
--- /dev/null
+++ b/api/src/main/java/io/grpc/FeatureFlags.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+
+class FeatureFlags {
+ private static boolean enableRfc3986Uris = getFlag("GRPC_ENABLE_RFC3986_URIS", true);
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ @VisibleForTesting
+ static boolean setRfc3986UrisEnabled(boolean value) {
+ boolean prevValue = enableRfc3986Uris;
+ enableRfc3986Uris = value;
+ return prevValue;
+ }
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ static boolean getRfc3986UrisEnabled() {
+ return enableRfc3986Uris;
+ }
+
+ static boolean getFlag(String envVarName, boolean enableByDefault) {
+ String envVar = System.getenv(envVarName);
+ if (envVar == null) {
+ envVar = System.getProperty(envVarName);
+ }
+ if (envVar != null) {
+ envVar = envVar.trim();
+ }
+ if (enableByDefault) {
+ return Strings.isNullOrEmpty(envVar) || Boolean.parseBoolean(envVar);
+ } else {
+ return !Strings.isNullOrEmpty(envVar) && Boolean.parseBoolean(envVar);
+ }
+ }
+
+ private FeatureFlags() {}
+}
diff --git a/api/src/main/java/io/grpc/ForwardingServerBuilder.java b/api/src/main/java/io/grpc/ForwardingServerBuilder.java
index 9cef7cfa331..d1f183dd824 100644
--- a/api/src/main/java/io/grpc/ForwardingServerBuilder.java
+++ b/api/src/main/java/io/grpc/ForwardingServerBuilder.java
@@ -201,6 +201,12 @@ public Server build() {
return delegate().build();
}
+ @Override
+ public T addMetricSink(MetricSink metricSink) {
+ delegate().addMetricSink(metricSink);
+ return thisT();
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add("delegate", delegate()).toString();
diff --git a/api/src/main/java/io/grpc/Grpc.java b/api/src/main/java/io/grpc/Grpc.java
index baa9f5f0ab6..628f6fac399 100644
--- a/api/src/main/java/io/grpc/Grpc.java
+++ b/api/src/main/java/io/grpc/Grpc.java
@@ -56,6 +56,13 @@ private Grpc() {
public static final Attributes.Key TRANSPORT_ATTR_SSL_SESSION =
Attributes.Key.create("io.grpc.Grpc.TRANSPORT_ATTR_SSL_SESSION");
+ /**
+ * The value for the custom label of per-RPC metrics. Defaults to empty string when unset. Must
+ * not be set to {@code null}.
+ */
+ public static final CallOptions.Key CALL_OPTION_CUSTOM_LABEL =
+ CallOptions.Key.createWithDefault("io.grpc.Grpc.CALL_OPTION_CUSTOM_LABEL", "");
+
/**
* Annotation for transport attributes. It follows the annotation semantics defined
* by {@link Attributes}.
@@ -66,11 +73,10 @@ private Grpc() {
public @interface TransportAttr {}
/**
- * Creates a channel builder with a target string and credentials. The target can be either a
- * valid {@link NameResolver}-compliant URI, or an authority string.
+ * Creates a channel builder with a target string and credentials. The target can be either an RFC
+ * 3986 URI, or an authority string.
*
- *
A {@code NameResolver}-compliant URI is an absolute hierarchical URI as defined by {@link
- * java.net.URI}. Example URIs:
+ *
An authority string will be converted to a {@code NameResolver}-compliant URI, which has
- * the scheme from the name resolver with the highest priority (e.g. {@code "dns"}),
- * no authority, and the original authority string as its path after properly escaped.
- * We recommend libraries to specify the schema explicitly if it is known, since libraries cannot
- * know which NameResolver will be default during runtime.
+ *
An authority string will be converted to a URI having the scheme of the name resolver with
+ * the highest priority (e.g. {@code "dns"}), the empty string as the authority, and
+ * {@code target} as its absolute path. We recommend libraries specify {@code target} as a URI
+ * instead since they cannot know which NameResolver will be default at runtime.
* Example authority strings:
*
The URI form of {@code target} is preferred because it is less ambiguous. For example, the
+ * target string {@code foo:8080} is a valid authority string with host {@code foo} and port
+ * {@code 8080} but it is also a valid RFC 3986 URI with scheme {@code foo} and path {@code 8080}.
+ * gRPC prioritizes the URI form, which means {@code foo:8080} will be treated as a URI with
+ * scheme {@code foo}. Using {@code dns:///foo:8080} avoids this ambiguity.
*/
public static ManagedChannelBuilder> newChannelBuilder(
String target, ChannelCredentials creds) {
return ManagedChannelRegistry.getDefaultRegistry().newChannelBuilder(target, creds);
}
+ /**
+ * Creates a channel builder with a target string, credentials, and a specific
+ * name resolver registry.
+ *
+ *
The provided {@code nameResolverRegistry} is used to resolve the target address
+ * into physical addresses (e.g., DNS or custom schemes).
+ *
+ * @param target the target URI for the channel, such as {@code "localhost:8080"}
+ * or {@code "dns:///example.com"}
+ * @param creds the channel credentials to use for secure communication
+ * @param nameResolverRegistry the registry used to look up {@link NameResolver}
+ * providers for the target
+ * @return a {@link ManagedChannelBuilder} instance configured with the given parameters
+ * @throws IllegalArgumentException if no provider is available for the given target
+ * or credentials
+ * @since 1.83.0
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12694")
+ public static ManagedChannelBuilder> newChannelBuilder(
+ String target,
+ ChannelCredentials creds,
+ NameResolverRegistry nameResolverRegistry) {
+ return ManagedChannelRegistry.getDefaultRegistry().newChannelBuilder(
+ nameResolverRegistry,
+ target,
+ creds);
+ }
+
/**
* Creates a channel builder from a host, port, and credentials. The host and port are combined to
* form an authority string and then passed to {@link #newChannelBuilder(String,
diff --git a/api/src/main/java/io/grpc/HandlerRegistry.java b/api/src/main/java/io/grpc/HandlerRegistry.java
index 4aaf0114fb1..148573ada9a 100644
--- a/api/src/main/java/io/grpc/HandlerRegistry.java
+++ b/api/src/main/java/io/grpc/HandlerRegistry.java
@@ -19,12 +19,12 @@
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
/**
* Registry of services and their methods used by servers to dispatching incoming calls.
+ *
+ *
This class is thread-safe.
*/
-@ThreadSafe
public abstract class HandlerRegistry {
/**
diff --git a/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java b/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java
index d59c53db1d1..0df8dc452c1 100644
--- a/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java
+++ b/api/src/main/java/io/grpc/HttpConnectProxiedSocketAddress.java
@@ -23,6 +23,9 @@
import com.google.common.base.Objects;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
import javax.annotation.Nullable;
/**
@@ -33,6 +36,8 @@ public final class HttpConnectProxiedSocketAddress extends ProxiedSocketAddress
private final SocketAddress proxyAddress;
private final InetSocketAddress targetAddress;
+ @SuppressWarnings("serial")
+ private final Map headers;
@Nullable
private final String username;
@Nullable
@@ -41,6 +46,7 @@ public final class HttpConnectProxiedSocketAddress extends ProxiedSocketAddress
private HttpConnectProxiedSocketAddress(
SocketAddress proxyAddress,
InetSocketAddress targetAddress,
+ Map headers,
@Nullable String username,
@Nullable String password) {
checkNotNull(proxyAddress, "proxyAddress");
@@ -53,6 +59,7 @@ private HttpConnectProxiedSocketAddress(
}
this.proxyAddress = proxyAddress;
this.targetAddress = targetAddress;
+ this.headers = headers;
this.username = username;
this.password = password;
}
@@ -87,6 +94,14 @@ public InetSocketAddress getTargetAddress() {
return targetAddress;
}
+ /**
+ * Returns the custom HTTP headers to be sent during the HTTP CONNECT handshake.
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12479")
+ public Map getHeaders() {
+ return headers;
+ }
+
@Override
public boolean equals(Object o) {
if (!(o instanceof HttpConnectProxiedSocketAddress)) {
@@ -95,13 +110,14 @@ public boolean equals(Object o) {
HttpConnectProxiedSocketAddress that = (HttpConnectProxiedSocketAddress) o;
return Objects.equal(proxyAddress, that.proxyAddress)
&& Objects.equal(targetAddress, that.targetAddress)
+ && Objects.equal(headers, that.headers)
&& Objects.equal(username, that.username)
&& Objects.equal(password, that.password);
}
@Override
public int hashCode() {
- return Objects.hashCode(proxyAddress, targetAddress, username, password);
+ return Objects.hashCode(proxyAddress, targetAddress, username, password, headers);
}
@Override
@@ -109,6 +125,7 @@ public String toString() {
return MoreObjects.toStringHelper(this)
.add("proxyAddr", proxyAddress)
.add("targetAddr", targetAddress)
+ .add("headers", headers)
.add("username", username)
// Intentionally mask out password
.add("hasPassword", password != null)
@@ -129,6 +146,7 @@ public static final class Builder {
private SocketAddress proxyAddress;
private InetSocketAddress targetAddress;
+ private Map headers = Collections.emptyMap();
@Nullable
private String username;
@Nullable
@@ -153,6 +171,18 @@ public Builder setTargetAddress(InetSocketAddress targetAddress) {
return this;
}
+ /**
+ * Sets custom HTTP headers to be sent during the HTTP CONNECT handshake. This is an optional
+ * field. The headers will be sent in addition to any authentication headers (if username and
+ * password are set).
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12479")
+ public Builder setHeaders(Map headers) {
+ this.headers = Collections.unmodifiableMap(
+ new HashMap<>(checkNotNull(headers, "headers")));
+ return this;
+ }
+
/**
* Sets the username used to connect to the proxy. This is an optional field and can be {@code
* null}.
@@ -175,7 +205,8 @@ public Builder setPassword(@Nullable String password) {
* Creates an {@code HttpConnectProxiedSocketAddress}.
*/
public HttpConnectProxiedSocketAddress build() {
- return new HttpConnectProxiedSocketAddress(proxyAddress, targetAddress, username, password);
+ return new HttpConnectProxiedSocketAddress(
+ proxyAddress, targetAddress, headers, username, password);
}
}
}
diff --git a/api/src/main/java/io/grpc/InternalEquivalentAddressGroup.java b/api/src/main/java/io/grpc/InternalEquivalentAddressGroup.java
new file mode 100644
index 00000000000..cd171208af7
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalEquivalentAddressGroup.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+@Internal
+public final class InternalEquivalentAddressGroup {
+ private InternalEquivalentAddressGroup() {}
+
+ /**
+ * Endpoint weight for load balancing purposes. While the type is Long, it must be a valid uint32.
+ * Must not be zero. The weight is proportional to the other endpoints; if an endpoint's weight is
+ * twice that of another endpoint, it is intended to receive twice the load.
+ */
+ public static final Attributes.Key ATTR_WEIGHT = EquivalentAddressGroup.ATTR_WEIGHT;
+
+ /**
+ * The backend service associated with this EquivalentAddressGroup.
+ */
+ public static final Attributes.Key ATTR_BACKEND_SERVICE =
+ EquivalentAddressGroup.ATTR_BACKEND_SERVICE;
+}
diff --git a/api/src/main/java/io/grpc/InternalFeatureFlags.java b/api/src/main/java/io/grpc/InternalFeatureFlags.java
new file mode 100644
index 00000000000..a1e771a7571
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalFeatureFlags.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+import com.google.common.annotations.VisibleForTesting;
+
+/** Global variables that govern major changes to the behavior of more than one grpc module. */
+@Internal
+public class InternalFeatureFlags {
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ @VisibleForTesting
+ public static boolean setRfc3986UrisEnabled(boolean value) {
+ return FeatureFlags.setRfc3986UrisEnabled(value);
+ }
+
+ /** Whether to parse targets as RFC 3986 URIs (true), or use {@link java.net.URI} (false). */
+ public static boolean getRfc3986UrisEnabled() {
+ return FeatureFlags.getRfc3986UrisEnabled();
+ }
+
+ public static boolean getFlag(String envVarName, boolean enableByDefault) {
+ return FeatureFlags.getFlag(envVarName, enableByDefault);
+ }
+
+ private InternalFeatureFlags() {}
+}
diff --git a/api/src/main/java/io/grpc/InternalServiceProviders.java b/api/src/main/java/io/grpc/InternalServiceProviders.java
index c72e01db67a..debc786a82a 100644
--- a/api/src/main/java/io/grpc/InternalServiceProviders.java
+++ b/api/src/main/java/io/grpc/InternalServiceProviders.java
@@ -17,7 +17,9 @@
package io.grpc;
import com.google.common.annotations.VisibleForTesting;
+import java.util.Iterator;
import java.util.List;
+import java.util.ServiceLoader;
@Internal
public final class InternalServiceProviders {
@@ -27,12 +29,17 @@ private InternalServiceProviders() {
/**
* Accessor for method.
*/
- public static T load(
+ @Deprecated
+ public static List loadAll(
Class klass,
- Iterable> hardcoded,
+ Iterable> hardCodedClasses,
ClassLoader classLoader,
PriorityAccessor priorityAccessor) {
- return ServiceProviders.load(klass, hardcoded, classLoader, priorityAccessor);
+ return loadAll(
+ klass,
+ ServiceLoader.load(klass, classLoader).iterator(),
+ () -> hardCodedClasses,
+ priorityAccessor);
}
/**
@@ -40,10 +47,10 @@ public static T load(
*/
public static List loadAll(
Class klass,
- Iterable> hardCodedClasses,
- ClassLoader classLoader,
+ Iterator serviceLoader,
+ Supplier>> hardCodedClasses,
PriorityAccessor priorityAccessor) {
- return ServiceProviders.loadAll(klass, hardCodedClasses, classLoader, priorityAccessor);
+ return ServiceProviders.loadAll(klass, serviceLoader, hardCodedClasses::get, priorityAccessor);
}
/**
@@ -71,4 +78,8 @@ public static boolean isAndroid(ClassLoader cl) {
}
public interface PriorityAccessor extends ServiceProviders.PriorityAccessor {}
+
+ public interface Supplier {
+ T get();
+ }
}
diff --git a/api/src/main/java/io/grpc/InternalTcpMetrics.java b/api/src/main/java/io/grpc/InternalTcpMetrics.java
new file mode 100644
index 00000000000..3dd89b6f76c
--- /dev/null
+++ b/api/src/main/java/io/grpc/InternalTcpMetrics.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * TCP Metrics defined to be shared across transport implementations.
+ * These metrics and their definitions are specified in
+ * gRFC
+ * A80.
+ */
+@Internal
+public final class InternalTcpMetrics {
+
+ private InternalTcpMetrics() {
+ }
+
+ private static final List OPTIONAL_LABELS = Arrays.asList(
+ "network.local.address",
+ "network.local.port",
+ "network.peer.address",
+ "network.peer.port");
+
+ public static final DoubleHistogramMetricInstrument MIN_RTT_INSTRUMENT =
+ MetricInstrumentRegistry.getDefaultRegistry()
+ .registerDoubleHistogram(
+ "grpc.tcp.min_rtt",
+ "Minimum round-trip time of a TCP connection",
+ "s",
+ Collections.emptyList(),
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongCounterMetricInstrument CONNECTIONS_CREATED_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongCounter(
+ "grpc.tcp.connections_created",
+ "The total number of TCP connections established.",
+ "{connection}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongUpDownCounterMetricInstrument CONNECTION_COUNT_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongUpDownCounter(
+ "grpc.tcp.connection_count",
+ "The current number of active TCP connections.",
+ "{connection}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongCounterMetricInstrument PACKETS_RETRANSMITTED_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongCounter(
+ "grpc.tcp.packets_retransmitted",
+ "The total number of packets retransmitted for all TCP connections.",
+ "{packet}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+ public static final LongCounterMetricInstrument RECURRING_RETRANSMITS_INSTRUMENT =
+ MetricInstrumentRegistry
+ .getDefaultRegistry()
+ .registerLongCounter(
+ "grpc.tcp.recurring_retransmits",
+ "The total number of times the retransmit timer "
+ + "popped for all TCP connections.",
+ "{timeout}",
+ Collections.emptyList(),
+ OPTIONAL_LABELS,
+ false);
+
+}
diff --git a/api/src/main/java/io/grpc/LoadBalancer.java b/api/src/main/java/io/grpc/LoadBalancer.java
index adc43b19841..5dd44a492ee 100644
--- a/api/src/main/java/io/grpc/LoadBalancer.java
+++ b/api/src/main/java/io/grpc/LoadBalancer.java
@@ -32,7 +32,6 @@
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;
-import javax.annotation.concurrent.ThreadSafe;
/**
* A pluggable component that receives resolved addresses from {@link NameResolver} and provides the
@@ -64,7 +63,7 @@
* allows implementations to schedule tasks to be run in the same Synchronization Context, with or
* without a delay, thus those tasks don't need to worry about synchronizing with the balancer
* methods.
- *
+ *
*
However, the actual running thread may be the network thread, thus the following rules must be
* followed to prevent blocking or even dead-locking in a network:
*
@@ -156,15 +155,16 @@ public String toString() {
private int recursionCount;
/**
- * Handles newly resolved server groups and metadata attributes from name resolution system.
- * {@code servers} contained in {@link EquivalentAddressGroup} should be considered equivalent
- * but may be flattened into a single list if needed.
- *
- *
Implementations should not modify the given {@code servers}.
+ * Handles newly resolved addresses and metadata attributes from name resolution system.
+ * Addresses in {@link EquivalentAddressGroup} should be considered equivalent but may be
+ * flattened into a single list if needed.
*
* @param resolvedAddresses the resolved server addresses, attributes, and config.
* @since 1.21.0
+ *
+ * @deprecated Use instead {@link #acceptResolvedAddresses(ResolvedAddresses)}
*/
+ @Deprecated
public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
if (recursionCount++ == 0) {
// Note that the information about the addresses actually being accepted will be lost
@@ -179,12 +179,10 @@ public void handleResolvedAddresses(ResolvedAddresses resolvedAddresses) {
* EquivalentAddressGroup} addresses should be considered equivalent but may be flattened into a
* single list if needed.
*
- *
Implementations can choose to reject the given addresses by returning {@code false}.
- *
- *
Implementations should not modify the given {@code addresses}.
+ * @param resolvedAddresses the resolved server addresses, attributes, and config
+ * @return {@code Status.OK} if the resolved addresses were accepted, otherwise an error to report
+ * to the name resolver
*
- * @param resolvedAddresses the resolved server addresses, attributes, and config.
- * @return {@code true} if the resolved addresses were accepted. {@code false} if rejected.
* @since 1.49.0
*/
public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) {
@@ -339,8 +337,8 @@ public ResolvedAddresses build() {
public String toString() {
return MoreObjects.toStringHelper(this)
.add("addresses", addresses)
- .add("attributes", attributes)
.add("loadBalancingPolicyConfig", loadBalancingPolicyConfig)
+ .add("attributes", attributes)
.toString();
}
@@ -418,7 +416,16 @@ public void handleSubchannelState(
*
*
This method should always return a constant value. It's not specified when this will be
* called.
+ *
+ *
Note that this method is only called when implementing {@code handleResolvedAddresses()}
+ * instead of {@code acceptResolvedAddresses()}.
+ *
+ * @deprecated Instead of overwriting this and {@code handleResolvedAddresses()}, only
+ * overwrite {@code acceptResolvedAddresses()} which indicates if the addresses provided
+ * by the name resolver are acceptable with the {@code boolean} return value.
*/
+ @Deprecated
+ @SuppressWarnings("InlineMeSuggester")
public boolean canHandleEmptyAddressListFromNameResolution() {
return false;
}
@@ -442,7 +449,6 @@ public void requestConnection() {}
*
* @since 1.2.0
*/
- @ThreadSafe
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771")
public abstract static class SubchannelPicker {
/**
@@ -632,6 +638,8 @@ private PickResult(
* stream is created at all in some cases.
* @since 1.3.0
*/
+ // TODO(shivaspeaks): Need to deprecate old APIs and create new ones,
+ // per https://github.com/grpc/grpc-java/issues/12662.
public static PickResult withSubchannel(
Subchannel subchannel, @Nullable ClientStreamTracer.Factory streamTracerFactory) {
return new PickResult(
@@ -661,6 +669,28 @@ public static PickResult withSubchannel(Subchannel subchannel) {
return withSubchannel(subchannel, null);
}
+ /**
+ * Creates a new {@code PickResult} with the given {@code subchannel},
+ * but retains all other properties from this {@code PickResult}.
+ *
+ * @since 1.80.0
+ */
+ public PickResult copyWithSubchannel(Subchannel subchannel) {
+ return new PickResult(checkNotNull(subchannel, "subchannel"), streamTracerFactory,
+ status, drop, authorityOverride);
+ }
+
+ /**
+ * Creates a new {@code PickResult} with the given {@code streamTracerFactory},
+ * but retains all other properties from this {@code PickResult}.
+ *
+ * @since 1.80.0
+ */
+ public PickResult copyWithStreamTracerFactory(
+ @Nullable ClientStreamTracer.Factory streamTracerFactory) {
+ return new PickResult(subchannel, streamTracerFactory, status, drop, authorityOverride);
+ }
+
/**
* A decision to report a connectivity error to the RPC. If the RPC is {@link
* CallOptions#withWaitForReady wait-for-ready}, it will stay buffered. Otherwise, it will fail
@@ -998,9 +1028,10 @@ public String toString() {
/**
* Provides essentials for LoadBalancer implementations.
*
+ *
This class is thread-safe.
+ *
* @since 1.2.0
*/
- @ThreadSafe
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771")
public abstract static class Helper {
/**
@@ -1300,7 +1331,7 @@ public MetricRecorder getMetricRecorder() {
}
/**
- * A logical connection to a server, or a group of equivalent servers represented by an {@link
+ * A logical connection to a server, or a group of equivalent servers represented by an {@link
* EquivalentAddressGroup}.
*
*
It maintains at most one physical connection (aka transport) for sending new RPCs, while
@@ -1519,9 +1550,10 @@ public interface SubchannelStateListener {
/**
* Factory to create {@link LoadBalancer} instance.
*
+ *
This class is thread-safe.
+ *
* @since 1.2.0
*/
- @ThreadSafe
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1771")
public abstract static class Factory {
/**
diff --git a/api/src/main/java/io/grpc/LoadBalancerProvider.java b/api/src/main/java/io/grpc/LoadBalancerProvider.java
index bb4c574211e..7dc30d6baaf 100644
--- a/api/src/main/java/io/grpc/LoadBalancerProvider.java
+++ b/api/src/main/java/io/grpc/LoadBalancerProvider.java
@@ -81,7 +81,7 @@ public abstract class LoadBalancerProvider extends LoadBalancer.Factory {
* @return a tuple of the fully parsed and validated balancer configuration, else the Status.
* @since 1.20.0
* @see
- * A24-lb-policy-config.md
+ * A24-lb-policy-config.md
*/
public ConfigOrError parseLoadBalancingPolicyConfig(Map rawLoadBalancingPolicyConfig) {
return UNKNOWN_CONFIG;
diff --git a/api/src/main/java/io/grpc/LoadBalancerRegistry.java b/api/src/main/java/io/grpc/LoadBalancerRegistry.java
index f6b69f978b8..a8fbc102f5f 100644
--- a/api/src/main/java/io/grpc/LoadBalancerRegistry.java
+++ b/api/src/main/java/io/grpc/LoadBalancerRegistry.java
@@ -26,6 +26,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -42,7 +43,6 @@
public final class LoadBalancerRegistry {
private static final Logger logger = Logger.getLogger(LoadBalancerRegistry.class.getName());
private static LoadBalancerRegistry instance;
- private static final Iterable> HARDCODED_CLASSES = getHardCodedClasses();
private final LinkedHashSet allProviders =
new LinkedHashSet<>();
@@ -101,8 +101,10 @@ public static synchronized LoadBalancerRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
LoadBalancerProvider.class,
- HARDCODED_CLASSES,
- LoadBalancerProvider.class.getClassLoader(),
+ ServiceLoader
+ .load(LoadBalancerProvider.class, LoadBalancerProvider.class.getClassLoader())
+ .iterator(),
+ LoadBalancerRegistry::getHardCodedClasses,
new LoadBalancerPriorityAccessor());
instance = new LoadBalancerRegistry();
for (LoadBalancerProvider provider : providerList) {
diff --git a/api/src/main/java/io/grpc/ManagedChannel.java b/api/src/main/java/io/grpc/ManagedChannel.java
index 7875fdb57f2..2b1d89946bf 100644
--- a/api/src/main/java/io/grpc/ManagedChannel.java
+++ b/api/src/main/java/io/grpc/ManagedChannel.java
@@ -17,12 +17,12 @@
package io.grpc;
import java.util.concurrent.TimeUnit;
-import javax.annotation.concurrent.ThreadSafe;
/**
* A {@link Channel} that provides lifecycle management.
+ *
+ *
This class is thread-safe.
*/
-@ThreadSafe
public abstract class ManagedChannel extends Channel {
/**
* Initiates an orderly shutdown in which preexisting calls continue but new calls are immediately
diff --git a/api/src/main/java/io/grpc/ManagedChannelBuilder.java b/api/src/main/java/io/grpc/ManagedChannelBuilder.java
index df867d09ba1..7681c8f7b62 100644
--- a/api/src/main/java/io/grpc/ManagedChannelBuilder.java
+++ b/api/src/main/java/io/grpc/ManagedChannelBuilder.java
@@ -45,11 +45,10 @@ public static ManagedChannelBuilder> forAddress(String name, int port) {
}
/**
- * Creates a channel with a target string, which can be either a valid {@link
- * NameResolver}-compliant URI, or an authority string.
+ * Creates a channel with a target string, which can be either an RFC 3986 URI, or an authority
+ * string.
*
- *
A {@code NameResolver}-compliant URI is an absolute hierarchical URI as defined by {@link
- * java.net.URI}. Example URIs:
+ *
Example URIs:
*
*
{@code "dns:///foo.googleapis.com:8080"}
*
{@code "dns:///foo.googleapis.com"}
@@ -57,13 +56,13 @@ public static ManagedChannelBuilder> forAddress(String name, int port) {
*
An authority string will be converted to a {@code NameResolver}-compliant URI, which has
- * the scheme from the name resolver with the highest priority (e.g. {@code "dns"}),
- * no authority, and the original authority string as its path after properly escaped.
- * We recommend libraries to specify the schema explicitly if it is known, since libraries cannot
- * know which NameResolver will be default during runtime.
+ *
An authority string will be converted to a URI having the scheme of the name resolver with
+ * the highest priority (e.g. {@code "dns"}), the empty string as the authority, and
+ * {@code target} as its absolute path. We recommend libraries specify {@code target} as a URI
+ * instead since they cannot know which NameResolver will be default at runtime.
* Example authority strings:
*
*
{@code "localhost"}
@@ -75,6 +74,12 @@ public static ManagedChannelBuilder> forAddress(String name, int port) {
*
The URI form of {@code target} is preferred because it is less ambiguous. For example, the
+ * target string {@code foo:8080} is a valid authority string with host {@code foo} and port
+ * {@code 8080} but it is also a valid RFC 3986 URI with scheme {@code foo} and path {@code 8080}.
+ * gRPC prioritizes the URI form, which means {@code foo:8080} will be treated as a URI with
+ * scheme {@code foo}. Using {@code dns:///foo:8080} avoids this ambiguity.
+ *
*
Note that there is an open JDK bug on {@link java.net.URI} class parsing an ipv6 scope ID:
* bugs.openjdk.org/browse/JDK-8199396. This method is exposed to this bug. If you experience an
* issue, a work-around is to convert the scope ID to its numeric form (e.g. by using
@@ -374,9 +379,17 @@ public T maxInboundMetadataSize(int bytes) {
* notice when they are causing excessive load. Clients are strongly encouraged to use only as
* small of a value as necessary.
*
+ *
When the channel implementation supports TCP_USER_TIMEOUT, enabling keepalive will also
+ * enable TCP_USER_TIMEOUT for the connection. This requires all sent packets to receive
+ * a TCP acknowledgement before the keepalive timeout. The keepalive time is not used for
+ * TCP_USER_TIMEOUT, except as a signal to enable the feature. grpc-netty supports
+ * TCP_USER_TIMEOUT on Linux platforms supported by netty-transport-native-epoll.
+ *
* @throws UnsupportedOperationException if unsupported
* @see gRFC A8
* Client-side Keepalive
+ * @see gRFC A18
+ * TCP User Timeout
* @since 1.7.0
*/
public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
@@ -393,6 +406,8 @@ public T keepAliveTime(long keepAliveTime, TimeUnit timeUnit) {
* @throws UnsupportedOperationException if unsupported
* @see gRFC A8
* Client-side Keepalive
+ * @see gRFC A18
+ * TCP User Timeout
* @since 1.7.0
*/
public T keepAliveTimeout(long keepAliveTimeout, TimeUnit timeUnit) {
diff --git a/api/src/main/java/io/grpc/ManagedChannelProvider.java b/api/src/main/java/io/grpc/ManagedChannelProvider.java
index 42941dfc809..8a61c097ed8 100644
--- a/api/src/main/java/io/grpc/ManagedChannelProvider.java
+++ b/api/src/main/java/io/grpc/ManagedChannelProvider.java
@@ -81,6 +81,31 @@ protected NewChannelBuilderResult newChannelBuilder(String target, ChannelCreden
return NewChannelBuilderResult.error("ChannelCredentials are unsupported");
}
+ /**
+ * Creates a channel builder using the provided target, credentials, and resolution
+ * components.
+ *
+ *
This method allows for fine-grained control over name resolution by providing
+ * both a {@link NameResolverRegistry} and a specific {@link NameResolverProvider}.
+ * This returns a {@link NewChannelBuilderResult},
+ * which may contain an error string if the provided credentials or target are
+ * not supported by this provider.
+ *
+ * @param target the target URI for the channel
+ * @param creds the channel credentials to use
+ * @param nameResolverRegistry the registry used for looking up name resolvers
+ * @param nameResolverProvider a specific provider to use, or {@code null} to
+ * search the registry
+ * @return a {@link NewChannelBuilderResult} containing either the builder or an
+ * error description
+ * @since 1.83.0
+ */
+ protected NewChannelBuilderResult newChannelBuilder(String target, ChannelCredentials creds,
+ NameResolverRegistry nameResolverRegistry,
+ NameResolverProvider nameResolverProvider) {
+ return newChannelBuilder(target, creds);
+ }
+
/**
* Returns the {@link SocketAddress} types this ManagedChannelProvider supports.
*/
diff --git a/api/src/main/java/io/grpc/ManagedChannelRegistry.java b/api/src/main/java/io/grpc/ManagedChannelRegistry.java
index aed5eca9abf..c70b6812651 100644
--- a/api/src/main/java/io/grpc/ManagedChannelRegistry.java
+++ b/api/src/main/java/io/grpc/ManagedChannelRegistry.java
@@ -29,6 +29,7 @@
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
@@ -100,8 +101,10 @@ public static synchronized ManagedChannelRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
ManagedChannelProvider.class,
- getHardCodedClasses(),
- ManagedChannelProvider.class.getClassLoader(),
+ ServiceLoader
+ .load(ManagedChannelProvider.class, ManagedChannelProvider.class.getClassLoader())
+ .iterator(),
+ ManagedChannelRegistry::getHardCodedClasses,
new ManagedChannelPriorityAccessor());
instance = new ManagedChannelRegistry();
for (ManagedChannelProvider provider : providerList) {
@@ -155,13 +158,15 @@ ManagedChannelBuilder> newChannelBuilder(String target, ChannelCredentials cre
return newChannelBuilder(NameResolverRegistry.getDefaultRegistry(), target, creds);
}
- @VisibleForTesting
ManagedChannelBuilder> newChannelBuilder(NameResolverRegistry nameResolverRegistry,
String target, ChannelCredentials creds) {
NameResolverProvider nameResolverProvider = null;
try {
- URI uri = new URI(target);
- nameResolverProvider = nameResolverRegistry.getProviderForScheme(uri.getScheme());
+ String scheme =
+ FeatureFlags.getRfc3986UrisEnabled()
+ ? Uri.parse(target).getScheme()
+ : new URI(target).getScheme();
+ nameResolverProvider = nameResolverRegistry.getProviderForScheme(scheme);
} catch (URISyntaxException ignore) {
// bad URI found, just ignore and continue
}
@@ -192,7 +197,7 @@ ManagedChannelBuilder> newChannelBuilder(NameResolverRegistry nameResolverRegi
continue;
}
ManagedChannelProvider.NewChannelBuilderResult result
- = provider.newChannelBuilder(target, creds);
+ = provider.newChannelBuilder(target, creds, nameResolverRegistry, nameResolverProvider);
if (result.getChannelBuilder() != null) {
return result.getChannelBuilder();
}
diff --git a/api/src/main/java/io/grpc/NameResolver.java b/api/src/main/java/io/grpc/NameResolver.java
index 2ac4ecae69e..f52a38ba809 100644
--- a/api/src/main/java/io/grpc/NameResolver.java
+++ b/api/src/main/java/io/grpc/NameResolver.java
@@ -35,7 +35,6 @@
import java.util.concurrent.ScheduledExecutorService;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
-import javax.annotation.concurrent.ThreadSafe;
/**
* A pluggable component that resolves a target {@link URI} and return addresses to the caller.
@@ -78,7 +77,7 @@ public abstract class NameResolver {
* Starts the resolution. The method is not supposed to throw any exceptions. That might cause the
* Channel that the name resolver is serving to crash. Errors should be propagated
* through {@link Listener#onError}.
- *
+ *
*
An instance may not be started more than once, by any overload of this method, even after
* an intervening call to {@link #shutdown}.
*
@@ -97,8 +96,14 @@ public void onError(Status error) {
@Override
public void onResult(ResolutionResult resolutionResult) {
- listener.onAddresses(resolutionResult.getAddressesOrError().getValue(),
- resolutionResult.getAttributes());
+ StatusOr> addressesOrError =
+ resolutionResult.getAddressesOrError();
+ if (addressesOrError.hasValue()) {
+ listener.onAddresses(addressesOrError.getValue(),
+ resolutionResult.getAttributes());
+ } else {
+ listener.onError(addressesOrError.getStatus());
+ }
}
});
}
@@ -108,7 +113,7 @@ public void onResult(ResolutionResult resolutionResult) {
* Starts the resolution. The method is not supposed to throw any exceptions. That might cause the
* Channel that the name resolver is serving to crash. Errors should be propagated
* through {@link Listener2#onError}.
- *
+ *
*
An instance may not be started more than once, by any overload of this method, even after
* an intervening call to {@link #shutdown}.
*
@@ -152,6 +157,10 @@ public abstract static class Factory {
* cannot be resolved by this factory. The decision should be solely based on the scheme of the
* URI.
*
+ *
This method will eventually be deprecated and removed as part of a migration from {@code
+ * java.net.URI} to {@code io.grpc.Uri}. Implementations will override {@link
+ * #newNameResolver(Uri, Args)} instead.
+ *
* @param targetUri the target URI to be resolved, whose scheme must not be {@code null}
* @param args other information that may be useful
*
@@ -159,6 +168,37 @@ public abstract static class Factory {
*/
public abstract NameResolver newNameResolver(URI targetUri, final Args args);
+ /**
+ * Creates a {@link NameResolver} for the given target URI.
+ *
+ *
Implementations return {@code null} if 'targetUri' cannot be resolved by this factory. The
+ * decision should be solely based on the target's scheme.
+ *
+ *
All {@link NameResolver.Factory} implementations should override this method, as it will
+ * eventually replace {@link #newNameResolver(URI, Args)}. For backwards compatibility, this
+ * default implementation delegates to {@link #newNameResolver(URI, Args)} if 'targetUri' can be
+ * converted to a java.net.URI.
+ *
+ *
NB: Conversion is not always possible, for example {@code scheme:#frag} is a valid {@link
+ * Uri} but not a valid {@link URI} because its path is empty. The default implementation throws
+ * IllegalArgumentException in these cases.
+ *
+ * @param targetUri the target URI to be resolved
+ * @param args other information that may be useful
+ * @throws IllegalArgumentException if targetUri does not have the expected form
+ * @since 1.79
+ */
+ public NameResolver newNameResolver(Uri targetUri, final Args args) {
+ // Not every io.grpc.Uri can be converted but in the ordinary ManagedChannel creation flow,
+ // any IllegalArgumentException thrown here would have happened anyway, just earlier. That's
+ // because parse/toString is transparent so java.net.URI#create here sees the original target
+ // string just like it did before the io.grpc.Uri migration.
+ //
+ // Throwing IAE shouldn't surprise non-framework callers either. After all, many existing
+ // Factory impls are picky about targetUri and throw IAE when it doesn't look how they expect.
+ return newNameResolver(URI.create(targetUri.toString()), args);
+ }
+
/**
* Returns the default scheme, which will be used to construct a URI when {@link
* ManagedChannelBuilder#forTarget(String)} is given an authority string instead of a compliant
@@ -174,10 +214,11 @@ public abstract static class Factory {
*
*
All methods are expected to return quickly.
*
+ *
This interface is thread-safe.
+ *
* @since 1.0.0
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
- @ThreadSafe
public interface Listener {
/**
* Handles updates on resolved addresses and attributes.
@@ -314,7 +355,7 @@ public static final class Args {
@Nullable private final ChannelLogger channelLogger;
@Nullable private final Executor executor;
@Nullable private final String overrideAuthority;
- @Nullable private final MetricRecorder metricRecorder;
+ private final MetricRecorder metricRecorder;
@Nullable private final NameResolverRegistry nameResolverRegistry;
@Nullable private final IdentityHashMap, Object> customArgs;
@@ -328,7 +369,8 @@ private Args(Builder builder) {
this.channelLogger = builder.channelLogger;
this.executor = builder.executor;
this.overrideAuthority = builder.overrideAuthority;
- this.metricRecorder = builder.metricRecorder;
+ this.metricRecorder = builder.metricRecorder != null ? builder.metricRecorder
+ : new MetricRecorder() {};
this.nameResolverRegistry = builder.nameResolverRegistry;
this.customArgs = cloneCustomArgs(builder.customArgs);
}
@@ -456,7 +498,6 @@ public String getOverrideAuthority() {
/**
* Returns the {@link MetricRecorder} that the channel uses to record metrics.
*/
- @Nullable
public MetricRecorder getMetricRecorder() {
return metricRecorder;
}
@@ -639,7 +680,7 @@ public Builder setArg(Key key, T value) {
* See {@link Args#getMetricRecorder()}. This is an optional field.
*/
public Builder setMetricRecorder(MetricRecorder metricRecorder) {
- this.metricRecorder = metricRecorder;
+ this.metricRecorder = checkNotNull(metricRecorder, "metricRecorder");
return this;
}
diff --git a/api/src/main/java/io/grpc/NameResolverProvider.java b/api/src/main/java/io/grpc/NameResolverProvider.java
index 70e22e366d5..220fa4edc10 100644
--- a/api/src/main/java/io/grpc/NameResolverProvider.java
+++ b/api/src/main/java/io/grpc/NameResolverProvider.java
@@ -65,7 +65,7 @@ public abstract class NameResolverProvider extends NameResolver.Factory {
*
* @since 1.40.0
* */
- protected String getScheme() {
+ public String getScheme() {
return getDefaultScheme();
}
diff --git a/api/src/main/java/io/grpc/NameResolverRegistry.java b/api/src/main/java/io/grpc/NameResolverRegistry.java
index 26eb5552b9b..c5e9f7467ab 100644
--- a/api/src/main/java/io/grpc/NameResolverRegistry.java
+++ b/api/src/main/java/io/grpc/NameResolverRegistry.java
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -125,8 +126,10 @@ public static synchronized NameResolverRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
NameResolverProvider.class,
- getHardCodedClasses(),
- NameResolverProvider.class.getClassLoader(),
+ ServiceLoader
+ .load(NameResolverProvider.class, NameResolverProvider.class.getClassLoader())
+ .iterator(),
+ NameResolverRegistry::getHardCodedClasses,
new NameResolverPriorityAccessor());
if (providerList.isEmpty()) {
logger.warning("No NameResolverProviders found via ServiceLoader, including for DNS. This "
@@ -182,6 +185,13 @@ public NameResolver newNameResolver(URI targetUri, NameResolver.Args args) {
return provider == null ? null : provider.newNameResolver(targetUri, args);
}
+ @Override
+ @Nullable
+ public NameResolver newNameResolver(io.grpc.Uri targetUri, NameResolver.Args args) {
+ NameResolverProvider provider = getProviderForScheme(targetUri.getScheme());
+ return provider == null ? null : provider.newNameResolver(targetUri, args);
+ }
+
@Override
public String getDefaultScheme() {
return NameResolverRegistry.this.getDefaultScheme();
diff --git a/api/src/main/java/io/grpc/QueryParams.java b/api/src/main/java/io/grpc/QueryParams.java
new file mode 100644
index 00000000000..31bc2e0e6da
--- /dev/null
+++ b/api/src/main/java/io/grpc/QueryParams.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Splitter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * A parser and mutable container class for {@code application/x-www-form-urlencoded}-style URL
+ * parameters as conceived by
+ * RFC 1866 Section 8.2.1.
+ *
+ *
For example, a URI like {@code "http://who?name=John+Doe&role=admin&role=user&active"} has:
+ *
+ *
+ *
A key {@code name} with value {@code John Doe}
+ *
A key {@code role} with value {@code admin}
+ *
A second key named {@code role} with value {@code user}
+ *
"Lone" key {@code active} without a value.
+ *
+ *
+ *
This class is meant to be used with {@link io.grpc.Uri}. For example:
+ *
+ *
Note that the empty collection is encoded as a null raw query string, which means "absent" to
+ * {@link io.grpc.Uri.Builder#setRawQuery}. An empty string query component (""), on the other hand,
+ * is modeled as an instance of QueryParams containing a single lone (empty) key. It must be this
+ * way if we are to simultaneously 1) support lone keys, 2) have parse/toRawQuery round-trip
+ * transparency, and 3) never fail to parse a valid RFC 3986 query component.
+ *
+ *
This container and its {@link Entry} take the same position as {@link io.grpc.Uri} on
+ * equality: raw keys and values must match exactly to be equal. Most callers won't care about how
+ * keys and values are encoded on the wire and will work with the getters for cooked keys and values
+ * instead.
+ *
+ *
Instances are not safe for concurrent access by multiple threads, including by way of the
+ * {@link #asList()} view method.
+ */
+@Internal
+public final class QueryParams {
+
+ private static final String UTF_8 = "UTF-8";
+ private final List entries = new ArrayList<>();
+
+ /** Creates a new, empty {@code QueryParams} instance. */
+ public QueryParams() {}
+
+ /**
+ * Parses a raw query string into a {@code QueryParams} instance.
+ *
+ *
The input is split on {@code '&'} and each parameter is parsed as either a key/value pair
+ * (if it contains an equals sign) or a "lone" key (if it does not).
+ *
+ *
No valid RFC 3986 query component will fail to parse. For example, {@code ===} is parsed as
+ * a single parameter with "" as the key and "==" as the value. {@code &&&} is parsed as three
+ * lone keys named "". And so on. If {@code rawQuery} is not a valid RFC 3986 query component, the
+ * behavior is undefined. But if you are starting with a {@link io.grpc.Uri}, passing the value
+ * returned by {@link io.grpc.Uri#getRawQuery()} is always well-defined and will never fail.
+ *
+ *
Calling {@link #toRawQuery()} on the returned object is guaranteed to return exactly {@code
+ * rawQuery}.
+ *
+ * @param rawQuery the raw query component to parse, or null to return an empty container
+ * @return a new instance of {@code QueryParams} representing the input
+ */
+ public static QueryParams fromRawQuery(@Nullable String rawQuery) {
+ QueryParams params = new QueryParams();
+ if (rawQuery != null) {
+ for (String part : Splitter.on('&').split(rawQuery)) {
+ int equalsIndex = part.indexOf('=');
+ if (equalsIndex == -1) {
+ params.entries.add(Entry.forRawLoneKey(part));
+ } else {
+ String rawKey = part.substring(0, equalsIndex);
+ String rawValue = part.substring(equalsIndex + 1);
+ params.entries.add(Entry.forRawKeyValue(rawKey, rawValue));
+ }
+ }
+ }
+ return params;
+ }
+
+ /**
+ * Returns a mutable list view of the query parameters.
+ *
+ * @return the mutable list of entries
+ */
+ public List asList() {
+ return entries;
+ }
+
+ /**
+ * Returns the "raw" query string representation of these parameters, suitable for passing to the
+ * {@link io.grpc.Uri.Builder#setRawQuery} method.
+ *
+ * @return the raw query string
+ */
+ @Nullable
+ public String toRawQuery() {
+ if (entries.isEmpty()) {
+ return null;
+ }
+ StringBuilder resultBuilder = new StringBuilder();
+ boolean first = true;
+ for (Entry entry : entries) {
+ if (!first) {
+ resultBuilder.append('&');
+ }
+ entry.appendToRawQueryStringBuilder(resultBuilder);
+ first = false;
+ }
+ return resultBuilder.toString();
+ }
+
+ @Override
+ public String toString() {
+ return entries.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof QueryParams)) {
+ return false;
+ }
+ QueryParams other = (QueryParams) o;
+ return entries.equals(other.entries);
+ }
+
+ @Override
+ public int hashCode() {
+ return entries.hashCode();
+ }
+
+ /** A single query parameter entry. */
+ public static final class Entry {
+ private final String rawKey;
+ @Nullable private final String rawValue;
+ private final String key;
+ @Nullable private final String value;
+
+ private Entry(String rawKey, @Nullable String rawValue, String key, @Nullable String value) {
+ this.rawKey = checkNotNull(rawKey, "rawKey");
+ this.rawValue = rawValue;
+ this.key = checkNotNull(key, "key");
+ this.value = value;
+ }
+
+ /**
+ * Returns the key.
+ *
+ *
Any characters that needed URL encoding have already been decoded.
+ */
+ public String getKey() {
+ return key;
+ }
+
+ /**
+ * Returns the value, or {@code null} if this is a "lone" key.
+ *
+ *
Any characters that needed URL encoding have already been decoded.
+ */
+ @Nullable
+ public String getValue() {
+ return value;
+ }
+
+ /** Returns {@code true} if this entry has a value, {@code false} if it is a "lone" key. */
+ public boolean hasValue() {
+ return value != null;
+ }
+
+ /**
+ * Creates a new key/value pair entry.
+ *
+ *
Both key and value can contain any character. They will be URL encoded for you if
+ * necessary.
+ */
+ public static Entry forKeyValue(String key, String value) {
+ checkNotNull(key, "key");
+ checkNotNull(value, "value");
+ return new Entry(encode(key), encode(value), key, value);
+ }
+
+ /**
+ * Creates a new query parameter with a "lone" key.
+ *
+ *
'key' can contain any character. It will be URL encoded for you later, as necessary.
+ *
+ * @param key the decoded key, must not be null
+ * @return a new {@code Entry}
+ */
+ public static Entry forLoneKey(String key) {
+ checkNotNull(key, "key");
+ return new Entry(encode(key), null, key, null);
+ }
+
+ static Entry forRawKeyValue(String rawKey, String rawValue) {
+ checkNotNull(rawKey, "rawKey");
+ checkNotNull(rawValue, "rawValue");
+ return new Entry(rawKey, rawValue, decode(rawKey), decode(rawValue));
+ }
+
+ static Entry forRawLoneKey(String rawKey) {
+ checkNotNull(rawKey, "rawKey");
+ return new Entry(rawKey, null, decode(rawKey), null);
+ }
+
+ void appendToRawQueryStringBuilder(StringBuilder sb) {
+ sb.append(rawKey);
+ if (rawValue != null) {
+ sb.append('=').append(rawValue);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Entry)) {
+ return false;
+ }
+ Entry entry = (Entry) o;
+ return Objects.equals(rawKey, entry.rawKey) && Objects.equals(rawValue, entry.rawValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(rawKey, rawValue);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ appendToRawQueryStringBuilder(sb);
+ return sb.toString();
+ }
+ }
+
+ private static String decode(String s) {
+ try {
+ // TODO: Use URLDecoder.decode(String, Charset) when available
+ return URLDecoder.decode(s, UTF_8);
+ } catch (UnsupportedEncodingException impossible) {
+ throw new AssertionError("UTF-8 is not supported", impossible);
+ }
+ }
+
+ private static String encode(String s) {
+ try {
+ // TODO: Use URLEncoder.encode(String, Charset) when available
+ return URLEncoder.encode(s, UTF_8);
+ } catch (UnsupportedEncodingException impossible) {
+ throw new AssertionError("UTF-8 is not supported", impossible);
+ }
+ }
+}
diff --git a/api/src/main/java/io/grpc/Server.java b/api/src/main/java/io/grpc/Server.java
index 97ea06a81c2..97c4d495b8a 100644
--- a/api/src/main/java/io/grpc/Server.java
+++ b/api/src/main/java/io/grpc/Server.java
@@ -21,13 +21,13 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import javax.annotation.concurrent.ThreadSafe;
/**
* Server for listening for and dispatching incoming calls. It is not expected to be implemented by
* application code or interceptors.
+ *
+ *
This class is thread-safe.
*/
-@ThreadSafe
public abstract class Server {
/**
diff --git a/api/src/main/java/io/grpc/ServerBuilder.java b/api/src/main/java/io/grpc/ServerBuilder.java
index cd1cddbb93f..3effe593e57 100644
--- a/api/src/main/java/io/grpc/ServerBuilder.java
+++ b/api/src/main/java/io/grpc/ServerBuilder.java
@@ -435,6 +435,17 @@ public T setBinaryLog(BinaryLog binaryLog) {
*/
public abstract Server build();
+ /**
+ * Adds a metric sink to the server.
+ *
+ * @param metricSink the metric sink to add.
+ * @return this
+ */
+ @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12693")
+ public T addMetricSink(MetricSink metricSink) {
+ return thisT();
+ }
+
/**
* Returns the correctly typed version of the builder.
*/
diff --git a/api/src/main/java/io/grpc/ServerCallHandler.java b/api/src/main/java/io/grpc/ServerCallHandler.java
index fdfa9997957..7d7d8217300 100644
--- a/api/src/main/java/io/grpc/ServerCallHandler.java
+++ b/api/src/main/java/io/grpc/ServerCallHandler.java
@@ -16,13 +16,12 @@
package io.grpc;
-import javax.annotation.concurrent.ThreadSafe;
-
/**
* Interface to initiate processing of incoming remote calls. Advanced applications and generated
* code will implement this interface to allows {@link Server}s to invoke service methods.
+ *
+ *
This interface is thread-safe.
*/
-@ThreadSafe
public interface ServerCallHandler {
/**
* Starts asynchronous processing of an incoming call.
diff --git a/api/src/main/java/io/grpc/ServerInterceptor.java b/api/src/main/java/io/grpc/ServerInterceptor.java
index 9b2e76ef608..2944cf680fe 100644
--- a/api/src/main/java/io/grpc/ServerInterceptor.java
+++ b/api/src/main/java/io/grpc/ServerInterceptor.java
@@ -16,7 +16,6 @@
package io.grpc;
-import javax.annotation.concurrent.ThreadSafe;
/**
* Interface for intercepting incoming calls before they are dispatched by
@@ -34,8 +33,9 @@
* without completing the previous ones first. Refer to the
* {@link io.grpc.ServerCall.Listener ServerCall.Listener} docs for more details regarding thread
* safety of the returned listener.
+ *
+ *
This interface is thread-safe.
*/
-@ThreadSafe
public interface ServerInterceptor {
/**
* Intercept {@link ServerCall} dispatch by the {@code next} {@link ServerCallHandler}. General
diff --git a/api/src/main/java/io/grpc/ServerRegistry.java b/api/src/main/java/io/grpc/ServerRegistry.java
index 5b9c8c558e7..1ec7030b82b 100644
--- a/api/src/main/java/io/grpc/ServerRegistry.java
+++ b/api/src/main/java/io/grpc/ServerRegistry.java
@@ -24,6 +24,7 @@
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.concurrent.ThreadSafe;
@@ -93,8 +94,9 @@ public static synchronized ServerRegistry getDefaultRegistry() {
if (instance == null) {
List providerList = ServiceProviders.loadAll(
ServerProvider.class,
- getHardCodedClasses(),
- ServerProvider.class.getClassLoader(),
+ ServiceLoader.load(ServerProvider.class, ServerProvider.class.getClassLoader())
+ .iterator(),
+ ServerRegistry::getHardCodedClasses,
new ServerPriorityAccessor());
instance = new ServerRegistry();
for (ServerProvider provider : providerList) {
diff --git a/api/src/main/java/io/grpc/ServerStreamTracer.java b/api/src/main/java/io/grpc/ServerStreamTracer.java
index d522610ab3a..81691642131 100644
--- a/api/src/main/java/io/grpc/ServerStreamTracer.java
+++ b/api/src/main/java/io/grpc/ServerStreamTracer.java
@@ -17,13 +17,13 @@
package io.grpc;
import javax.annotation.Nullable;
-import javax.annotation.concurrent.ThreadSafe;
/**
* Listens to events on a stream to collect metrics.
+ *
+ *
This class is thread-safe.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
-@ThreadSafe
public abstract class ServerStreamTracer extends StreamTracer {
/**
* Called before the interceptors and the call handlers and make changes to the Context object
diff --git a/api/src/main/java/io/grpc/ServiceProviders.java b/api/src/main/java/io/grpc/ServiceProviders.java
index ac4b27d8783..861688be9fb 100644
--- a/api/src/main/java/io/grpc/ServiceProviders.java
+++ b/api/src/main/java/io/grpc/ServiceProviders.java
@@ -17,10 +17,13 @@
package io.grpc;
import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Supplier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Iterator;
import java.util.List;
+import java.util.ListIterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
@@ -29,42 +32,44 @@ private ServiceProviders() {
// do not instantiate
}
- /**
- * If this is not Android, returns the highest priority implementation of the class via
- * {@link ServiceLoader}.
- * If this is Android, returns an instance of the highest priority class in {@code hardcoded}.
- */
- public static T load(
- Class klass,
- Iterable> hardcoded,
- ClassLoader cl,
- PriorityAccessor priorityAccessor) {
- List candidates = loadAll(klass, hardcoded, cl, priorityAccessor);
- if (candidates.isEmpty()) {
- return null;
- }
- return candidates.get(0);
- }
-
/**
* If this is not Android, returns all available implementations discovered via
* {@link ServiceLoader}.
* If this is Android, returns all available implementations in {@code hardcoded}.
* The list is sorted in descending priority order.
+ *
+ *
{@code serviceLoader} should be created with {@code ServiceLoader.load(MyClass.class,
+ * MyClass.class.getClassLoader()).iterator()} in order to be detected by R8 so that R8 full mode
+ * will keep the constructors for the provider classes.
*/
public static List loadAll(
Class klass,
- Iterable> hardcoded,
- ClassLoader cl,
+ Iterator serviceLoader,
+ Supplier>> hardcoded,
final PriorityAccessor priorityAccessor) {
- Iterable candidates;
- if (isAndroid(cl)) {
- candidates = getCandidatesViaHardCoded(klass, hardcoded);
+ Iterator candidates;
+ if (serviceLoader instanceof ListIterator) {
+ // A rewriting tool has replaced the ServiceLoader with a List of some sort (R8 uses
+ // ArrayList, AppReduce uses singletonList). We prefer to use such iterators on Android as
+ // they won't need reflection like the hard-coded list does. In addition, the provider
+ // instances will have already been created, so it seems we should use them.
+ //
+ // R8: https://r8.googlesource.com/r8/+/490bc53d9310d4cc2a5084c05df4aadaec8c885d/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+ // AppReduce: service_loader_pass.cc
+ candidates = serviceLoader;
+ } else if (isAndroid(klass.getClassLoader())) {
+ // Avoid getResource() on Android, which must read from a zip which uses a lot of memory
+ candidates = getCandidatesViaHardCoded(klass, hardcoded.get()).iterator();
+ } else if (!serviceLoader.hasNext()) {
+ // Attempt to load using the context class loader and ServiceLoader.
+ // This allows frameworks like http://aries.apache.org/modules/spi-fly.html to plug in.
+ candidates = ServiceLoader.load(klass).iterator();
} else {
- candidates = getCandidatesViaServiceLoader(klass, cl);
+ candidates = serviceLoader;
}
List list = new ArrayList<>();
- for (T current: candidates) {
+ while (candidates.hasNext()) {
+ T current = candidates.next();
if (!priorityAccessor.isAvailable(current)) {
continue;
}
@@ -101,15 +106,14 @@ static boolean isAndroid(ClassLoader cl) {
}
/**
- * Loads service providers for the {@code klass} service using {@link ServiceLoader}.
+ * For testing only: Loads service providers for the {@code klass} service using {@link
+ * ServiceLoader}. Does not support spi-fly and related tricks.
*/
@VisibleForTesting
public static Iterable getCandidatesViaServiceLoader(Class klass, ClassLoader cl) {
Iterable i = ServiceLoader.load(klass, cl);
- // Attempt to load using the context class loader and ServiceLoader.
- // This allows frameworks like http://aries.apache.org/modules/spi-fly.html to plug in.
if (!i.iterator().hasNext()) {
- i = ServiceLoader.load(klass);
+ return null;
}
return i;
}
diff --git a/api/src/main/java/io/grpc/StatusException.java b/api/src/main/java/io/grpc/StatusException.java
index 2a235c3aaaf..c0a67a375b2 100644
--- a/api/src/main/java/io/grpc/StatusException.java
+++ b/api/src/main/java/io/grpc/StatusException.java
@@ -65,6 +65,7 @@ public final Status getStatus() {
*
* @since 1.0.0
*/
+ @Nullable
public final Metadata getTrailers() {
return trailers;
}
diff --git a/api/src/main/java/io/grpc/StatusOr.java b/api/src/main/java/io/grpc/StatusOr.java
index 0f1e9da3c75..b7dd68cfd7b 100644
--- a/api/src/main/java/io/grpc/StatusOr.java
+++ b/api/src/main/java/io/grpc/StatusOr.java
@@ -33,7 +33,7 @@ private StatusOr(Status status, T value) {
}
/** Construct from a value. */
- public static StatusOr fromValue(@Nullable T value) {
+ public static StatusOr fromValue(T value) {
StatusOr result = new StatusOr(null, value);
return result;
}
@@ -54,7 +54,7 @@ public boolean hasValue() {
* Returns the value if set or throws exception if there is no value set. This method is meant
* to be called after checking the return value of hasValue() first.
*/
- public @Nullable T getValue() {
+ public T getValue() {
if (status != null) {
throw new IllegalStateException("No value present.");
}
@@ -105,6 +105,7 @@ public String toString() {
return stringHelper.toString();
}
+ @Nullable
private final Status status;
private final T value;
}
diff --git a/api/src/main/java/io/grpc/StreamTracer.java b/api/src/main/java/io/grpc/StreamTracer.java
index 66b3de8be6b..251e6e2b49f 100644
--- a/api/src/main/java/io/grpc/StreamTracer.java
+++ b/api/src/main/java/io/grpc/StreamTracer.java
@@ -16,15 +16,14 @@
package io.grpc;
-import javax.annotation.concurrent.ThreadSafe;
-
/**
* Listens to events on a stream to collect metrics.
*
*
DO NOT MOCK: Use TestStreamTracer. Mocks are not thread-safe
+ *
+ *
This class is thread-safe.
*/
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/2861")
-@ThreadSafe
public abstract class StreamTracer {
/**
* Stream is closed. This will be called exactly once.
diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java
new file mode 100644
index 00000000000..a88bb6138d8
--- /dev/null
+++ b/api/src/main/java/io/grpc/Uri.java
@@ -0,0 +1,1184 @@
+/*
+ * Copyright 2025 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+import com.google.common.base.VerifyException;
+import com.google.common.collect.ImmutableList;
+import com.google.common.net.InetAddresses;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.net.InetAddress;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CodingErrorAction;
+import java.nio.charset.MalformedInputException;
+import java.nio.charset.StandardCharsets;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+/**
+ * A not-quite-general-purpose representation of a Uniform Resource Identifier (URI), as defined by
+ * RFC 3986.
+ *
+ *
The URI
+ *
+ *
A URI identifies a resource by its name or location or both. The resource could be a file,
+ * service, or some other abstract entity.
+ *
+ *
This class aims to meet the needs of grpc-java itself and RPC related code that depend on it.
+ * It isn't quite general-purpose. It definitely would not be suitable for building an HTTP user
+ * agent or proxy server. In particular, it:
+ *
+ *
+ *
Can only represent a URI, not a "URI-reference" or "relative reference". In other words, a
+ * "scheme" is always required.
+ *
Has no knowledge of the particulars of any scheme, with respect to normalization and
+ * comparison. We don't know https://google.com is the same as
+ * https://google.com:443, that file:/// is the same as
+ * file://localhost, or that joe@example.com is the same as
+ * joe@EXAMPLE.COM. No one class can or should know everything about every scheme so
+ * all this is better handled at a higher layer.
+ *
Implements {@link #equals(Object)} as a char-by-char comparison. Expect false negatives.
+ *
Does not support "IPvFuture" literal addresses.
+ *
Does not reflect how web browsers parse user input or the URL Living Standard.
+ *
Does not support different character encodings. Assumes UTF-8 in several places.
+ *
+ *
+ *
Migrating from RFC 2396 and {@link java.net.URI}
+ *
+ *
Those migrating from {@link java.net.URI} and/or its primary specification in RFC 2396 should
+ * note some differences.
+ *
+ *
Uniform Hierarchical Syntax
+ *
+ *
RFC 3986 unifies the older ideas of "hierarchical" and "opaque" URIs into a single generic
+ * syntax. What RFC 2396 called an opaque "scheme-specific part" is always broken out by RFC 3986
+ * into an authority and path hierarchy, followed by query and fragment components. Accordingly,
+ * this class has only getters for those components but no {@link
+ * java.net.URI#getSchemeSpecificPart()} analog.
+ *
+ *
The RFC 3986 definition of path is now more liberal to accommodate this:
+ *
+ *
+ *
Path doesn't have to start with a slash. For example, the path of
+ * urn:isbn:978-1492082798 is isbn:978-1492082798 even though it doesn't
+ * look much like a file system path.
+ *
The path can now be empty. So Android's
+ * intent:#Intent;action=MAIN;category=LAUNCHER;end is now a valid {@link Uri}. Even
+ * the scheme-only about: is now valid.
+ *
+ *
+ *
The uniform syntax always understands what follows a '?' to be a query string. For example,
+ * mailto:me@example.com?subject=foo now has a query component whereas RFC 2396
+ * considered everything after the mailto: scheme to be opaque.
+ *
+ *
Same goes for fragment. data:image/png;...#xywh=0,0,10,10 now has a fragment
+ * whereas RFC 2396 considered everything after the scheme to be opaque.
+ *
+ *
Uniform Authority Syntax
+ *
+ *
RFC 2396 tried to guess if an authority was a "server" (host:port) or "registry-based"
+ * (arbitrary string) based on its contents. RFC 3986 expects every authority to look like
+ * [userinfo@]host[:port] and loosens the definition of a "host" to accommodate. Accordingly, this
+ * class has no equivalent to {@link java.net.URI#parseServerAuthority()} -- authority was parsed
+ * into its components and checked for validity when the {@link Uri} was created.
+ *
+ *
Other Specific Differences
+ *
+ *
RFC 2396 does not allow underscores in a host name, meaning {@link java.net.URI} switches to
+ * opaque mode when it sees one. {@link Uri} does allow underscores in host, to accommodate
+ * registries other than DNS. So http://my_site.com:8080/index.html now parses as a
+ * host, port and path rather than a single opaque scheme-specific part.
+ *
+ *
{@link Uri} strictly *requires* square brackets in the query string and fragment to be
+ * percent-encoded whereas RFC 2396 merely recommended doing so.
+ *
+ *
Other URx classes are "liberal in what they accept and strict in what they produce." {@link
+ * Uri#parse(String)} and {@link Uri#create(String)}, however, are strict in what they accept and
+ * transparent when asked to reproduce it via {@link Uri#toString()}. The former policy may be
+ * appropriate for parsing user input or web content, but this class is meant for gRPC clients,
+ * servers and plugins like name resolvers where human error at runtime is less likely and best
+ * detected early. {@link java.net.URI#create(String)} is similarly strict, which makes migration
+ * easy, except for the server/registry-based ambiguity addressed by {@link
+ * java.net.URI#parseServerAuthority()}.
+ *
+ *
{@link java.net.URI} and {@link Uri} both support IPv6 literals in square brackets as defined
+ * by RFC 2732.
+ *
+ *
{@link java.net.URI} supports IPv6 scope IDs but accepts and emits a non-standard syntax.
+ * {@link Uri} implements the newer RFC 6874, which percent encodes scope IDs and the % delimiter
+ * itself. RFC 9844 claims to obsolete RFC 6874 because web browsers would not support it. This
+ * class implements RFC 6874 anyway, mostly to avoid creating a barrier to migration away from
+ * {@link java.net.URI}.
+ *
+ *
Some URI components, e.g. scheme, are required while others may or may not be present, e.g.
+ * authority. {@link Uri} is careful to preserve the distinction between an absent string component
+ * (getter returns null) and one with an empty value (getter returns ""). {@link java.net.URI} makes
+ * this distinction too, *except* when it comes to the authority and host components: {@link
+ * java.net.URI#getAuthority()} and {@link java.net.URI#getHost()} return null when an authority is
+ * absent, e.g. file:/path as expected. But these methods surprisingly also return null
+ * when the authority is the empty string, e.g.file:///path. {@link Uri}'s getters
+ * correctly return null and "" in these cases, respectively, as one would expect.
+ */
+@Internal
+public final class Uri {
+ // Components are stored percent-encoded, just as originally parsed for transparent parse/toString
+ // round-tripping.
+ private final String scheme; // != null since we don't support relative references.
+ @Nullable private final String userInfo;
+ @Nullable private final String host;
+ @Nullable private final String port;
+ private final String path; // In RFC 3986, path is always defined (but can be empty).
+ @Nullable private final String query;
+ @Nullable private final String fragment;
+
+ private Uri(Builder builder) {
+ this.scheme = checkNotNull(builder.scheme, "scheme");
+ this.userInfo = builder.userInfo;
+ this.host = builder.host;
+ this.port = builder.port;
+ this.path = builder.path;
+ this.query = builder.query;
+ this.fragment = builder.fragment;
+
+ // Checks common to the parse() and Builder code paths.
+ if (hasAuthority()) {
+ if (!path.isEmpty() && !path.startsWith("/")) {
+ throw new IllegalArgumentException("Has authority -- Non-empty path must start with '/'");
+ }
+ } else {
+ if (path.startsWith("//")) {
+ throw new IllegalArgumentException("No authority -- Path cannot start with '//'");
+ }
+ }
+ }
+
+ /**
+ * Parses a URI from its string form.
+ *
+ * @throws URISyntaxException if 's' is not a valid RFC 3986 URI.
+ */
+ public static Uri parse(String s) throws URISyntaxException {
+ try {
+ return create(s);
+ } catch (IllegalArgumentException e) {
+ throw new URISyntaxException(s, e.getMessage());
+ }
+ }
+
+ /**
+ * Creates a URI from a string assumed to be valid.
+ *
+ *
Useful for defining URI constants in code. Not for user input.
+ *
+ * @throws IllegalArgumentException if 's' is not a valid RFC 3986 URI.
+ */
+ public static Uri create(String s) {
+ Builder builder = new Builder();
+ int i = 0;
+ final int n = s.length();
+
+ // 3.1. Scheme: Look for a ':' before '/', '?', or '#'.
+ int schemeColon = -1;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == ':') {
+ schemeColon = i;
+ break;
+ } else if (c == '/' || c == '?' || c == '#') {
+ break;
+ }
+ }
+ if (schemeColon < 0) {
+ throw new IllegalArgumentException("Missing required scheme.");
+ }
+ builder.setRawScheme(s.substring(0, schemeColon));
+
+ // 3.2. Authority. Look for '//' then keep scanning until '/', '?', or '#'.
+ i = schemeColon + 1;
+ if (i + 1 < n && s.charAt(i) == '/' && s.charAt(i + 1) == '/') {
+ // "//" just means we have an authority. Skip over it.
+ i += 2;
+
+ int authorityStart = i;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == '/' || c == '?' || c == '#') {
+ break;
+ }
+ }
+ builder.setRawAuthority(s.substring(authorityStart, i));
+ }
+
+ // 3.3. Path: Whatever is left before '?' or '#'.
+ int pathStart = i;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == '?' || c == '#') {
+ break;
+ }
+ }
+ builder.setRawPath(s.substring(pathStart, i));
+
+ // 3.4. Query, if we stopped at '?'.
+ if (i < n && s.charAt(i) == '?') {
+ i++; // Skip '?'
+ int queryStart = i;
+ for (; i < n; ++i) {
+ char c = s.charAt(i);
+ if (c == '#') {
+ break;
+ }
+ }
+ builder.setRawQuery(s.substring(queryStart, i));
+ }
+
+ // 3.5. Fragment, if we stopped at '#'.
+ if (i < n && s.charAt(i) == '#') {
+ ++i; // Skip '#'
+ builder.setRawFragment(s.substring(i));
+ }
+
+ return builder.build();
+ }
+
+ private static int findPortStartColon(String authority, int hostStart) {
+ for (int i = authority.length() - 1; i >= hostStart; --i) {
+ char c = authority.charAt(i);
+ if (c == ':') {
+ return i;
+ }
+ if (c == ']') {
+ // Hit the end of IP-literal. Any further colon is inside it and couldn't indicate a port.
+ break;
+ }
+ if (!digitChars.get(c)) {
+ // Found a non-digit, non-colon, non-bracket.
+ // This means there is no valid port (e.g. host is "example.com")
+ break;
+ }
+ }
+ return -1;
+ }
+
+ // Checks a raw path for validity and parses it into segments. Let 'out' be null to just validate.
+ private static void parseAssumedUtf8PathIntoSegments(
+ String path, ImmutableList.Builder out) {
+ // Skip the first slash so it doesn't count as an empty segment at the start.
+ // (e.g., "/a" -> ["a"], not ["", "a"])
+ int start = path.startsWith("/") ? 1 : 0;
+
+ for (int i = start; i < path.length(); ) {
+ int nextSlash = path.indexOf('/', i);
+ String segment;
+ if (nextSlash >= 0) {
+ // Typical segment case (e.g., "foo" in "/foo/bar").
+ segment = path.substring(i, nextSlash);
+ i = nextSlash + 1;
+ } else {
+ // Final segment case (e.g., "bar" in "/foo/bar").
+ segment = path.substring(i);
+ i = path.length();
+ }
+ if (out != null) {
+ out.add(percentDecodeAssumedUtf8(segment));
+ } else {
+ checkPercentEncodedArg(segment, "path segment", pChars);
+ }
+ }
+
+ // RFC 3986 says a trailing slash creates a final empty segment.
+ // (e.g., "/foo/" -> ["foo", ""])
+ if (path.endsWith("/") && out != null) {
+ out.add("");
+ }
+ }
+
+ /** Returns the scheme of this URI. */
+ public String getScheme() {
+ return scheme;
+ }
+
+ /**
+ * Returns the percent-decoded "Authority" component of this URI, or null if not present.
+ *
+ *
NB: This method's decoding is lossy -- It only exists for compatibility with {@link
+ * java.net.URI}. Prefer {@link #getRawAuthority()} or work instead with authority in terms of its
+ * individual components ({@link #getUserInfo()}, {@link #getHost()} and {@link #getPort()}). The
+ * problem with getAuthority() is that it returns the delimited concatenation of the percent-
+ * decoded userinfo, host and port components. But both userinfo and host can contain the '@'
+ * character, which becomes indistinguishable from the userinfo/host delimiter after decoding. For
+ * example, URIs scheme://x@y%40z and scheme://x%40y@z have different
+ * userinfo and host components but getAuthority() returns "x@y@z" for both of them.
+ *
+ *
NB: This method assumes the "host" component was encoded as UTF-8, as mandated by RFC 3986.
+ * This method also assumes the "user information" part of authority was encoded as UTF-8,
+ * although RFC 3986 doesn't specify an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawAuthority()}, {@link #percentDecode(CharSequence)}, then decode the bytes for
+ * themselves.
+ */
+ @Nullable
+ public String getAuthority() {
+ return percentDecodeAssumedUtf8(getRawAuthority());
+ }
+
+ private boolean hasAuthority() {
+ return host != null;
+ }
+
+ /**
+ * Returns the "authority" component of this URI in its originally parsed, possibly
+ * percent-encoded form.
+ */
+ @Nullable
+ public String getRawAuthority() {
+ if (hasAuthority()) {
+ StringBuilder sb = new StringBuilder();
+ appendAuthority(sb);
+ return sb.toString();
+ }
+ return null;
+ }
+
+ private void appendAuthority(StringBuilder sb) {
+ if (userInfo != null) {
+ sb.append(userInfo).append('@');
+ }
+ if (host != null) {
+ sb.append(host);
+ }
+ if (port != null) {
+ sb.append(':').append(port);
+ }
+ }
+
+ /**
+ * Returns the percent-decoded "User Information" component of this URI, or null if not present.
+ *
+ *
NB: This method *assumes* this component was encoded as UTF-8, although RFC 3986 doesn't
+ * specify an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawUserInfo()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ */
+ @Nullable
+ public String getUserInfo() {
+ return percentDecodeAssumedUtf8(userInfo);
+ }
+
+ /**
+ * Returns the "User Information" component of this URI in its originally parsed, possibly
+ * percent-encoded form.
+ */
+ @Nullable
+ public String getRawUserInfo() {
+ return userInfo;
+ }
+
+ /**
+ * Returns the percent-decoded "host" component of this URI, or null if not present.
+ *
+ *
This method assumes the host was encoded as UTF-8, as mandated by RFC 3986.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawHost()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ */
+ @Nullable
+ public String getHost() {
+ return percentDecodeAssumedUtf8(host);
+ }
+
+ /**
+ * Returns the host component of this URI in its originally parsed, possibly percent-encoded form.
+ */
+ @Nullable
+ public String getRawHost() {
+ return host;
+ }
+
+ /** Returns the "port" component of this URI, or -1 if empty or not present. */
+ public int getPort() {
+ return port != null && !port.isEmpty() ? Integer.parseInt(port) : -1;
+ }
+
+ /** Returns the raw port component of this URI in its originally parsed form. */
+ @Nullable
+ public String getRawPort() {
+ return port;
+ }
+
+ /**
+ * Returns the (possibly empty) percent-decoded "path" component of this URI.
+ *
+ *
NB: This method *assumes* the path was encoded as UTF-8, although RFC 3986 doesn't specify
+ * an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawPath()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ *
+ *
NB: Prefer {@link #getPathSegments()} because this method's decoding is lossy. For example,
+ * consider these (different) URIs:
+ *
+ *
+ *
file:///home%2Ffolder/my%20file
+ *
file:///home/folder/my%20file
+ *
+ *
+ *
Calling getPath() on each returns the same string: /home/folder/my file. You
+ * can't tell whether the second '/' character is part of the first path segment or separates the
+ * first and second path segments. This method only exists to ease migration from {@link
+ * java.net.URI}.
+ */
+ public String getPath() {
+ return percentDecodeAssumedUtf8(path);
+ }
+
+ /**
+ * Returns this URI's path as a list of path segments not including the '/' segment delimiters.
+ *
+ *
Prefer this method over {@link #getPath()} because it preserves the distinction between
+ * segment separators and literal '/'s within a path segment.
+ *
+ *
A trailing '/' delimiter in the path results in the empty string as the last element in the
+ * returned list. For example, file://localhost/foo/bar/ has path segments
+ * ["foo", "bar", ""]
+ *
+ *
A leading '/' delimiter cannot be detected using this method. For example, both
+ * dns:example.com and dns:///example.com have the same list of path segments:
+ * ["example.com"]. Use {@link #isPathAbsolute()} or {@link #isPathRootless()} to
+ * distinguish these cases.
+ *
+ *
The returned list is immutable.
+ */
+ public List getPathSegments() {
+ // Returned list must be immutable but we intentionally keep guava out of the public API.
+ ImmutableList.Builder segmentsBuilder = ImmutableList.builder();
+ parseAssumedUtf8PathIntoSegments(path, segmentsBuilder);
+ return segmentsBuilder.build();
+ }
+
+ /**
+ * Returns true iff this URI's path component starts with a path segment (rather than the '/'
+ * segment delimiter).
+ *
+ *
The path of an RFC 3986 URI is either empty, absolute (starts with the '/' segment
+ * delimiter) or rootless (starts with a path segment). For example, tel:+1-206-555-1212
+ * , mailto:me@example.com and urn:isbn:978-1492082798 all have
+ * rootless paths. mailto:%2Fdev%2Fnull@example.com is also rootless because its
+ * percent-encoded slashes are not segment delimiters but rather part of the first and only path
+ * segment.
+ *
+ *
Contrast rootless paths with absolute ones (see {@link #isPathAbsolute()}.
+ */
+ public boolean isPathRootless() {
+ return !path.isEmpty() && !path.startsWith("/");
+ }
+
+ /**
+ * Returns true iff this URI's path component starts with the '/' segment delimiter (rather than a
+ * path segment).
+ *
+ *
The path of an RFC 3986 URI is either empty, absolute (starts with the '/' segment
+ * delimiter) or rootless (starts with a path segment). For example, file:///resume.txt
+ * , file:/resume.txt and file://localhost/ all have absolute
+ * paths while tel:+1-206-555-1212's path is not absolute.
+ * mailto:%2Fdev%2Fnull@example.com is also not absolute because its percent-encoded
+ * slashes are not segment delimiters but rather part of the first and only path segment.
+ *
+ *
Contrast absolute paths with rootless ones (see {@link #isPathRootless()}.
+ *
+ *
NB: The term "absolute" has two different meanings in RFC 3986 which are easily confused.
+ * This method tests for a property of this URI's path component. Contrast with {@link
+ * #isAbsolute()} which tests the URI itself for a different property.
+ */
+ public boolean isPathAbsolute() {
+ return path.startsWith("/");
+ }
+
+ /**
+ * Returns the path component of this URI in its originally parsed, possibly percent-encoded form.
+ */
+ public String getRawPath() {
+ return path;
+ }
+
+ /**
+ * Returns the query component of this URI in its originally parsed, possibly percent-encoded
+ * form, without any leading '?' character, or null if not present.
+ *
+ *
The query component can only be read in its raw form. That’s because virtually everyone uses
+ * query as a container for structured data, with some additional layer of encoding not present in
+ * RFC-3986. Like 'application/x-www-form-urlencoded', which encodes key/value pairs like so:
+ * ?k1=v1&k2=v+2. The encoding of these containers always has characters that take on
+ * a special delimiter meaning when not percent-encoded and a literal meaning when they are (like
+ * '&', '=' and '+' above). Since it matters whether a character was percent encoded or not,
+ * offering a '#getQuery()' method that percent-decodes everything like we do for other components
+ * would be error-prone.
+ */
+ @Nullable
+ public String getRawQuery() {
+ return query;
+ }
+
+ /**
+ * Returns the percent-decoded "fragment" component of this URI, or null if not present.
+ *
+ *
NB: This method assumes the fragment was encoded as UTF-8, although RFC 3986 doesn't specify
+ * an encoding.
+ *
+ *
Decoding errors are indicated by a {@code '\u005CuFFFD'} unicode replacement character in
+ * the output. Callers who want to detect and handle errors in some other way should call {@link
+ * #getRawFragment()}, {@link #percentDecode(CharSequence)}, then decode the bytes for themselves.
+ *
+ *
NB: Choose carefully between this method and {@link #getRawFragment()}. Many URI schemes
+ * embed further structure inside the fragment that isn't part of the RFC 3986 generic syntax. For
+ * example, Android uses the fragment to encode the many fields of an Intent, like {@code
+ * intent:#Intent;S.key=val;end;}. And the URI of a JSON resource may use RFC 6901 in its fragment
+ * to point at a particular node, e.g. {@code
+ * file:/etc/config/service.json#/methodConfig/0/retryPolicy/maxBackoff}.
+ *
+ *
When percent-encoding is used to escape internal delimiters, like a literal ';' and '=' in
+ * an `intent:`, call {@link #getRawFragment()} to preserve that percent-encoding, or risk
+ * corruption. Conversely, use *this* method when percent-decoding is needed *before* any further
+ * interpretation, like with a JSON pointer, which must be percent-encoded in a URI fragment but
+ * uses a completely different method of escaping literal '/' characters.
+ */
+ @Nullable
+ public String getFragment() {
+ return percentDecodeAssumedUtf8(fragment);
+ }
+
+ /**
+ * Returns the fragment component of this URI in its original, possibly percent-encoded form, and
+ * without any leading '#' character.
+ *
+ *
NB: Choose carefully between this method and {@link #getFragment()}. See that Javadoc for
+ * details.
+ */
+ @Nullable
+ public String getRawFragment() {
+ return fragment;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
If this URI was created by {@link #parse(String)} or {@link #create(String)}, then the
+ * returned string will match that original input exactly.
+ */
+ @Override
+ public String toString() {
+ // https://datatracker.ietf.org/doc/html/rfc3986#section-5.3
+ StringBuilder sb = new StringBuilder();
+ sb.append(scheme).append(':');
+ if (hasAuthority()) {
+ sb.append("//");
+ appendAuthority(sb);
+ }
+ sb.append(path);
+ if (query != null) {
+ sb.append('?').append(query);
+ }
+ if (fragment != null) {
+ sb.append('#').append(fragment);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns true iff this URI has a scheme and an authority/path hierarchy, but no fragment.
+ *
+ *
All instances of {@link Uri} are RFC 3986 URIs, not "relative references", so this method is
+ * equivalent to {@code getFragment() == null}. It mostly exists for compatibility with {@link
+ * java.net.URI}.
+ */
+ public boolean isAbsolute() {
+ return scheme != null && fragment == null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ *
Two instances of {@link Uri} are equal if and only if they have the same string
+ * representation, which RFC 3986 calls "Simple String Comparison" (6.2.1). Callers with a higher
+ * layer expectation of equality (e.g. http://some%2Dhost:80/foo/./bar.txt ~=
+ * http://some-host/foo/bar.txt) will experience false negatives.
+ */
+ @Override
+ public boolean equals(Object otherObj) {
+ if (!(otherObj instanceof Uri)) {
+ return false;
+ }
+ Uri other = (Uri) otherObj;
+ return Objects.equals(scheme, other.scheme)
+ && Objects.equals(userInfo, other.userInfo)
+ && Objects.equals(host, other.host)
+ && Objects.equals(port, other.port)
+ && Objects.equals(path, other.path)
+ && Objects.equals(query, other.query)
+ && Objects.equals(fragment, other.fragment);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(scheme, userInfo, host, port, path, query, fragment);
+ }
+
+ /** Returns a new Builder initialized with the fields of this URI. */
+ public Builder toBuilder() {
+ return new Builder(this);
+ }
+
+ /** Creates a new {@link Builder} with all fields uninitialized or set to their default values. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Builder for {@link Uri}. */
+ public static final class Builder {
+ private String scheme;
+ private String path = "";
+ private String query;
+ private String fragment;
+ private String userInfo;
+ private String host;
+ private String port;
+
+ private Builder() {}
+
+ Builder(Uri prototype) {
+ this.scheme = prototype.scheme;
+ this.userInfo = prototype.userInfo;
+ this.host = prototype.host;
+ this.port = prototype.port;
+ this.path = prototype.path;
+ this.query = prototype.query;
+ this.fragment = prototype.fragment;
+ }
+
+ /**
+ * Sets the scheme, e.g. "https", "dns" or "xds".
+ *
+ *
This field is required.
+ *
+ * @return this, for fluent building
+ * @throws IllegalArgumentException if the scheme is invalid.
+ */
+ @CanIgnoreReturnValue
+ public Builder setScheme(String scheme) {
+ return setRawScheme(scheme.toLowerCase(Locale.ROOT));
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawScheme(String scheme) {
+ if (scheme.isEmpty() || !alphaChars.get(scheme.charAt(0))) {
+ throw new IllegalArgumentException("Scheme must start with an alphabetic char");
+ }
+ for (int i = 0; i < scheme.length(); i++) {
+ char c = scheme.charAt(i);
+ if (!schemeChars.get(c)) {
+ throw new IllegalArgumentException("Invalid character in scheme at index " + i);
+ }
+ }
+ this.scheme = scheme;
+ return this;
+ }
+
+ /**
+ * Specifies the new URI's path component as a string of zero or more '/' delimited segments.
+ *
+ *
Path segments can consist of any string of codepoints. Codepoints that can't be encoded
+ * literally will be percent-encoded for you.
+ *
+ *
If a URI contains an authority component, then the path component must either be empty or
+ * begin with a slash ("/") character. If a URI does not contain an authority component, then
+ * the path cannot begin with two slash characters ("//").
+ *
+ *
This method interprets all '/' characters in 'path' as segment delimiters. If any of your
+ * segments contain literal '/' characters, call {@link #setRawPath(String)} instead.
+ *
+ *
This field is required but can be empty (its default value).
+ *
+ * @param path the new path
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setPath(String path) {
+ checkArgument(path != null, "Path can be empty but not null");
+ this.path = percentEncode(path, pCharsAndSlash);
+ return this;
+ }
+
+ /**
+ * Specifies the new URI's path component as a string of zero or more '/' delimited segments.
+ *
+ *
Path segments can consist of any string of codepoints but the caller must first percent-
+ * encode anything other than RFC 3986's "pchar" character class using UTF-8.
+ *
+ *
If a URI contains an authority component, then the path component must either be empty or
+ * begin with a slash ("/") character. If a URI does not contain an authority component, then
+ * the path cannot begin with two slash characters ("//").
+ *
+ *
This method interprets all '/' characters in 'path' as segment delimiters. If any of your
+ * segments contain literal '/' characters, you must percent-encode them.
+ *
+ *
This field is required but can be empty (its default value).
+ *
+ * @param path the new path, a string consisting of characters from "pchar"
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawPath(String path) {
+ checkArgument(path != null, "Path can be empty but not null");
+ parseAssumedUtf8PathIntoSegments(path, null);
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * Specifies the query component of the new URI, possibly percent-encoded, exactly as it will
+ * appear in the string form of the built URI.
+ *
+ *
'query' must only contain codepoints from RFC 3986's "query" character class. Any other
+ * characters must be percent-encoded using UTF-8. Do not include the leading '?' delimiter.
+ *
+ *
The query component can only be provided in its raw form. That’s because virtually
+ * everyone uses query as a container for structured data, with some additional layer of
+ * encoding not present in RFC-3986. Like 'application/x-www-form-urlencoded', which encodes
+ * key/value pairs like so: ?k1=v1&k2=v+2. The encoding of these containers always
+ * has characters that take on a special delimiter meaning when not percent-encoded and a
+ * literal meaning when they are (like '&', '=' and '+' above). Since 'query' must have already
+ * been carefully percent-encoded externally, a '#setQuery(String)' method that percent-encodes
+ * an assumed-cooked string would be error-prone.
+ *
+ *
This field is optional.
+ *
+ * @param query the new query component, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawQuery(@Nullable String query) {
+ if (query != null) {
+ checkPercentEncodedArg(query, "query", queryChars);
+ }
+ this.query = query;
+ return this;
+ }
+
+ /**
+ * Specifies the fragment component of the new URI (not including the leading '#').
+ *
+ *
The fragment can contain any string of codepoints. Codepoints that can't be encoded
+ * literally will be percent-encoded for you as UTF-8.
+ *
+ *
NB: Choose carefully between this method and {@link #setRawFragment(String)}. Many URI
+ * schemes embed further structure in the fragment that isn't part of the RFC 3986 generic
+ * syntax. These schemes often use internal delimiters that must be carefully percent-encoded in
+ * ways that this method doesn't understand. See {@link #getFragment()} for an example. In that
+ * case, callers should percent-encode externally then call {@link #setRawFragment(String)}
+ * instead.
+ *
+ *
This field is optional.
+ *
+ * @param fragment the new fragment component, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setFragment(@Nullable String fragment) {
+ this.fragment = percentEncode(fragment, fragmentChars);
+ return this;
+ }
+
+ /**
+ * Specifies the fragment component of the new URI, already percent-encoded, exactly as it will
+ * appear after the '#' delimiter in the string form of the built URI.
+ *
+ *
NB: Choose carefully between this method and {@link #setFragment(String)}. {@code
+ * fragment} must only contain codepoints from RFC 3986's "fragment" character class. Use
+ * percent-encoding and UTF-8 to represent anything else. In certain cases, you can use {@link
+ * #setFragment(String)} to have the fragment percent-encoded for you instead, but see that
+ * method's Javadoc for its limitations.
+ *
+ *
This field is optional.
+ *
+ * @param fragment the new fragment component, or null to clear this field
+ * @return this, for fluent building
+ * @throws IllegalArgumentException if 'fragment' contains forbidden characters
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawFragment(@Nullable String fragment) {
+ if (fragment != null) {
+ checkPercentEncodedArg(fragment, "fragment", fragmentChars);
+ }
+ this.fragment = fragment;
+ return this;
+ }
+
+ /**
+ * Set the "user info" component of the new URI, e.g. "username:password", not including the
+ * trailing '@' character.
+ *
+ *
User info can contain any string of codepoints. Codepoints that can't be encoded literally
+ * will be percent-encoded for you as UTF-8.
+ *
+ *
This field is optional.
+ *
+ * @param userInfo the new "user info" component, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setUserInfo(@Nullable String userInfo) {
+ this.userInfo = percentEncode(userInfo, userInfoChars);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawUserInfo(String userInfo) {
+ checkPercentEncodedArg(userInfo, "userInfo", userInfoChars);
+ this.userInfo = userInfo;
+ return this;
+ }
+
+ /**
+ * Specifies the "host" component of the new URI in its "registered name" form (usually DNS),
+ * e.g. "server.com".
+ *
+ *
The registered name can contain any string of codepoints. Codepoints that can't be encoded
+ * literally will be percent-encoded for you as UTF-8.
+ *
+ *
This field is optional.
+ *
+ * @param regName the new host component in "registered name" form, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setHost(@Nullable String regName) {
+ if (regName != null) {
+ regName = regName.toLowerCase(Locale.ROOT);
+ regName = percentEncode(regName, regNameChars);
+ }
+ this.host = regName;
+ return this;
+ }
+
+ /**
+ * Specifies the "host" component of the new URI as an IP address.
+ *
+ *
This field is optional.
+ *
+ * @param addr the new "host" component in InetAddress form, or null to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setHost(@Nullable InetAddress addr) {
+ this.host = addr != null ? toUriString(addr) : null;
+ return this;
+ }
+
+ private static String toUriString(InetAddress addr) {
+ // InetAddresses.toUriString(addr) is almost enough but neglects RFC 6874 percent encoding.
+ String inetAddrStr = InetAddresses.toUriString(addr);
+ int percentIndex = inetAddrStr.indexOf('%');
+ if (percentIndex < 0) {
+ return inetAddrStr;
+ }
+
+ String scope = inetAddrStr.substring(percentIndex, inetAddrStr.length() - 1);
+ return inetAddrStr.substring(0, percentIndex) + percentEncode(scope, unreservedChars) + "]";
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawHost(String host) {
+ if (host.startsWith("[") && host.endsWith("]")) {
+ // IP-literal: Guava's isUriInetAddress() is almost enough but it doesn't check the scope.
+ int percentIndex = host.indexOf('%');
+ if (percentIndex > 0) {
+ String scope = host.substring(percentIndex, host.length() - 1);
+ checkPercentEncodedArg(scope, "scope", unreservedChars);
+ }
+ }
+ // IP-literal validation is complicated so we delegate it to Guava. We use this particular
+ // method of InetAddresses because it doesn't try to match interfaces on the local machine.
+ // (The validity of a URI should be the same no matter which machine does the parsing.)
+ // TODO(jdcormie): IPFuture
+ if (!InetAddresses.isUriInetAddress(host)) {
+ // Must be a "registered name".
+ checkPercentEncodedArg(host, "host", regNameChars);
+ }
+ this.host = host;
+ return this;
+ }
+
+ /**
+ * Specifies the "port" component of the new URI, e.g. "8080".
+ *
+ *
The port can be any non-negative integer. A negative value represents "no port".
+ *
+ *
This field is optional.
+ *
+ * @param port the new "port" component, or -1 to clear this field
+ * @return this, for fluent building
+ */
+ @CanIgnoreReturnValue
+ public Builder setPort(int port) {
+ this.port = port < 0 ? null : Integer.toString(port);
+ return this;
+ }
+
+ @CanIgnoreReturnValue
+ Builder setRawPort(String port) {
+ if (port != null && !port.isEmpty()) {
+ try {
+ Integer.parseInt(port); // Result unused.
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid port", e);
+ }
+ }
+ this.port = port;
+ return this;
+ }
+
+ /**
+ * Specifies the userinfo, host and port URI components all at once using a single string.
+ *
+ *
This setter is "raw" in the sense that special characters in userinfo and host must be
+ * passed in percent-encoded. See RFC 3986 3.2 for the set
+ * of characters allowed in each component of an authority.
+ *
+ *
There's no "cooked" method to set authority like for other URI components because
+ * authority is a *compound* URI component whose userinfo, host and port components are
+ * delimited with special characters '@' and ':'. But the first two of those components can
+ * themselves contain these delimiters so we need percent-encoding to parse them unambiguously.
+ *
+ * @param authority an RFC 3986 authority string that will be used to set userinfo, host and
+ * port, or null to clear all three of those components
+ */
+ @CanIgnoreReturnValue
+ public Builder setRawAuthority(@Nullable String authority) {
+ if (authority == null) {
+ setUserInfo(null);
+ setHost((String) null);
+ setPort(-1);
+ } else {
+ // UserInfo. Easy because '@' cannot appear unencoded inside userinfo or host.
+ int userInfoEnd = authority.indexOf('@');
+ if (userInfoEnd >= 0) {
+ setRawUserInfo(authority.substring(0, userInfoEnd));
+ } else {
+ setUserInfo(null);
+ }
+
+ // Host/Port.
+ int hostStart = userInfoEnd >= 0 ? userInfoEnd + 1 : 0;
+ int portStartColon = findPortStartColon(authority, hostStart);
+ if (portStartColon < 0) {
+ setRawHost(authority.substring(hostStart));
+ setPort(-1);
+ } else {
+ setRawHost(authority.substring(hostStart, portStartColon));
+ setRawPort(authority.substring(portStartColon + 1));
+ }
+ }
+ return this;
+ }
+
+ /** Builds a new instance of {@link Uri} as specified by the setters. */
+ public Uri build() {
+ checkState(scheme != null, "Missing required scheme.");
+ if (host == null) {
+ checkState(port == null, "Cannot set port without host.");
+ checkState(userInfo == null, "Cannot set userInfo without host.");
+ }
+ return new Uri(this);
+ }
+ }
+
+ /**
+ * Decodes a string of characters in the range [U+0000, U+007F] to bytes.
+ *
+ *
Each percent-encoded sequence (e.g. "%F0" or "%2a", as defined by RFC 3986 2.1) is decoded
+ * to the octet it encodes. Other characters are decoded to their code point's single byte value.
+ * A literal % character must be encoded as %25.
+ *
+ * @throws IllegalArgumentException if 's' contains characters out of range or invalid percent
+ * encoding sequences.
+ */
+ public static ByteBuffer percentDecode(CharSequence s) {
+ // This is large enough because each input character needs *at most* one byte of output.
+ ByteBuffer outBuf = ByteBuffer.allocate(s.length());
+ percentDecode(s, "input", null, outBuf);
+ outBuf.flip();
+ return outBuf;
+ }
+
+ private static void percentDecode(
+ CharSequence s, String what, BitSet allowedChars, ByteBuffer outBuf) {
+ for (int i = 0; i < s.length(); i++) {
+ char c = s.charAt(i);
+ if (c == '%') {
+ if (i + 2 >= s.length()) {
+ throw new IllegalArgumentException(
+ "Invalid percent-encoding at index " + i + " of " + what + ": " + s);
+ }
+ int h1 = Character.digit(s.charAt(i + 1), 16);
+ int h2 = Character.digit(s.charAt(i + 2), 16);
+ if (h1 == -1 || h2 == -1) {
+ throw new IllegalArgumentException(
+ "Invalid hex digit in " + what + " at index " + i + " of: " + s);
+ }
+ if (outBuf != null) {
+ outBuf.put((byte) (h1 << 4 | h2));
+ }
+ i += 2;
+ } else if (allowedChars == null || allowedChars.get(c)) {
+ if (outBuf != null) {
+ outBuf.put((byte) c);
+ }
+ } else {
+ throw new IllegalArgumentException("Invalid character in " + what + " at index " + i);
+ }
+ }
+ }
+
+ @Nullable
+ private static String percentDecodeAssumedUtf8(@Nullable String s) {
+ if (s == null || s.indexOf('%') == -1) {
+ return s;
+ }
+
+ ByteBuffer utf8Bytes = percentDecode(s);
+ try {
+ return StandardCharsets.UTF_8
+ .newDecoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .onUnmappableCharacter(CodingErrorAction.REPLACE)
+ .decode(utf8Bytes)
+ .toString();
+ } catch (CharacterCodingException e) {
+ throw new VerifyException(e); // Should not happen in REPLACE mode.
+ }
+ }
+
+ @Nullable
+ private static String percentEncode(String s, BitSet allowedCodePoints) {
+ if (s == null) {
+ return null;
+ }
+ CharsetEncoder encoder =
+ StandardCharsets.UTF_8
+ .newEncoder()
+ .onMalformedInput(CodingErrorAction.REPORT)
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+ ByteBuffer utf8Bytes;
+ try {
+ utf8Bytes = encoder.encode(CharBuffer.wrap(s));
+ } catch (MalformedInputException e) {
+ throw new IllegalArgumentException("Malformed input", e); // Must be a broken surrogate pair.
+ } catch (CharacterCodingException e) {
+ throw new VerifyException(e); // Should not happen when encoding to UTF-8.
+ }
+
+ StringBuilder sb = new StringBuilder();
+ while (utf8Bytes.hasRemaining()) {
+ int b = 0xff & utf8Bytes.get();
+ if (allowedCodePoints.get(b)) {
+ sb.append((char) b);
+ } else {
+ sb.append('%');
+ sb.append(hexDigitsByVal[(b & 0xF0) >> 4]);
+ sb.append(hexDigitsByVal[b & 0x0F]);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static void checkPercentEncodedArg(String s, String what, BitSet allowedChars) {
+ percentDecode(s, what, allowedChars, null);
+ }
+
+ // See UriTest for how these were computed from the ABNF constants in RFC 3986.
+ static final BitSet digitChars = BitSet.valueOf(new long[] {0x3ff000000000000L});
+ static final BitSet alphaChars = BitSet.valueOf(new long[] {0L, 0x7fffffe07fffffeL});
+ // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+ static final BitSet schemeChars =
+ BitSet.valueOf(new long[] {0x3ff680000000000L, 0x7fffffe07fffffeL});
+ // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ static final BitSet unreservedChars =
+ BitSet.valueOf(new long[] {0x3ff600000000000L, 0x47fffffe87fffffeL});
+ // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
+ static final BitSet genDelimsChars =
+ BitSet.valueOf(new long[] {0x8400800800000000L, 0x28000001L});
+ // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
+ static final BitSet subDelimsChars = BitSet.valueOf(new long[] {0x28001fd200000000L});
+ // reserved = gen-delims / sub-delims
+ static final BitSet reservedChars = BitSet.valueOf(new long[] {0xac009fda00000000L, 0x28000001L});
+ // reg-name = *( unreserved / pct-encoded / sub-delims )
+ static final BitSet regNameChars =
+ BitSet.valueOf(new long[] {0x2bff7fd200000000L, 0x47fffffe87fffffeL});
+ // userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
+ static final BitSet userInfoChars =
+ BitSet.valueOf(new long[] {0x2fff7fd200000000L, 0x47fffffe87fffffeL});
+ // pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
+ static final BitSet pChars =
+ BitSet.valueOf(new long[] {0x2fff7fd200000000L, 0x47fffffe87ffffffL});
+ static final BitSet pCharsAndSlash =
+ BitSet.valueOf(new long[] {0x2fffffd200000000L, 0x47fffffe87ffffffL});
+ // query = *( pchar / "/" / "?" )
+ static final BitSet queryChars =
+ BitSet.valueOf(new long[] {0xafffffd200000000L, 0x47fffffe87ffffffL});
+ // fragment = *( pchar / "/" / "?" )
+ static final BitSet fragmentChars = queryChars;
+
+ private static final char[] hexDigitsByVal = "0123456789ABCDEF".toCharArray();
+}
diff --git a/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java b/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java
new file mode 100644
index 00000000000..6620a7d413a
--- /dev/null
+++ b/api/src/test/java/io/grpc/HttpConnectProxiedSocketAddressTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2025 The gRPC Authors
+ *
+ * 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
+ *
+ * http://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.
+ */
+
+package io.grpc;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.testing.EqualsTester;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class HttpConnectProxiedSocketAddressTest {
+
+ private final InetSocketAddress proxyAddress =
+ new InetSocketAddress(InetAddress.getLoopbackAddress(), 8080);
+ private final InetSocketAddress targetAddress =
+ InetSocketAddress.createUnresolved("example.com", 443);
+
+ @Test
+ public void buildWithAllFields() {
+ Map headers = new HashMap<>();
+ headers.put("X-Custom-Header", "custom-value");
+ headers.put("Proxy-Authorization", "Bearer token");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .setUsername("user")
+ .setPassword("pass")
+ .build();
+
+ assertThat(address.getProxyAddress()).isEqualTo(proxyAddress);
+ assertThat(address.getTargetAddress()).isEqualTo(targetAddress);
+ assertThat(address.getHeaders()).hasSize(2);
+ assertThat(address.getHeaders()).containsEntry("X-Custom-Header", "custom-value");
+ assertThat(address.getHeaders()).containsEntry("Proxy-Authorization", "Bearer token");
+ assertThat(address.getUsername()).isEqualTo("user");
+ assertThat(address.getPassword()).isEqualTo("pass");
+ }
+
+ @Test
+ public void buildWithoutOptionalFields() {
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .build();
+
+ assertThat(address.getProxyAddress()).isEqualTo(proxyAddress);
+ assertThat(address.getTargetAddress()).isEqualTo(targetAddress);
+ assertThat(address.getHeaders()).isEmpty();
+ assertThat(address.getUsername()).isNull();
+ assertThat(address.getPassword()).isNull();
+ }
+
+ @Test
+ public void buildWithEmptyHeaders() {
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(Collections.emptyMap())
+ .build();
+
+ assertThat(address.getHeaders()).isEmpty();
+ }
+
+ @Test
+ public void headersAreImmutable() {
+ Map headers = new HashMap<>();
+ headers.put("key1", "value1");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .build();
+
+ headers.put("key2", "value2");
+
+ assertThat(address.getHeaders()).hasSize(1);
+ assertThat(address.getHeaders()).containsEntry("key1", "value1");
+ assertThat(address.getHeaders()).doesNotContainKey("key2");
+ }
+
+ @Test
+ public void returnedHeadersAreUnmodifiable() {
+ Map headers = new HashMap<>();
+ headers.put("key", "value");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .build();
+
+ assertThrows(UnsupportedOperationException.class,
+ () -> address.getHeaders().put("newKey", "newValue"));
+ }
+
+ @Test
+ public void nullHeadersThrowsException() {
+ assertThrows(NullPointerException.class,
+ () -> HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(null)
+ .build());
+ }
+
+ @Test
+ public void equalsAndHashCode() {
+ Map headers1 = new HashMap<>();
+ headers1.put("header", "value");
+
+ Map headers2 = new HashMap<>();
+ headers2.put("header", "value");
+
+ Map differentHeaders = new HashMap<>();
+ differentHeaders.put("different", "header");
+
+ new EqualsTester()
+ .addEqualityGroup(
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers1)
+ .setUsername("user")
+ .setPassword("pass")
+ .build(),
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers2)
+ .setUsername("user")
+ .setPassword("pass")
+ .build())
+ .addEqualityGroup(
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(differentHeaders)
+ .setUsername("user")
+ .setPassword("pass")
+ .build())
+ .addEqualityGroup(
+ HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .build())
+ .testEquals();
+ }
+
+ @Test
+ public void toStringContainsHeaders() {
+ Map headers = new HashMap<>();
+ headers.put("X-Test", "test-value");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .setUsername("user")
+ .setPassword("secret")
+ .build();
+
+ String toString = address.toString();
+ assertThat(toString).contains("headers");
+ assertThat(toString).contains("X-Test");
+ assertThat(toString).contains("hasPassword=true");
+ assertThat(toString).doesNotContain("secret");
+ }
+
+ @Test
+ public void toStringWithoutPassword() {
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .build();
+
+ String toString = address.toString();
+ assertThat(toString).contains("hasPassword=false");
+ }
+
+ @Test
+ public void hashCodeDependsOnHeaders() {
+ Map headers1 = new HashMap<>();
+ headers1.put("header", "value1");
+
+ Map headers2 = new HashMap<>();
+ headers2.put("header", "value2");
+
+ HttpConnectProxiedSocketAddress address1 = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers1)
+ .build();
+
+ HttpConnectProxiedSocketAddress address2 = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers2)
+ .build();
+
+ assertNotEquals(address1.hashCode(), address2.hashCode());
+ }
+
+ @Test
+ public void multipleHeadersSupported() {
+ Map headers = new HashMap<>();
+ headers.put("X-Header-1", "value1");
+ headers.put("X-Header-2", "value2");
+ headers.put("X-Header-3", "value3");
+
+ HttpConnectProxiedSocketAddress address = HttpConnectProxiedSocketAddress.newBuilder()
+ .setProxyAddress(proxyAddress)
+ .setTargetAddress(targetAddress)
+ .setHeaders(headers)
+ .build();
+
+ assertThat(address.getHeaders()).hasSize(3);
+ assertThat(address.getHeaders()).containsEntry("X-Header-1", "value1");
+ assertThat(address.getHeaders()).containsEntry("X-Header-2", "value2");
+ assertThat(address.getHeaders()).containsEntry("X-Header-3", "value3");
+ }
+}
+
diff --git a/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java b/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
index 5b348b7adab..690db6622e0 100644
--- a/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
+++ b/api/src/test/java/io/grpc/LoadBalancerRegistryTest.java
@@ -40,7 +40,7 @@ public void getClassesViaHardcoded_classesPresent() throws Exception {
@Test
public void stockProviders() {
LoadBalancerRegistry defaultRegistry = LoadBalancerRegistry.getDefaultRegistry();
- assertThat(defaultRegistry.providers()).hasSize(3);
+ assertThat(defaultRegistry.providers()).hasSize(4);
LoadBalancerProvider pickFirst = defaultRegistry.getProvider("pick_first");
assertThat(pickFirst).isInstanceOf(PickFirstLoadBalancerProvider.class);
@@ -56,6 +56,12 @@ public void stockProviders() {
assertThat(outlierDetection.getClass().getName()).isEqualTo(
"io.grpc.util.OutlierDetectionLoadBalancerProvider");
assertThat(roundRobin.getPriority()).isEqualTo(5);
+
+ LoadBalancerProvider randomSubsetting = defaultRegistry.getProvider(
+ "random_subsetting_experimental");
+ assertThat(randomSubsetting.getClass().getName()).isEqualTo(
+ "io.grpc.util.RandomSubsettingLoadBalancerProvider");
+ assertThat(randomSubsetting.getPriority()).isEqualTo(5);
}
@Test
diff --git a/api/src/test/java/io/grpc/LoadBalancerTest.java b/api/src/test/java/io/grpc/LoadBalancerTest.java
index 5e9e5cbe816..22fdc220081 100644
--- a/api/src/test/java/io/grpc/LoadBalancerTest.java
+++ b/api/src/test/java/io/grpc/LoadBalancerTest.java
@@ -64,6 +64,26 @@ public void pickResult_withSubchannelAndTracer() {
assertThat(result.isDrop()).isFalse();
}
+ @Test
+ public void pickResult_withSubchannelReplacement() {
+ PickResult result = PickResult.withSubchannel(subchannel, tracerFactory)
+ .copyWithSubchannel(subchannel2);
+ assertThat(result.getSubchannel()).isSameInstanceAs(subchannel2);
+ assertThat(result.getStatus()).isSameInstanceAs(Status.OK);
+ assertThat(result.getStreamTracerFactory()).isSameInstanceAs(tracerFactory);
+ assertThat(result.isDrop()).isFalse();
+ }
+
+ @Test
+ public void pickResult_withStreamTracerFactory() {
+ PickResult result = PickResult.withSubchannel(subchannel)
+ .copyWithStreamTracerFactory(tracerFactory);
+ assertThat(result.getSubchannel()).isSameInstanceAs(subchannel);
+ assertThat(result.getStatus()).isSameInstanceAs(Status.OK);
+ assertThat(result.getStreamTracerFactory()).isSameInstanceAs(tracerFactory);
+ assertThat(result.isDrop()).isFalse();
+ }
+
@Test
public void pickResult_withNoResult() {
PickResult result = PickResult.withNoResult();
diff --git a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java
index 30de2477d77..7b02459290e 100644
--- a/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java
+++ b/api/src/test/java/io/grpc/ManagedChannelRegistryTest.java
@@ -20,17 +20,23 @@
import static org.junit.Assert.fail;
import com.google.common.collect.ImmutableSet;
+import io.grpc.FlagResetRule;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
/** Unit tests for {@link ManagedChannelRegistry}. */
-@RunWith(JUnit4.class)
+@RunWith(Parameterized.class)
public class ManagedChannelRegistryTest {
private String target = "testing123";
private ChannelCredentials creds = new ChannelCredentials() {
@@ -40,6 +46,20 @@ public ChannelCredentials withoutBearerTokens() {
}
};
+ @Rule public final FlagResetRule flagResetRule = new FlagResetRule();
+
+ @Parameters(name = "enableRfc3986UrisParam={0}")
+ public static Iterable