Search Tutorials


Spring Boot + gRPC Error Handling - Using Trailer Metadata | JavaInUse

Spring Boot + gRPC Error Handling - Using Trailer Metadata

In previous tutorial we implemented Spring Boot gRPC Error Handling Hello World Example. In this tutorial we will be modifying the example to pass additional error details from the server to the client using gRPC Trailer Metadata. Also in another tutorial we will be implementing Global Exception Handler Using GrpcAdvice.
In the example we implemented previously the gRPC server passed the gRPC error status code and description to the client.
Now suppose we also need the server send some more application specific error information to the client. This is where trailer metadata comes into picture.
Spring Boot 3 + gRPC error handling metadata trailer

Trailers are a special kind of header that is sent after the message data. They are used internally to communicate the outcome of an RPC. At the application level, custom trailers can be used to communicate things not directly part of the data, such as server utilization and query cost. Trailers are sent only by the server.
Using Trailers we can provide additional information or context at the end of an RPC (Remote Procedure Call) response or response stream. Trailers are sent as part of the RPC's trailing metadata-metadata that is sent after the main response data. This additional metadata often includes details related to the RPC's status, error conditions, or other context information.
Spring Boot + gRPC error handling metadata trailer

Video

This tutorial is explained in the below Youtube Video.

gRPC - Table of Contents

Spring Boot+ gRPC Hello World Example Spring Boot gRPC Server + C# gRPC Client Example Spring Boot 3 + gRPC - Types of gRPC Spring Boot 3 + gRPC Unary Example Spring Boot 3 + gRPC Server Streaming Example Spring Boot 3 + gRPC Client Streaming Example Spring Boot 3 + gRPC Bidirectional Streaming Example Spring Boot + gRPC Deadline Example Spring Boot + gRPC Error Handling Example Spring Boot + gRPC Error Handling - Using Trailer Metadata Spring Boot + gRPC Error Handling - Global Exception Handler Using GrpcAdvice

Implementation

We will be modifying the code we had implemented for Spring Boot gRPC Error Handling Example.
Suppose the gRPC server needs to send the error cause along with the description and gRPC server code.
Create a class named AccountNotFoundException which should have more custom information that needs to be sent to the client.
package com.javainuse.bank.exception;

public class AccountNotFoundException extends RuntimeException {

	private static final long serialVersionUID = 1L;

	public AccountNotFoundException(String message) {
		super();
	}
}
Next we will be passing an instance of the AccountNotFoundException as cause in the returned error message.
package com.javainuse.bank.service;

import com.javainuse.banking.AccountBalanceResponse;
import com.javainuse.banking.AccountBalanceServiceGrpc;
import com.javainuse.banking.AccountRequest;

import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class BankAccountBalanceService extends AccountBalanceServiceGrpc.AccountBalanceServiceImplBase {

	@Override
	public void getAccountBalance(AccountRequest request,
			StreamObserver<com.javainuse.banking.AccountBalanceResponse> responseObserver) {

		if ((request.getAccountNumber().equals("account5"))) {
			responseObserver.onError((Status.NOT_FOUND.withDescription("The requested Account Number cannot be found.")
					.withCause(new AccountNotFoundException("Database Error- Connection Refused."))).asRuntimeException());
			return;
		}
		AccountBalanceResponse empResp = AccountBalanceResponse.newBuilder()
				.setAccountNumber(request.getAccountNumber()).setBalance(100).build();

		// set the response object
		responseObserver.onNext(empResp);

		// mark process is completed
		responseObserver.onCompleted();
	}
}
At the gRPC client end let us now try to print the cause when the exception is received.
package com.javainuse.bank.service;

import org.springframework.stereotype.Service;

import com.javainuse.banking.AccountBalanceResponse;
import com.javainuse.banking.AccountBalanceServiceGrpc;
import com.javainuse.banking.AccountRequest;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;

@Service
public class BankService {

	public void getAccountBalance() {

		ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8090).usePlaintext().build();

		AccountBalanceServiceGrpc.AccountBalanceServiceBlockingStub stub = AccountBalanceServiceGrpc
				.newBlockingStub(channel);

		try {
			AccountBalanceResponse bookResponse = stub
					.getAccountBalance(AccountRequest.newBuilder().setAccountNumber("account5").build());

			System.out.println(bookResponse);
		} catch (StatusRuntimeException ex) {
			Status status = ex.getStatus();
			System.out.println("error code -" + status.getCode());
			System.out.println("error description -" + status.getDescription());
			System.out.println("error cause -" + status.getCause());
		}

		channel.shutdown();

	}
}




We get the error cause as null. So the gRPC server is unable to pass the cause to the client.
Spring Boot 3 + gRPC error handling no cause
We will be passing any additional error information that needs to passed from server to client using trailer metadata.
Next we will be modifying the proto file at both the gRPC client and server side to add the CustomError message.
syntax = "proto3";

package banking;

option java_multiple_files = true;
option java_package = "com.javainuse.banking";

// Message representing a client's account balance request
message AccountRequest {
  string account_number = 1;
}

// Message representing a server's response to a client's account balance request
message AccountBalanceResponse {
  string account_number = 1;
  double balance = 2;
}

message CustomError {
  string errorType = 1;
  string message = 2;
}

// Service for retrieving account balance
service AccountBalanceService {
  rpc GetAccountBalance (AccountRequest) returns (AccountBalanceResponse) {}
}
We will be modifying the BankAccountBalanceService to construct a new Metadata object and add a CustomError message to it with the error type set to "CONNECTION_REFUSED". Next we create a StatusRuntimeException with a NOT_FOUND status and a custom description stating that the requested account number cannot be found. Finally, using the responseObserver to send an error response containing the StatusRuntimeException.
package com.javainuse.bank.service;

import com.javainuse.banking.AccountBalanceResponse;
import com.javainuse.banking.AccountBalanceServiceGrpc;
import com.javainuse.banking.AccountRequest;
import com.javainuse.banking.CustomError;

import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.protobuf.ProtoUtils;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;

@GrpcService
public class BankAccountBalanceService extends AccountBalanceServiceGrpc.AccountBalanceServiceImplBase {

	@Override
	public void getAccountBalance(AccountRequest request,
			StreamObserver<com.javainuse.banking.AccountBalanceResponse> responseObserver) {

		if ((request.getAccountNumber().equals("account5"))) {
			Metadata metadata = new Metadata();
			Metadata.Key<CustomError> customError = ProtoUtils.keyForProto(CustomError.getDefaultInstance());
			metadata.put(customError, CustomError.newBuilder().setMessage("Database Error- Connection Refused.")
					.setErrorType("CONNECTION_REFUSED").build());
			var statusRuntimeException = Status.NOT_FOUND
					.withDescription("The requested Account Number cannot be found.").asRuntimeException(metadata);
			responseObserver.onError(statusRuntimeException);
			return;
		}

		AccountBalanceResponse empResp = AccountBalanceResponse.newBuilder()
				.setAccountNumber(request.getAccountNumber()).setBalance(100).build();

		// set the response object
		responseObserver.onNext(empResp);

		// mark process is completed
		responseObserver.onCompleted();
	}
}
At the gRPC client side using gRPC metadata we extract the custom error object called CustomError from an exception.
package com.javainuse.bank.service;

import org.springframework.stereotype.Service;

import com.javainuse.banking.AccountBalanceResponse;
import com.javainuse.banking.AccountBalanceServiceGrpc;
import com.javainuse.banking.AccountRequest;
import com.javainuse.banking.CustomError;

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.ProtoUtils;

@Service
public class BankService {

	public void getAccountBalance() {

		ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8090).usePlaintext().build();

		AccountBalanceServiceGrpc.AccountBalanceServiceBlockingStub stub = AccountBalanceServiceGrpc
				.newBlockingStub(channel);

		try {
			AccountBalanceResponse bookResponse = stub
					.getAccountBalance(AccountRequest.newBuilder().setAccountNumber("account5").build());

			System.out.println(bookResponse);
		} catch (StatusRuntimeException ex) {
			Status status = ex.getStatus();
			System.out.println("error code -" + status.getCode());
			System.out.println("error description -" + status.getDescription());

			Metadata metadata = Status.trailersFromThrowable(ex);
			Metadata.Key<CustomError> customErrorKey = ProtoUtils.keyForProto(CustomError.getDefaultInstance());
			CustomError customError = metadata.get(customErrorKey);
			System.out.println(customError);
		}
		channel.shutdown();
	}
}
Start the gRPC server and also the client.
Spring Boot 3 + gRPC error handling custom code

Download Source Code

Download it - Spring Boot + gRPC Metadata Trailer Client Example
Download it - Spring Boot + gRPC Metadata Trailer Server Example