From 59992c4e7644d939ac0bf9a665c611de7deb412d Mon Sep 17 00:00:00 2001 From: John Cormie Date: Fri, 8 May 2026 16:21:54 -0700 Subject: [PATCH] api: Make io.grpc.Uri's setRawFragment public with warnings --- api/src/main/java/io/grpc/Uri.java | 29 +++++++++++++++++-- api/src/test/java/io/grpc/UriTest.java | 40 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/grpc/Uri.java b/api/src/main/java/io/grpc/Uri.java index 1d824759846..a88bb6138d8 100644 --- a/api/src/main/java/io/grpc/Uri.java +++ b/api/src/main/java/io/grpc/Uri.java @@ -821,6 +821,13 @@ public Builder setRawQuery(@Nullable String query) { *

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 @@ -832,9 +839,27 @@ public Builder setFragment(@Nullable String fragment) { 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 - Builder setRawFragment(String fragment) { - checkPercentEncodedArg(fragment, "fragment", fragmentChars); + public Builder setRawFragment(@Nullable String fragment) { + if (fragment != null) { + checkPercentEncodedArg(fragment, "fragment", fragmentChars); + } this.fragment = fragment; return this; } diff --git a/api/src/test/java/io/grpc/UriTest.java b/api/src/test/java/io/grpc/UriTest.java index 71ec1749b7d..0de7ef0fc10 100644 --- a/api/src/test/java/io/grpc/UriTest.java +++ b/api/src/test/java/io/grpc/UriTest.java @@ -643,6 +643,46 @@ public void builder_setRawQuery_null() { assertThat(uri.toString()).isEqualTo("http://host"); } + @Test + public void builder_setRawFragment() { + Uri uri = Uri.newBuilder().setScheme("http").setHost("host").setRawFragment("a%20b").build(); + assertThat(uri.getRawFragment()).isEqualTo("a%20b"); + assertThat(uri.getFragment()).isEqualTo("a b"); + assertThat(uri.toString()).isEqualTo("http://host#a%20b"); + } + + @Test + public void builder_setRawFragment_null() { + Uri uri = + Uri.newBuilder() + .setScheme("http") + .setHost("host") + .setRawFragment("a%20b") + .setRawFragment(null) + .build(); + assertThat(uri.getRawFragment()).isNull(); + assertThat(uri.getFragment()).isNull(); + assertThat(uri.toString()).isEqualTo("http://host"); + } + + @Test + public void builder_setRawFragment_invalidCharacters_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> Uri.newBuilder().setRawFragment("f[]rag")); + assertThat(e).hasMessageThat().contains("Invalid character in fragment"); + } + + @Test + public void builder_setRawFragment_invalidPercentEncoding_throws() { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> Uri.newBuilder().setRawFragment("f%XXragment")); + assertThat(e).hasMessageThat().contains("Invalid"); + } + @Test public void builder_canClearAuthorityComponents() { Uri uri = Uri.create("s://user@host:80/path").toBuilder().setRawAuthority(null).build();