Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NullPointerException occurring in HttpJsonSpannerStub.batchWriteCallable() when handling an error status response. #3640

Open
baeminbo opened this issue Feb 12, 2025 · 1 comment
Assignees
Labels
api: spanner Issues related to the googleapis/java-spanner API.

Comments

@baeminbo
Copy link

I'd like to report a NullPointerException in HttpJsonSpannerStub generated by gapic-generator. I understand the code is not intended for end-user use, but I'm reporting it in case others encounter similar issues

Environment details

  • OS type and version: macOS 15.3
  • Java version: 11
  • version(s): google-cloud-spanner 6.86.6

Steps to reproduce

  1. Create a Spanner table with CREATE TABLE table1 (k STRING(100), v INT64) PRIMARY KEY (k).
  2. Add a row with the key k as "k1".
  3. Run the following code which writes a row with k as "k1". This will result in an error status response from Cloud Spanner.

An exception with an appropriate error message, such as "Row [k1] in table table1 already exists," is expected. However, a NullPointerException is thrown instead.

Code example

import com.google.api.gax.rpc.ServerStream;
import com.google.api.gax.rpc.ServerStreamingCallable;
import com.google.cloud.spanner.v1.stub.SpannerStub;
import com.google.cloud.spanner.v1.stub.SpannerStubSettings;
import com.google.protobuf.ListValue;
import com.google.protobuf.Value;
import com.google.spanner.v1.*;

import java.io.IOException;

public class SpannerHttpJsonStubMain {
  public static void main(String[] args) throws IOException {
    SpannerStubSettings stubSettings = SpannerStubSettings.newHttpJsonBuilder().build();
    try (SpannerStub stub = stubSettings.createStub()) {
      Session session = stub.createSessionCallable()
          .call(CreateSessionRequest.newBuilder()
              .setDatabase("projects/<REDACTED>/instances/spanner-1/databases/db-1")
              .build());
      BatchWriteRequest request = BatchWriteRequest.newBuilder()
          .setSession(session.getName())
          .addMutationGroups(BatchWriteRequest.MutationGroup.newBuilder()
              .addMutations(Mutation.newBuilder()
                  .setInsert(Mutation.Write.newBuilder()
                      // SCHEMA: CREATE TABLE table1 (k STRING(100), v INT64) PRIMARY KEY (k)
                      .setTable("table1")
                      .addColumns("k")
                      .addColumns("v")
                      // The key "k1" already exists in the table. Therefore, this request will result in an error
                      // response.
                      .addValues(ListValue.newBuilder()
                          .addValues(Value.newBuilder().setStringValue("k1"))
                          .addValues(Value.newBuilder().setStringValue("1"))))))
          .build();

      ServerStreamingCallable<BatchWriteRequest, BatchWriteResponse> callable = stub.batchWriteCallable();

      ServerStream<BatchWriteResponse> responseStream = callable.call(request);
      for (BatchWriteResponse response : responseStream) {
        System.out.println("response = " + response);
      }
    }
  }
}

Stack trace

The code was run with the JVM option -Djava.util.logging.config.file=logging.properties to enable HTTP transport debug logging. The logging.properties file is here. The output shows that parsing DebugInfo failed because TypedRegistry was null in some location

... skipped ...

Feb 11, 2025 11:21:51 PM com.google.api.client.http.HttpResponse <init>
CONFIG: -------------- RESPONSE --------------
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
Transfer-Encoding: chunked
Server: ESF
Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
Server-Timing: gfet4t7; dur=148
Content-Encoding: gzip
Vary: Origin
Vary: X-Origin
Vary: Referer
X-XSS-Protection: 0
Date: Wed, 12 Feb 2025 07:21:51 GMT
Content-Type: application/json; charset=UTF-8

Feb 11, 2025 11:21:51 PM com.google.api.client.util.LoggingByteArrayOutputStream close
CONFIG: Total: 254 bytes
Feb 11, 2025 11:21:51 PM com.google.api.client.util.LoggingByteArrayOutputStream close
CONFIG: [{
  "indexes": [
    0
  ],
  "status": {
    "code": 6,
    "message": "Row [k1] in table table1 already exists",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.DebugInfo",
        "detail": "table1(k1)"
      }
    ]
  }
}
]
Exception in thread "main" com.google.api.gax.rpc.CancelledException: Exception in message delivery
        at com.google.api.gax.rpc.ApiExceptionFactory.createException(ApiExceptionFactory.java:48)
        at com.google.api.gax.httpjson.HttpJsonApiExceptionFactory.createApiException(HttpJsonApiExceptionFactory.java:76)
        at com.google.api.gax.httpjson.HttpJsonApiExceptionFactory.create(HttpJsonApiExceptionFactory.java:58)
        at com.google.api.gax.httpjson.HttpJsonExceptionResponseObserver.onErrorImpl(HttpJsonExceptionResponseObserver.java:82)
        at com.google.api.gax.rpc.StateCheckingResponseObserver.onError(StateCheckingResponseObserver.java:84)
        at com.google.api.gax.httpjson.HttpJsonDirectStreamController$ResponseObserverAdapter.onClose(HttpJsonDirectStreamController.java:125)
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl$OnCloseNotificationTask.call(HttpJsonClientCallImpl.java:551)
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl.notifyListeners(HttpJsonClientCallImpl.java:390)
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl.deliver(HttpJsonClientCallImpl.java:317)
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl.setResult(HttpJsonClientCallImpl.java:163)
        at com.google.api.gax.httpjson.HttpRequestRunnable.run(HttpRequestRunnable.java:148)
        at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
        at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304)
        at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
        at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
        at java.base/java.lang.Thread.run(Thread.java:1570)
        Suppressed: java.lang.RuntimeException: Asynchronous task failed
                at com.google.api.gax.rpc.ServerStreamIterator.hasNext(ServerStreamIterator.java:105)
                at SpannerHttpJsonStubMain.main(SpannerHttpJsonStubMain.java:38)
Caused by: com.google.api.gax.httpjson.HttpJsonStatusRuntimeException: Exception in message delivery
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl.deliver(HttpJsonClientCallImpl.java:367)
        ... 8 more
Caused by: com.google.api.gax.httpjson.RestSerializationException: Failed to parse response message
        at com.google.api.gax.httpjson.ProtoRestSerializer.fromJson(ProtoRestSerializer.java:107)
        at com.google.api.gax.httpjson.ProtoMessageResponseParser.parse(ProtoMessageResponseParser.java:76)
        at com.google.api.gax.httpjson.ProtoMessageResponseParser.parse(ProtoMessageResponseParser.java:41)
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl.consumeMessageFromStream(HttpJsonClientCallImpl.java:430)
        at com.google.api.gax.httpjson.HttpJsonClientCallImpl.deliver(HttpJsonClientCallImpl.java:362)
        ... 8 more
Caused by: com.google.protobuf.InvalidProtocolBufferException: Cannot invoke "com.google.protobuf.TypeRegistry.getDescriptorForTypeUrl(String)" because "this.registry" is null
        at com.google.protobuf.util.JsonFormat$ParserImpl.merge(JsonFormat.java:1309)
        at com.google.protobuf.util.JsonFormat$Parser.merge(JsonFormat.java:463)
        at com.google.api.gax.httpjson.ProtoRestSerializer.fromJson(ProtoRestSerializer.java:104)
        ... 12 more
Caused by: java.lang.NullPointerException: Cannot invoke "com.google.protobuf.TypeRegistry.getDescriptorForTypeUrl(String)" because "this.registry" is null
        at com.google.protobuf.util.JsonFormat$ParserImpl.mergeAny(JsonFormat.java:1507)
        at com.google.protobuf.util.JsonFormat$ParserImpl.access$2000(JsonFormat.java:1276)
        at com.google.protobuf.util.JsonFormat$ParserImpl$1.merge(JsonFormat.java:1343)
        at com.google.protobuf.util.JsonFormat$ParserImpl.merge(JsonFormat.java:1432)
        at com.google.protobuf.util.JsonFormat$ParserImpl.parseFieldValue(JsonFormat.java:1995)
        at com.google.protobuf.util.JsonFormat$ParserImpl.mergeRepeatedField(JsonFormat.java:1710)
        at com.google.protobuf.util.JsonFormat$ParserImpl.mergeField(JsonFormat.java:1642)
        at com.google.protobuf.util.JsonFormat$ParserImpl.mergeMessage(JsonFormat.java:1477)
        at com.google.protobuf.util.JsonFormat$ParserImpl.merge(JsonFormat.java:1435)
        at com.google.protobuf.util.JsonFormat$ParserImpl.parseFieldValue(JsonFormat.java:1995)
        at com.google.protobuf.util.JsonFormat$ParserImpl.mergeField(JsonFormat.java:1646)
        at com.google.protobuf.util.JsonFormat$ParserImpl.mergeMessage(JsonFormat.java:1477)
        at com.google.protobuf.util.JsonFormat$ParserImpl.merge(JsonFormat.java:1435)
        at com.google.protobuf.util.JsonFormat$ParserImpl.merge(JsonFormat.java:1299)
        ... 14 more
@product-auto-label product-auto-label bot added the api: spanner Issues related to the googleapis/java-spanner API. label Feb 12, 2025
@baeminbo
Copy link
Author

This issue can be resolved by passing a HttpJsonCallContext with TypeRegistry that includes DebugInfo to the call() method. The following example adds all proto types within ErrorDetails to the TypedRegistry and passing it to call(). The full code is available here.

      ServerStreamingCallable<BatchWriteRequest, BatchWriteResponse> callable = stub.batchWriteCallable();

      TypeRegistry registry = TypeRegistry.newBuilder().add(ErrorDetailsProto.getDescriptor().getMessageTypes()).build();
      HttpJsonCallContext callContext = HttpJsonCallContext.createDefault()
          .withCallOptions(HttpJsonCallOptions.newBuilder().setTypeRegistry(registry).build());

      ServerStream<BatchWriteResponse> responseStream = callable.call(request, callContext);

The example parsed the DebugInfo successfully as shown below.

Feb 11, 2025 11:23:06 PM com.google.api.client.util.LoggingByteArrayOutputStream close
CONFIG: Total: 254 bytes
Feb 11, 2025 11:23:06 PM com.google.api.client.util.LoggingByteArrayOutputStream close
CONFIG: [{
  "indexes": [
    0
  ],
  "status": {
    "code": 6,
    "message": "Row [k1] in table table1 already exists",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.DebugInfo",
        "detail": "table1(k1)"
      }
    ]
  }
}
]
response = indexes: 0
status {
  code: 6
  message: "Row [k1] in table table1 already exists"
  details {
    type_url: "type.googleapis.com/google.rpc.DebugInfo"
    value: "\022\ntable1(k1)"
  }
}```

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api: spanner Issues related to the googleapis/java-spanner API.
Projects
None yet
Development

No branches or pull requests

2 participants