Update error model in messages.proto

This commit is contained in:
Ravi Khadiwala
2026-02-06 16:03:57 -06:00
parent 1009f3ba51
commit 809ba29ce8
11 changed files with 272 additions and 219 deletions

View File

@@ -29,22 +29,22 @@ public class GroupSendTokenUtil {
this.clock = clock;
}
public void checkGroupSendToken(final ByteString serializedGroupSendToken,
final ServiceIdentifier serviceIdentifier) throws StatusException {
checkGroupSendToken(serializedGroupSendToken, List.of(serviceIdentifier.toLibsignal()));
public boolean checkGroupSendToken(final ByteString groupSendToken, final ServiceIdentifier serviceIdentifier) {
return checkGroupSendToken(groupSendToken, List.of(serviceIdentifier.toLibsignal()));
}
public void checkGroupSendToken(final ByteString serializedGroupSendToken,
final Collection<ServiceId> serviceIds) throws StatusException {
public boolean checkGroupSendToken(final ByteString groupSendToken, final Collection<ServiceId> serviceIds) {
try {
final GroupSendFullToken token = new GroupSendFullToken(serializedGroupSendToken.toByteArray());
token.verify(serviceIds, clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
final GroupSendFullToken token = new GroupSendFullToken(groupSendToken.toByteArray());
final GroupSendDerivedKeyPair groupSendKeyPair =
GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams);
token.verify(serviceIds, clock.instant(), groupSendKeyPair);
return true;
} catch (final InvalidInputException e) {
throw Status.INVALID_ARGUMENT.asException();
throw GrpcExceptions.fieldViolation("group_send_token", "malformed group send token");
} catch (VerificationFailedException e) {
throw Status.UNAUTHENTICATED.asException();
return false;
}
}
}

View File

@@ -10,7 +10,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Clock;
import java.util.Arrays;
import java.util.List;
import org.signal.chat.errors.FailedUnidentifiedAuthorization;
import org.signal.chat.errors.NotFound;
import org.signal.chat.keys.CheckIdentityKeyRequest;
@@ -19,11 +18,7 @@ import org.signal.chat.keys.GetPreKeysAnonymousRequest;
import org.signal.chat.keys.GetPreKeysAnonymousResponse;
import org.signal.chat.keys.ReactorKeysAnonymousGrpc;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.ServerSecretParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.groupsend.GroupSendDerivedKeyPair;
import org.signal.libsignal.zkgroup.groupsend.GroupSendFullToken;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessUtil;
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
import org.whispersystems.textsecuregcm.storage.Account;
@@ -37,15 +32,13 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
private final AccountsManager accountsManager;
private final KeysManager keysManager;
private final ServerSecretParams serverSecretParams;
private final Clock clock;
private final GroupSendTokenUtil groupSendTokenUtil;
public KeysAnonymousGrpcService(
final AccountsManager accountsManager, final KeysManager keysManager, final ServerSecretParams serverSecretParams, final Clock clock) {
this.accountsManager = accountsManager;
this.keysManager = keysManager;
this.serverSecretParams = serverSecretParams;
this.clock = clock;
groupSendTokenUtil = new GroupSendTokenUtil(serverSecretParams, clock);
}
@Override
@@ -59,25 +52,18 @@ public class KeysAnonymousGrpcService extends ReactorKeysAnonymousGrpc.KeysAnony
return switch (request.getAuthorizationCase()) {
case GROUP_SEND_TOKEN -> {
try {
final GroupSendFullToken token = new GroupSendFullToken(request.getGroupSendToken().toByteArray());
token.verify(List.of(serviceIdentifier.toLibsignal()), clock.instant(),
GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
yield lookUpAccount(serviceIdentifier)
.flatMap(targetAccount -> KeysGrpcHelper
.getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager))
.map(preKeys -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(preKeys).build())
.switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder()
.setTargetNotFound(NotFound.getDefaultInstance())
.build()));
} catch (InvalidInputException e) {
throw GrpcExceptions.fieldViolation("group_send_token", "malformed group send token");
} catch (VerificationFailedException e) {
if (!groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), serviceIdentifier)) {
yield Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder()
.setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance())
.build());
}
yield lookUpAccount(serviceIdentifier)
.flatMap(targetAccount -> KeysGrpcHelper
.getPreKeys(targetAccount, serviceIdentifier, deviceId, keysManager))
.map(preKeys -> GetPreKeysAnonymousResponse.newBuilder().setPreKeys(preKeys).build())
.switchIfEmpty(Mono.fromSupplier(() -> GetPreKeysAnonymousResponse.newBuilder()
.setTargetNotFound(NotFound.getDefaultInstance())
.build()));
}
case UNIDENTIFIED_ACCESS_KEY -> lookUpAccount(serviceIdentifier)
.filter(targetAccount ->

View File

@@ -6,14 +6,16 @@
package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusException;
import java.time.Clock;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import com.google.protobuf.Empty;
import org.signal.chat.errors.FailedUnidentifiedAuthorization;
import org.signal.chat.errors.NotFound;
import org.signal.chat.messages.IndividualRecipientMessageBundle;
import org.signal.chat.messages.MultiRecipientMismatchedDevices;
import org.signal.chat.messages.MultiRecipientSuccess;
import org.signal.chat.messages.SendMessageResponse;
import org.signal.chat.messages.SendMultiRecipientMessageRequest;
import org.signal.chat.messages.SendMultiRecipientMessageResponse;
@@ -52,7 +54,10 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
private final SpamChecker spamChecker;
private final Clock clock;
private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse.newBuilder().build();
private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse
.newBuilder()
.setSuccess(Empty.getDefaultInstance())
.build();
public MessagesAnonymousGrpcService(final AccountsManager accountsManager,
final RateLimiters rateLimiters,
@@ -65,35 +70,51 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
this.accountsManager = accountsManager;
this.rateLimiters = rateLimiters;
this.messageSender = messageSender;
this.groupSendTokenUtil = groupSendTokenUtil;
this.messageByteLimitEstimator = messageByteLimitEstimator;
this.spamChecker = spamChecker;
this.clock = clock;
this.groupSendTokenUtil = groupSendTokenUtil;
}
@Override
public SendMessageResponse sendSingleRecipientMessage(final SendSealedSenderMessageRequest request)
throws StatusException, RateLimitExceededException {
throws RateLimitExceededException {
final ServiceIdentifier destinationServiceIdentifier =
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getDestination());
final Account destination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier)
.orElseThrow(Status.UNAUTHENTICATED::asException);
final Optional<Account> maybeDestination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier);
switch (request.getAuthorizationCase()) {
final boolean authorized = switch (request.getAuthorizationCase()) {
case UNIDENTIFIED_ACCESS_KEY -> {
if (!UnidentifiedAccessUtil.checkUnidentifiedAccess(destination, request.getUnidentifiedAccessKey().toByteArray())) {
throw Status.UNAUTHENTICATED.asException();
if (destinationServiceIdentifier.identityType() == IdentityType.PNI) {
throw GrpcExceptions.fieldViolation("authorization",
"message for PNI cannot be authenticated with an unidentified access token");
}
final byte[] uak = request.getUnidentifiedAccessKey().toByteArray();
yield maybeDestination
.map(account -> UnidentifiedAccessUtil.checkUnidentifiedAccess(account, uak))
// If the destination is not found, return an authorization error instead of not-found. Otherwise,
// this would provide an unauthenticated existence check.
.orElse(false);
}
case GROUP_SEND_TOKEN ->
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), destinationServiceIdentifier);
case AUTHORIZATION_NOT_SET ->
throw GrpcExceptions.fieldViolation("authorization", "expected authorization token not provided");
};
case AUTHORIZATION_NOT_SET -> throw Status.UNAUTHENTICATED.asException();
if (!authorized) {
return SendMessageResponse.newBuilder()
.setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance())
.build();
}
return sendIndividualMessage(destination,
if (maybeDestination.isEmpty()) {
return SendMessageResponse.newBuilder().setDestinationNotFound(NotFound.getDefaultInstance()).build();
}
return sendIndividualMessage(maybeDestination.get(),
destinationServiceIdentifier,
request.getMessages(),
request.getEphemeral(),
@@ -103,7 +124,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
@Override
public SendMessageResponse sendStory(final SendStoryMessageRequest request)
throws StatusException, RateLimitExceededException {
throws RateLimitExceededException {
final ServiceIdentifier destinationServiceIdentifier =
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getDestination());
@@ -132,7 +153,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
final IndividualRecipientMessageBundle messages,
final boolean ephemeral,
final boolean urgent,
final boolean story) throws StatusException, RateLimitExceededException {
final boolean story) throws RateLimitExceededException {
final SpamCheckResult<GrpcResponse<SendMessageResponse>> spamCheckResult =
spamChecker.checkForIndividualRecipientSpamGrpc(
@@ -196,13 +217,16 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
}
@Override
public SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SendMultiRecipientMessageRequest request)
throws StatusException {
public SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SendMultiRecipientMessageRequest request) {
final SealedSenderMultiRecipientMessage multiRecipientMessage =
parseAndValidateMultiRecipientMessage(request.getMessage().getPayload().toByteArray());
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), multiRecipientMessage.getRecipients().keySet());
if (!groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), multiRecipientMessage.getRecipients().keySet())) {
return SendMultiRecipientMessageResponse.newBuilder()
.setFailedUnidentifiedAuthorization(FailedUnidentifiedAuthorization.getDefaultInstance())
.build();
}
return sendMultiRecipientMessage(multiRecipientMessage,
request.getMessage().getTimestamp(),
@@ -212,21 +236,26 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
}
@Override
public SendMultiRecipientMessageResponse sendMultiRecipientStory(final SendMultiRecipientStoryRequest request)
throws StatusException {
public SendMultiRecipientMessageResponse sendMultiRecipientStory(final SendMultiRecipientStoryRequest request) {
final SealedSenderMultiRecipientMessage multiRecipientMessage =
parseAndValidateMultiRecipientMessage(request.getMessage().getPayload().toByteArray());
return sendMultiRecipientMessage(multiRecipientMessage,
final SendMultiRecipientMessageResponse sendMultiRecipientMessageResponse = sendMultiRecipientMessage(
multiRecipientMessage,
request.getMessage().getTimestamp(),
false,
request.getUrgent(),
true)
.toBuilder()
// Don't identify unresolved recipients for stories
.clearUnresolvedRecipients()
.build();
true);
if (sendMultiRecipientMessageResponse.hasSuccess()) {
// Clear the unresolved recipients for stories
return sendMultiRecipientMessageResponse.toBuilder()
.setSuccess(MultiRecipientSuccess.getDefaultInstance())
.build();
} else {
return sendMultiRecipientMessageResponse;
}
}
private SendMultiRecipientMessageResponse sendMultiRecipientMessage(
@@ -234,7 +263,7 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
final long timestamp,
final boolean ephemeral,
final boolean urgent,
final boolean story) throws StatusException {
final boolean story) {
final SpamCheckResult<GrpcResponse<SendMultiRecipientMessageResponse>> spamCheckResult =
spamChecker.checkForMultiRecipientSpamGrpc(story
@@ -257,20 +286,18 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
story,
ephemeral,
urgent,
RequestAttributesUtil.getUserAgent().orElse(null));
RequestAttributesUtil.getUserAgent().orElse(null))
.join();
final SendMultiRecipientMessageResponse.Builder responseBuilder = SendMultiRecipientMessageResponse.newBuilder();
final MultiRecipientSuccess.Builder responseBuilder = MultiRecipientSuccess.newBuilder();
MessageUtil.getUnresolvedRecipients(multiRecipientMessage, resolvedRecipients).stream()
.map(ServiceIdentifierUtil::toGrpcServiceIdentifier)
.forEach(responseBuilder::addUnresolvedRecipients);
return responseBuilder.build();
return SendMultiRecipientMessageResponse.newBuilder().setSuccess(responseBuilder).build();
} catch (final MessageTooLargeException e) {
throw Status.INVALID_ARGUMENT
.withDescription("Message for an individual recipient was too large")
.withCause(e)
.asRuntimeException();
throw GrpcExceptions.invalidArguments("message for an individual recipient was too large");
} catch (final MultiRecipientMismatchedDevicesException e) {
final MultiRecipientMismatchedDevices.Builder mismatchedDevicesBuilder =
MultiRecipientMismatchedDevices.newBuilder();
@@ -285,22 +312,29 @@ public class MessagesAnonymousGrpcService extends SimpleMessagesAnonymousGrpc.Me
}
private SealedSenderMultiRecipientMessage parseAndValidateMultiRecipientMessage(
final byte[] serializedMultiRecipientMessage) throws StatusException {
final byte[] serializedMultiRecipientMessage) {
final SealedSenderMultiRecipientMessage multiRecipientMessage;
try {
multiRecipientMessage = SealedSenderMultiRecipientMessage.parse(serializedMultiRecipientMessage);
} catch (final InvalidMessageException | InvalidVersionException e) {
throw Status.INVALID_ARGUMENT.withCause(e).asException();
} catch (final InvalidMessageException _) {
throw GrpcExceptions.fieldViolation("payload", "invalid multi-recipient message");
} catch (final InvalidVersionException e) {
throw GrpcExceptions.fieldViolation("payload", "unrecognized sealed sender major version");
}
if (multiRecipientMessage.getRecipients().isEmpty()) {
throw GrpcExceptions.fieldViolation("payload", "recipient list is empty");
}
// Check that the request is well-formed and doesn't contain repeated entries for the same device for the same
// recipient
if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) {
throw Status.INVALID_ARGUMENT.withDescription("Multi-recipient message contains duplicate recipient").asException();
throw GrpcExceptions.fieldViolation("payload", "multi-recipient message contains duplicate recipient");
}
return multiRecipientMessage;
}
}

View File

@@ -5,6 +5,7 @@
package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.Empty;
import io.grpc.Status;
import io.grpc.StatusException;
import java.util.Map;
@@ -21,7 +22,10 @@ import org.whispersystems.textsecuregcm.storage.Account;
public class MessagesGrpcHelper {
private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse.newBuilder().build();
private static final SendMessageResponse SEND_MESSAGE_SUCCESS_RESPONSE = SendMessageResponse
.newBuilder()
.setSuccess(Empty.getDefaultInstance())
.build();
/**
* Sends a "bundle" of messages to an individual destination account, mapping common exceptions to appropriate gRPC
@@ -37,9 +41,8 @@ public class MessagesGrpcHelper {
*
* @return a response object to send to callers
*
* @throws StatusException if the message bundle could not be sent due to an out-of-date device set or an invalid
* message payload
* @throws RateLimitExceededException if the message bundle could not be sent due to a violated rated limit
* @throws io.grpc.StatusRuntimeException for invalid arguments if the message is too large to send
*/
public static SendMessageResponse sendMessage(final MessageSender messageSender,
final Account destination,
@@ -47,7 +50,7 @@ public class MessagesGrpcHelper {
final Map<Byte, MessageProtos.Envelope> messagesByDeviceId,
final Map<Byte, Integer> registrationIdsByDeviceId,
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional<Byte> syncMessageSenderDeviceId)
throws StatusException, RateLimitExceededException {
throws RateLimitExceededException {
try {
messageSender.sendMessages(destination,
@@ -63,7 +66,7 @@ public class MessagesGrpcHelper {
.setMismatchedDevices(buildMismatchedDevices(destinationServiceIdentifier, e.getMismatchedDevices()))
.build();
} catch (final MessageTooLargeException e) {
throw Status.INVALID_ARGUMENT.withDescription("Message too large").withCause(e).asException();
throw GrpcExceptions.invalidArguments("message too large");
}
}

View File

@@ -6,12 +6,11 @@
package org.whispersystems.textsecuregcm.grpc;
import com.google.protobuf.ByteString;
import io.grpc.Status;
import io.grpc.StatusException;
import java.time.Clock;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.signal.chat.errors.NotFound;
import org.signal.chat.messages.AuthenticatedSenderMessageType;
import org.signal.chat.messages.IndividualRecipientMessageBundle;
import org.signal.chat.messages.SendAuthenticatedSenderMessageRequest;
@@ -60,24 +59,25 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
@Override
public SendMessageResponse sendMessage(final SendAuthenticatedSenderMessageRequest request)
throws StatusException, RateLimitExceededException {
throws RateLimitExceededException {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
final AciServiceIdentifier senderServiceIdentifier = new AciServiceIdentifier(authenticatedDevice.accountIdentifier());
final Account sender =
accountsManager.getByServiceIdentifier(senderServiceIdentifier).orElseThrow(Status.UNAUTHENTICATED::asException);
final Account sender = accountsManager.getByServiceIdentifier(senderServiceIdentifier)
.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials"));
final ServiceIdentifier destinationServiceIdentifier =
ServiceIdentifierUtil.fromGrpcServiceIdentifier(request.getDestination());
if (sender.isIdentifiedBy(destinationServiceIdentifier)) {
throw Status.INVALID_ARGUMENT
.withDescription("Use `sendSyncMessage` to send messages to own account")
.asException();
throw GrpcExceptions.invalidArguments("use `sendSyncMessage` to send messages to own account");
}
final Account destination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier)
.orElseThrow(Status.NOT_FOUND::asException);
final Optional<Account> maybeDestination = accountsManager.getByServiceIdentifier(destinationServiceIdentifier);
if (maybeDestination.isEmpty()) {
return SendMessageResponse.newBuilder().setDestinationNotFound(NotFound.getDefaultInstance()).build();
}
final Account destination = maybeDestination.get();
rateLimiters.getMessagesLimiter().validate(authenticatedDevice.accountIdentifier(), destination.getUuid());
@@ -93,12 +93,12 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
@Override
public SendMessageResponse sendSyncMessage(final SendSyncMessageRequest request)
throws StatusException, RateLimitExceededException {
throws RateLimitExceededException {
final AuthenticatedDevice authenticatedDevice = AuthenticationUtil.requireAuthenticatedDevice();
final AciServiceIdentifier senderServiceIdentifier = new AciServiceIdentifier(authenticatedDevice.accountIdentifier());
final Account sender =
accountsManager.getByServiceIdentifier(senderServiceIdentifier).orElseThrow(Status.UNAUTHENTICATED::asException);
final Account sender = accountsManager.getByServiceIdentifier(senderServiceIdentifier)
.orElseThrow(() -> GrpcExceptions.invalidCredentials("invalid credentials"));
return sendMessage(sender,
senderServiceIdentifier,
@@ -117,7 +117,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
final MessageType messageType,
final IndividualRecipientMessageBundle messages,
final boolean ephemeral,
final boolean urgent) throws StatusException, RateLimitExceededException {
final boolean urgent) throws RateLimitExceededException {
try {
final int totalPayloadLength = messages.getMessagesMap().values().stream()
@@ -182,7 +182,7 @@ public class MessagesGrpcService extends SimpleMessagesGrpc.MessagesImplBase {
case PREKEY_MESSAGE -> MessageProtos.Envelope.Type.PREKEY_BUNDLE;
case PLAINTEXT_CONTENT -> MessageProtos.Envelope.Type.PLAINTEXT_CONTENT;
case UNSPECIFIED, UNRECOGNIZED ->
throw Status.INVALID_ARGUMENT.withDescription("Unrecognized envelope type").asRuntimeException();
throw GrpcExceptions.invalidArguments("unrecognized envelope type");
};
}
}

View File

@@ -57,8 +57,9 @@ public class ProfileAnonymousGrpcService extends SimpleProfileAnonymousGrpc.Prof
final Account account = switch (request.getAuthenticationCase()) {
case GROUP_SEND_TOKEN -> {
groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), targetIdentifier);
if (!groupSendTokenUtil.checkGroupSendToken(request.getGroupSendToken(), targetIdentifier)) {
throw Status.UNAUTHENTICATED.asException();
}
yield accountsManager.getByServiceIdentifier(targetIdentifier)
.orElseThrow(Status.NOT_FOUND::asException);
}

View File

@@ -7,6 +7,7 @@ package org.whispersystems.textsecuregcm.spam;
import io.grpc.Status;
import io.grpc.StatusException;
import io.grpc.StatusRuntimeException;
import javax.annotation.Nullable;
import java.util.Optional;
@@ -18,14 +19,18 @@ import java.util.Optional;
*/
public class GrpcResponse<R> {
private final Status status;
@Nullable
private final StatusRuntimeException statusException;
@Nullable
private final R response;
private GrpcResponse(final Status status, @Nullable final R response) {
this.status = status;
private GrpcResponse(final @Nullable StatusRuntimeException statusException, @Nullable final R response) {
this.statusException = statusException;
this.response = response;
if (!((statusException == null) ^ (response == null))) {
throw new IllegalArgumentException("exactly one of statusException and response must be non-null");
}
}
/**
@@ -37,7 +42,7 @@ public class GrpcResponse<R> {
*
* @param <R> the type of response object
*/
public static <R> GrpcResponse<R> withStatus(final Status status) {
public static <R> GrpcResponse<R> withStatusException(final StatusRuntimeException status) {
return new GrpcResponse<>(status, null);
}
@@ -51,7 +56,7 @@ public class GrpcResponse<R> {
* @param <R> the type of response object
*/
public static <R> GrpcResponse<R> withResponse(final R response) {
return new GrpcResponse<>(Status.OK, response);
return new GrpcResponse<>(null, response);
}
/**
@@ -62,11 +67,10 @@ public class GrpcResponse<R> {
*
* @throws StatusException if no message body is specified
*/
public R getResponseOrThrowStatus() throws StatusException {
if (response != null) {
return response;
public R getResponseOrThrowStatus() throws StatusRuntimeException {
if (statusException != null) {
throw statusException;
}
throw status.asException();
return response;
}
}