awslabs / aws-mobile-appsync-sdk-android

Android SDK for AWS AppSync.
https://docs.amplify.aws/sdk/api/graphql/q/platform/android/
Apache License 2.0
105 stars 58 forks source link

Subscriptions don't work in 3.0.x; NullPointerException on AWSConfiguration object #257

Closed rootko closed 2 years ago

rootko commented 4 years ago

Describe the bug Hi, in new Android version 3.0.x there's a new piece of code in AWSAppSyncClient constructor that expects that configuration is not filled programmatically via AWSAppSyncClient.Builder, but provided in awsconfiguration.json file:

SubscriptionAuthorizer subscriptionAuthorizer = new SubscriptionAuthorizer(
    builder.mAwsConfiguration,
    builder.mOidcAuthProvider,
    applicationContext
);

Therefore each subscription fails, unless we provide aforementioned awsconfiguration.json file. Since we use more environments for various stages of deployment and tests, and the configuration is already provided via Builder, this is clearly an oversight. I'd expect a check for builder.mAwsConfiguration != null and in case it's null, take the configuration from the builder instance instead.

As a workaround I probably could use a constructor public AWSConfiguration(JSONObject jsonObject) to provide json file from Builder properties and use it to populate mAwsConfiguration variable until this is solved.

The AWSAppSyncClient constructor ends up with the exception:

java.lang.NullPointerException: Attempt to invoke virtual method 'org.json.JSONObject com.amazonaws.mobile.config.AWSConfiguration.optJsonObject(java.lang.String)' on a null object reference
        at com.amazonaws.mobileconnectors.appsync.SubscriptionAuthorizer.getAuthorizationDetails(SubscriptionAuthorizer.java:66)
        at com.amazonaws.mobileconnectors.appsync.SubscriptionAuthorizer.getConnectionAuthorizationDetails(SubscriptionAuthorizer.java:55)
        at com.amazonaws.mobileconnectors.appsync.WebSocketConnectionManager.getConnectionRequestUrl(WebSocketConnectionManager.java:254)
        at com.amazonaws.mobileconnectors.appsync.WebSocketConnectionManager.createWebSocket(WebSocketConnectionManager.java:109)
        at com.amazonaws.mobileconnectors.appsync.WebSocketConnectionManager.requestSubscription(WebSocketConnectionManager.java:78)
        at com.amazonaws.mobileconnectors.appsync.AppSyncWebSocketSubscriptionCall.execute(AppSyncWebSocketSubscriptionCall.java:48)
        at com.masternaut.jobs.core.DataHolderJobs.subscribeDriver(DataHolderJobs.java:213)
        at com.masternaut.login.ui.fragment.LoginFragment.queueFinished(LoginFragment.java:335)
        at com.masternaut.smarterdriver.core.APICallManager.next(APICallManager.java:180)
        at com.masternaut.smarterdriver.core.APICallManager.onFinished(APICallManager.java:72)
        at com.masternaut.smarterdriver.core.APICallManager$1.onResponseData(APICallManager.java:380)
        at com.masternaut.jobs.core.ListActiveJobsForDriverTokenCallback.onResponse(ListActiveJobsForDriverTokenCallback.java:61)
        at com.apollographql.apollo.internal.RealAppSyncCall$1.onResponse(RealAppSyncCall.java:278)
        at com.apollographql.apollo.internal.fetcher.NetworkFirstFetcher$NetworkFirstInterceptor$1.onResponse(NetworkFirstFetcher.java:55)
        at com.apollographql.apollo.internal.interceptor.ApolloCacheInterceptor$1$1.onResponse(ApolloCacheInterceptor.java:102)
        at com.apollographql.apollo.internal.interceptor.ApolloParseInterceptor$1.onResponse(ApolloParseInterceptor.java:84)
        at com.apollographql.apollo.internal.interceptor.ApolloServerInterceptor$1$1.onResponse(ApolloServerInterceptor.java:110)
        at okhttp3.RealCall$AsyncCall.execute(RealCall.java:141)
        at okhttp3.internal.NamedRunnable.run(NamedRunnable.java:32)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1162)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:636)
        at java.lang.Thread.run(Thread.java:784)

To Reproduce Steps to reproduce the behavior:

  1. create client in such manner, that configuration is provided via Builder, not awsconfiguration.json file.
            client = AWSAppSyncClient.builder()
                    .context(context)
                    .credentialsProvider(credProvider)
                    .region(Regions.EU_WEST_1)
                    .serverUrl(env)
                    .persistentMutationsCallback(persistentCallback)
                    .s3ObjectManager(getS3ObjectManager(credProvider))
                    .subscriptionsAutoReconnect(true)
                    .okHttpClient(getOkHttpClient(context))
                    .subscriptionsAutoReconnect(true)
                    .build();

Expected behavior Subscriptions should work without awsconfiguration.json, if the configuration is provided programmatically in Builder.

Environment(please complete the following information):

    classpath 'com.amazonaws:aws-android-sdk-appsync-gradle-plugin:2.8.3'
    'com.amazonaws:aws-android-sdk-appsync:3.0.1'
    'com.amazonaws:aws-android-sdk-s3:2.16.7'
    'com.amazonaws:aws-android-sdk-cognitoidentityprovider:2.16.7'

Device Information (please complete the following information): Any and all devices

Additional context works with 'com.amazonaws:aws-android-sdk-appsync:2.10.1'

rootko commented 4 years ago

I've created a helper method that populates JSONObject with correct json format (as is expected from awsconfiguration.json) and use this object in AWSAppSyncClient.Builder like this: AWSAppSyncClient.builder().awsConfiguration(new AWSConfiguration(buildJsonConfig())) subscriptions are working again. So this workaround is working, it can be easily implemented to the official SDK.

syriail commented 4 years ago

I have the same problem. any Fix?

rootko commented 4 years ago

As mentioned above, you have to write your own wrapper that will populate theAWSConfiguration:

           client = AWSAppSyncClient.builder()
                    .context(context)
                    .credentialsProvider(credProvider)
                    .region(Regions.EU_WEST_1)
                    .serverUrl(Utils.getAppsyncApiUrl(env))
                    .persistentMutationsCallback(persistentCallback)
                    .s3ObjectManager(getS3ObjectManager(credProvider))
                    .subscriptionsAutoReconnect(true)
                    .okHttpClient(getOkHttpClient(context))
                    .subscriptionsAutoReconnect(true)
                    .awsConfiguration(new AWSConfiguration(buildJsonConfig(credProvider, region.getName(), identityId, env)))
                    .build();

I did a helper method like this:

private JSONObject buildJsonConfig(CognitoCachingCredentialsProvider credProvider, String region, String identityId, Utils.Env env) {
    JSONObject jsonConfig = new JSONObject();
    try {
        jsonConfig.put("UserAgent", "aws-amplify-cli/0.1.0");
        jsonConfig.put("Version", "1.0");
        JSONObject identityManager = new JSONObject();
        JSONObject identityManagerDefault = new JSONObject();
        identityManager.put("Default", identityManagerDefault);
        jsonConfig.put("IdentityManager", identityManager);

        JSONObject appSync = new JSONObject();
        JSONObject appSyncDefault = new JSONObject();
        appSyncDefault.put("ApiUrl", Utils.getAppsyncApiUrl(env));
        appSyncDefault.put("Region", region);
        appSyncDefault.put("AuthMode", "AWS_IAM");
        appSyncDefault.put("ClientDatabasePrefix", env);
        appSync.put("Default", appSyncDefault);
        jsonConfig.put("AppSync", appSync);

        JSONObject cognitoUserPool = new JSONObject();
        JSONObject cognitoUserPoolDefault = new JSONObject();
        cognitoUserPoolDefault.put("PoolId", identityId);
        cognitoUserPoolDefault.put("AppClientId", credProvider.getCredentials().getAWSAccessKeyId());
        cognitoUserPoolDefault.put("AppClientSecret", credProvider.getCredentials().getAWSSecretKey());
        cognitoUserPoolDefault.put("Region", region);
        cognitoUserPool.put("Default", cognitoUserPoolDefault);
        jsonConfig.put("CognitoUserPool", cognitoUserPool);

        JSONObject credentialsProvider = new JSONObject();
        JSONObject cognitoIdentity = new JSONObject();
        JSONObject cognitoIdentityDefault = new JSONObject();
        cognitoIdentityDefault.put("PoolId", Utils.getAppsyncDeveloperProviderIdentityPoolId(env));
        cognitoIdentityDefault.put("Region", region);
        cognitoIdentity.put("Default", cognitoIdentityDefault);
        credentialsProvider.put("CognitoIdentity", cognitoIdentity);
        jsonConfig.put("CredentialsProvider", credentialsProvider);
    } catch (Exception e) {
        Logger.e(e);
    }
    return jsonConfig;
}

Just be aware that credProvider.getCredentials().getAWSAccessKeyId() is a network call, so you have to create AWSAppSyncClient on background thread, otherwise you'll end up with an NetworkOnMainThreadException.

mslagle-godaddy commented 4 years ago

The SubscriptionAuthorizer (especially SubscriptionAuthorizer. getAuthorizationDetailsForIAM) ignores fields set on the AppSyncClient builder almost entirely, and relies on the AWSConfiguration information regardless of what has been set on the builder.

It also ignores any CredentialsProvider given to the builder and uses its own CognitoCachingCredentialsProvider -- which appears to be the reason this error is occurring in the first place. If it didn't ignore the credentials provider the Builder is given, we wouldn't need the PoolId to create a CognitoCachingCredentialsProvider... which (I don't believe) is required anyway -- it can use any AWSCredentialsProvider, which means the PoolId isn't needed.

I'm trying to create a PR to fix this issue, but I can't seem to include this project locally for some reason... has anyone been able to clone this and use a local version in their project?

mslagle-godaddy commented 4 years ago

@awslabs any idea if / when this is going to be fixed?

mslagle-godaddy commented 4 years ago

@jamesonwilliams @awslabs @rootko PR to fix this issue here: https://github.com/awslabs/aws-mobile-appsync-sdk-android/pull/276/files

rootko commented 4 years ago

@mslagle-gd a proper fix in the AWS library would be awesome, thanks for the PR ;)

ukevgen commented 3 years ago

Still doesn't work aws_appsync = '3.1.2'

Client.builder()
                    .context(applicationContext())
                    .cognitoUserPoolsAuthProvider(getUserPool())
                    .region(REGION)
                    .defaultResponseFetcher(NetworkOnlyFetcher())
                    .conflictResolver(ConflictResolver())
                    .serverUrl("url")
                    .okHttpClient(client)
                    .build()
jtn-devecto commented 3 years ago

When we can expect this will be fix? This prevents upgrading to 3.x.

poojamat commented 2 years ago

This PR was merged to fix this bug about ignoring the programmatically changes configuration. Please get the latest version.