diff --git a/binder/src/main/java/io/grpc/binder/internal/Outbound.java b/binder/src/main/java/io/grpc/binder/internal/Outbound.java index 7db5bf0fbe4..330699f6d14 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Outbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Outbound.java @@ -261,7 +261,9 @@ protected final void sendInternal() throws StatusException { transport.sendTransaction(callId, parcel); statsTraceContext.outboundWireSize(dataSize); statsTraceContext.outboundUncompressedSize(dataSize); - } catch (IOException e) { + } catch (IOException | RuntimeException e) { + // Checked IO failures and unchecked application serialization failures (for example + // Parcelable, metadata marshaller, or InputStream) both end the call. Do not catch Error. throw Status.INTERNAL.withCause(e).asException(); } } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 63c47bf4f19..b5db7b72534 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -669,6 +669,31 @@ public void msgFragmentsDeliveredToClientOutOfOrder() throws Exception { assertThat(methodDescriptor.parseResponse(msg)).isEqualTo(largeMessage); } + @Test + public void uncheckedSerializationFailureClosesClientStream() throws Exception { + // Unchecked serialization failures must close the call, same as IOException. + server.start(serverListener); + client = newClientTransport(server); + startTransport(client, mockClientTransportListener); + + RuntimeException failure = new IllegalStateException("serialization"); + ClientStreamListenerBase listener = new ClientStreamListenerBase(); + ClientStream stream = + client.newStream(methodDescriptor, new Metadata(), CallOptions.DEFAULT, noopTracers); + stream.start(listener); + stream.writeMessage( + new InputStream() { + @Override + public int read() { + throw failure; + } + }); + + Status status = listener.awaitClose(TIMEOUT_MS, MILLISECONDS); + assertAbout(status()).that(status).hasCode(Status.Code.INTERNAL); + assertThat(status.getCause()).isSameInstanceAs(failure); + } + private static OneWayBinderProxy takeNextBinder( BlockingBinderDecorator decorator) throws InterruptedException { OneWayBinderProxy proxy = decorator.takeNextRequest(TIMEOUT_MS, MILLISECONDS);