grpc-java icon indicating copy to clipboard operation
grpc-java copied to clipboard

Support for opaque "target" URIs with empty scheme-specific part per RFC 3986

Open jdcormie opened this issue 5 months ago • 7 comments

grpc-java appears to interpret "targetUri"s using the original RFC 2396 URI syntax from 1998. Unfortunately this means that it fails to parse the output of toUri(URI_INTENT_SCHEME) for many Android Intents, in particular, any Intent without a data URI. These are very common and encode all the non-URI fields in a fragment, like:

intent:#Intent;package=p;action=a;category=c;end;

ManagedChannelImplBuilder fails to pass a target string like this to the NameResolverProvider registered for "intent:" because java.net.URI() throws java.net.URISyntaxException: "Expected scheme-specific part at index 7"

In 2005, RFC 3986 revised the URI syntax to allow an empty scheme-specific part (among other relaxations). It would be nice if grpc-java could tolerate these too.

jdcormie avatar Jul 27 '25 05:07 jdcormie

@ejona86 Switching off of java.net.URI seems infeasible. Any ideas on how this could be fixed anyway, even if just for the special case of intent:#Intent;?

jdcormie avatar Jul 30 '25 16:07 jdcormie

@jdcormie, could you run a quick test on Android to see if URI behaves the same way as OpenJDK? (Edit: This is to help answer the question, "how is intent parsing handled on Android")

ejona86 avatar Jul 30 '25 16:07 ejona86

Sure. First note that Android has its own android.net.Uri class.

android.net.Uri#parse claims to implement RFC 2396 but it parses intent:#Intent;action=action1;end; as an opaque URI with a fragment and an SSP of the empty string.

android.content.Intent#toUri() returns String and #parseUri() accepts a String.

The following BinderChannelSmokeTest test case fails on an emulated device (:grpc-binder:connectedCheck):

  @Test
  public void testConnectViaTargetUri() throws Exception {
    // Compare with the <intent-filter> mapping in AndroidManifest.xml.
    channel =
        BinderChannelBuilder.forTarget("intent:#Intent;action=action1;end;", appContext).build();
    assertThat(doCall("Hello").get()).isEqualTo("Hello");
  }

with:

java.lang.IllegalArgumentException: Address types of NameResolver 'dns' for 'intent:#Intent;action=action1;end;' not supported by transport
at io.grpc.internal.ManagedChannelImplBuilder.getNameResolverProvider(ManagedChannelImplBuilder.java:869)
at io.grpc.internal.ManagedChannelImplBuilder.build(ManagedChannelImplBuilder.java:721)
at io.grpc.ForwardingChannelBuilder2.build(ForwardingChannelBuilder2.java:278)
at io.grpc.binder.BinderChannelBuilder.build(BinderChannelBuilder.java:328)
at io.grpc.binder.BinderChannelSmokeTest.testConnectViaTargetUri(BinderChannelSmokeTest.java:236)

because ManagedChannelImplBuilder fails to parse the target String using Android's implementation of java.net.URI and fails to match it against URI_PATTERN, concluding it is not a URI but a bare authority string.

jdcormie avatar Jul 30 '25 21:07 jdcormie

I considered a horrible hack of using reflection to set java.net.URI's private schemeSpecificPart field to "" post construction. Unfortunately/thankfully this can't work because Android's ART blocks access to this non-SDK element: https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces

jdcormie avatar Aug 12 '25 20:08 jdcormie

The only option I'm seeing, other than having a bespoke URI parser, is for the gRPC core to manually parse out the scheme and pass a string to the NameResolver which it would then need to parse. Unfortunately, that allows invalid URIs as target strings, unless the NameResolver verifies it. I would totally expect there would be NameResolvers that abuse such an ability.

ejona86 avatar Aug 12 '25 21:08 ejona86

I also think there's a class of hacks that keep the current API but use special sentinel value for things that java.net.URI can't represent. In one flavor of this hack, Grpc.java defines public static final EMPTY_SSP_SENTINEL = "__SOMETHING_UNLIKELY__"; If ManagedChannelImplBuilder receives a target string that won't parse according to RFC 2396, it could do a simple test for the empty SSP problem ([a-zA-Z][a-zA-Z0-9+.-]*:#.* and work around it by swapping in the sentinel SSP, i.e. turning intent:#fragment to intent:__SOMETHING_UNLIKELY__#fragment, again just using simple String operations that don't need much parsing. A NameResolver could override some new hasSupport() method to advertise its understanding of sentinels. If so, it would receive a java.net.URI with an understanding that an SSP equal to EMPTY_SSP_SENTINEL should be treated like "". IntentNameResolver could easily handle this case just by calling Intent.parseURI() like normal then nulling out the SSP in the resulting Intent's data URI.

So so ugly though.

jdcormie avatar Aug 12 '25 23:08 jdcormie

Another idea: let users avoid the problem by adding support for android-app: URIs (mbrophy's old preference). These are always hierarchical and so java.net.URI is happy. We need just one tiny hack which is interpreting the "localhost" authority in a non-standard way to permit service discovery. It could look like https://github.com/grpc/grpc-java/pull/12273. I'll update the design doc to consider this tomorrow.

jdcormie avatar Aug 13 '25 08:08 jdcormie