一键导入
grpc-migration-error-handling
Migrate from Failure entity to standard gRPC Status error handling. Create next-version services with proper error codes.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Migrate from Failure entity to standard gRPC Status error handling. Create next-version services with proper error codes.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
RFC-39 compliant API best practices for Java services. Covers request/response patterns, error handling, pagination, versioning, and authentication standards. Use when designing or reviewing REST APIs in Java services.
RFC-29 compliant changelog management with Common Changelog format and Unreleased section extension. Validates changelog on commits, generates from git history, and ensures all merged PRs are documented. Use when setting up changelog workflows or retrofitting existing repositories.
Systematic workflow for CodeRabbit reviews - local CLI, PR threads, and commit attribution
Enforces consistent coding standards across the codebase including naming conventions, code organization, and documentation requirements. Use when writing new code, reviewing pull requests, or refactoring existing code.
Database integration patterns for Java services using jOOQ and Flyway. Covers code generation, read/write splitting, migration guidelines, and version compatibility. Use when setting up or maintaining PostgreSQL integration.
Version catalog strategy, dependency management, BOMs, and version constraints for Java/Gradle projects. Covers version centralization, never-downgrade policy, bundle patterns, resolution strategies, and compatibility matrices.
| name | grpc-migration-error-handling |
| description | Migrate from Failure entity to standard gRPC Status error handling. Create next-version services with proper error codes. |
| compatibility | Java projects using CustomRPC Failure patterns; requires grpc-compliance-validate-repository |
| metadata | {"version":"1.0.0","technology":"java","category":"migration","tags":["java","grpc","error-handling","migration"]} |
Create next-version services with standard gRPC Status error handling.
Migrate services from CustomRPC implementations using Failure entity to standard gRPC error handling. This ensures:
📚 references/ - Detailed documentation
The grpc-compliance-validate-repository command must be available for the migration checklist validation step.
Cloud agents: Pre-installed (no action needed).
Local setup:
export HOMEBREW_GITHUB_API_TOKEN=your-token
brew tap bitsoex/homebrew-bitso
brew install bitso-grpc-linter
Verify: grpc-compliance-validate-repository --help
See ../grpc-services-rfc-33/references/installation.md for details.
Before starting, check what versioned services already exist to pick the correct next version suffix.
How to determine V{N}:
AccountServiceV2, AccountServiceV3)Example: If AccountService and AccountServiceV2 already exist, create AccountServiceV3 — along with AccountResponseV3, account_service_v3.proto, and AccountServiceV3Handler.
Throughout this document, V{N} means the next available version number. Examples use V2 as a concrete illustration — substitute the actual version for your project.
Why a new versioned service?
Always place the new versioned service and its specific message definitions in a NEW proto file, separate from the previous version's definitions.
Why separate files?
protos.model)What goes in the new version's file:
Failure/oneof)What stays in the old version's file:
The new version's file imports the old version's file (or its model file) to reference shared types.
Note: Examples below use V2 as a concrete illustration. If V2 already exists in your project, substitute V3 (or the appropriate next version) throughout.
service TransferService {
rpc Transfer(TransferRequest) returns (TransferResponse);
}
message TransferResponse {
oneof result {
Transfer transfer = 1;
Failure failure = 2; // DO NOT USE
}
}
// Old pattern
if (invalidAmount) {
return TransferResponse.newBuilder()
.setFailure(Failure.newBuilder()
.setCode("INVALID_AMOUNT")
.setMessage("Amount must be positive")
.build())
.build();
}
File: transfer_service.proto (old version — deprecated, kept for backwards compatibility)
syntax = "proto3";
package com.bitso.transfer;
/**
* @deprecated since v2.0.0, forRemoval (planned for v3.0.0).
* Use TransferServiceV2 instead.
*/
service TransferService {
option deprecated = true;
// @replacedBy: TransferServiceV2.Transfer
rpc Transfer(TransferRequest) returns (TransferResponse) {
option deprecated = true;
}
}
// Deprecated since v2.0.0. Planned for removal in v3.0.0.
// Use TransferResponseV2 instead.
message TransferResponse {
option deprecated = true;
oneof result {
Transfer transfer = 1;
Failure failure = 2;
}
}
// Shared types — NOT deprecated, reused by V2
message TransferRequest { ... }
message Transfer { ... }
File: transfer_service_v2.proto (new version — separate file)
syntax = "proto3";
package com.bitso.transfer.v2;
option java_package = "com.bitso.transfer.v2";
option java_outer_classname = "TransferServiceV2Proto";
import "transfer_service.proto";
// V2 service with standard gRPC error handling
service TransferServiceV2 {
rpc Transfer(com.bitso.transfer.TransferRequest) returns (TransferResponseV2);
}
message TransferResponseV2 {
com.bitso.transfer.Transfer transfer = 1; // No oneof, no Failure
}
// Business error details
enum TransferErrorCode {
INVALID_AMOUNT = 0;
INSUFFICIENT_FUNDS = 1;
}
message TransferError {
TransferErrorCode code = 1;
string message = 2;
}
// New versioned service handler
public class TransferServiceV2Handler extends TransferServiceV2Grpc.TransferServiceV2ImplBase {
@Override
public void transfer(TransferRequest request, StreamObserver<TransferResponseV2> responseObserver) {
if (invalidAmount) {
TransferError error = TransferError.newBuilder()
.setCode(TransferErrorCode.INVALID_AMOUNT)
.setMessage("Amount must be positive")
.build();
// Use google.rpc.Status (not io.grpc.Status)
Status status = Status.newBuilder()
.setCode(Code.FAILED_PRECONDITION.getNumber())
.setMessage("Business constraint violation")
.addDetails(Any.pack(error))
.build();
responseObserver.onError(StatusProto.toStatusRuntimeException(status));
return;
}
// Success case
TransferResponseV2 response = TransferResponseV2.newBuilder()
.setTransfer(transfer)
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
Use ONLY these error codes at Bitso:
| Error Code | When to Use |
|---|---|
INTERNAL | Infrastructure errors, dependency unavailable, system failures |
UNKNOWN | Unknown errors from downstream services |
FAILED_PRECONDITION | Business constraint violations, business logic errors |
Key Principle: gRPC errors are TECHNICAL. Business error details go in trailers/details using google.rpc.Status.
⚠️ CRITICAL: Package naming differs for existing vs NEW protos
package protos.model;, keep itpackage protos.model;com.bitso.{service-name}.{version} or com.bitso.{service-name}.v{N}com.bitso.iba.rate.v2, com.bitso.transfer.v3java_package option should align with the actual package structureDetermine project package convention by:
com.bitso.iba.grpc.service)java_package options in existing protos// ❌ WRONG - Generic package for NEW versioned services
syntax = "proto3";
package protos.model;
option java_package = "com.bitso.iba.model";
// ✅ CORRECT - Project-specific package for NEW versioned services
syntax = "proto3";
package com.bitso.iba.rate.v2;
option java_package = "com.bitso.iba.rate.v2";
Proto file location must match package:
package com.bitso.iba.rate.v2; → src/main/resources/com/bitso/iba/rate/v2/iba_rate.protoBefore implementing the new versioned services, create a centralized FailureHelper in the grpc module:
package com.bitso.{service}.grpc.util;
import com.bitso.commons.protobuf.DataCommonsProto;
import io.grpc.Metadata;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.lite.ProtoLiteUtils;
/**
* Utility class for consistent gRPC error handling.
* Use this for all new versioned service error responses.
*/
public class FailureHelper {
private static final Metadata.Key<DataCommonsProto.Failure> FAILURE_DETAILS_KEY =
Metadata.Key.of(
"bitso-failure-detail-bin",
ProtoLiteUtils.metadataMarshaller(DataCommonsProto.Failure.getDefaultInstance()));
/**
* Creates a StatusRuntimeException with failure details in metadata.
*
* @param code The gRPC status code (INTERNAL, FAILED_PRECONDITION, UNKNOWN)
* @param failure The failure details to include in metadata
* @return StatusRuntimeException ready to be thrown
*/
public static StatusRuntimeException createStatusRuntimeException(
Status.Code code, DataCommonsProto.Failure failure) {
Metadata metadata = new Metadata();
metadata.put(FAILURE_DETAILS_KEY, failure);
return code.toStatus().withDescription(failure.getCode()).asRuntimeException(metadata);
}
/**
* Extracts failure details from a throwable (typically StatusRuntimeException).
*
* @param throwable The exception to extract failure from
* @return The Failure details, or empty Failure if not present
*/
public static DataCommonsProto.Failure extractFailure(Throwable throwable) {
if (throwable instanceof StatusRuntimeException statusRuntimeException) {
if (statusRuntimeException.getTrailers() != null
&& statusRuntimeException.getTrailers().get(FAILURE_DETAILS_KEY) != null) {
return statusRuntimeException.getTrailers().get(FAILURE_DETAILS_KEY);
}
}
return DataCommonsProto.Failure.newBuilder().build();
}
}
Location: {grpc-module}/src/main/java/com/bitso/{service}/grpc/util/FailureHelper.java
Check existing proto files for versioned services:
ServiceV2 exists → use V3ServiceV3 exists → use V4Create a new proto file (e.g., account_service_v{N}.proto). Import the existing proto to reuse shared request/payload types.
// File: account_service_v2.proto (NEW file — separate from previous version)
syntax = "proto3";
package com.bitso.account.v2;
option java_package = "com.bitso.account.v2";
option java_outer_classname = "AccountServiceV2Proto";
import "account_service.proto";
service AccountServiceV2 {
rpc CreateAccount(original.package.CreateAccountRequest) returns (CreateAccountResponseV2);
rpc GetAccount(original.package.GetAccountRequest) returns (GetAccountResponseV2);
}
message CreateAccountResponseV2 {
original.package.Account account = 1; // No oneof, no Failure
}
Do NOT add the new versioned service or its response messages to an existing proto file. Shared types (request messages, payload types, enums) remain in the old file and are imported by the new one.
Mark the previous service with both @replacedBy comments and protobuf option deprecated = true.
The comments provide human-readable migration guidance; the options generate @Deprecated annotations
in Java/Kotlin/Go stubs, giving gRPC clients compile-time deprecation warnings.
/**
* @deprecated since v{CURRENT}, forRemoval (planned for v{NEXT_MAJOR}).
* Use AccountServiceV{N} instead.
*/
service AccountService {
option deprecated = true;
// @replacedBy: AccountServiceV{N}.CreateAccount
rpc CreateAccount(CreateAccountRequest) returns (CreateAccountResponse) {
option deprecated = true;
}
}
// Deprecated since v{CURRENT}. Planned for removal in v{NEXT_MAJOR}.
// Use CreateAccountResponseV{N} instead.
message CreateAccountResponse {
option deprecated = true;
oneof result {
Account account = 1;
Failure failure = 2;
}
}
Deprecation comment format:
@deprecated since v{CURRENT}, forRemoval (planned for v{NEXT_MAJOR}).Deprecated since v{CURRENT}. Planned for removal in v{NEXT_MAJOR}.{CURRENT} = the version where the new service is introduced (from gradle.properties){NEXT_MAJOR} = the next MAJOR version bump, when the old service will be deletedWhat to deprecate:
| Element | When to deprecate |
|---|---|
service | Always - the entire service is superseded by V{N} |
rpc methods | Always - each method has a V{N} replacement |
| Request/response messages | Only if V{N} uses new message types (e.g., flattened contract) |
| Shared types (enums, nested messages) | Never - if reused by V{N} service, keep them non-deprecated |
./gradlew generateProto
# OR
./gradlew :module-protos-generated:generateProto
Create V{N} handler class extending generated base class. See references/ERROR_PATTERNS.md for implementation patterns.
Register the new handler in your gRPC server configuration.
This step is NON-NEGOTIABLE. Adding versioned services is a BREAKING CHANGE.
Update the version in the protobuf module's gradle.properties file:
# File: {proto-module}/gradle.properties
# Before: version=1.2.3
# After: version=2.0.0 (BREAKING CHANGE - MAJOR bump required)
# {proto-module}/gradle.properties
version=2.0.0
Why MAJOR version bump is mandatory:
StatusRuntimeException instead of Failure entityFailure to bump MAJOR version will cause:
service_v{N}.proto) — do NOT add to an existing fileAccountServiceV{N})protos.model)@replacedBy: ServiceNameV{N}.methodNameoption deprecated = true to old service, RPC methods, and superseded messagessince v{CURRENT}, forRemoval planned for v{NEXT_MAJOR})Failure entity from new versioned response messagesoneof patterns in new versioned service responsesAccountResponseV{N})./gradlew generateProtogoogle.rpc.Status with addDetails() for business errorsgradle.properties (e.g., 1.2.3 → 2.0.0){proto-module}/gradle.properties./gradlew clean buildgrpc-compliance-validate-repository --dir ../gradlew testTransferServiceV{N} with clean method namestransferV{N}() to an existing service*_v{N}.proto file for the new versioned service and its response messages*_v{N}.proto file, removing an old version is as simple as deleting that fileprotos.model Package for NEW Versioned Servicescom.bitso.iba.rate.v2package protos.model; for NEW versioned servicesFailureHelper in grpc module BEFORE implementing handlers{grpc-module}/src/main/java/com/bitso/{service}/grpc/util/FailureHelper.java./gradlew generateProto after proto changes./gradlew clean build before creating PRoption deprecated)@replacedBy comments AND option deprecated = true to old service, RPCs, and superseded messages@deprecated comments without the protobuf optionoption deprecated = true generates @Deprecated in Java/Kotlin/Go, providing programmatic deprecation signals// Before: Using original service with oneof Failure
TransferServiceGrpc.TransferServiceBlockingStub stub =
TransferServiceGrpc.newBlockingStub(channel);
try {
TransferResponse response = stub.transfer(request);
if (response.hasFailure()) {
// Handle failure
}
} catch (Exception e) {
// Handle error
}
// After: Using new versioned service with StatusRuntimeException
TransferServiceV2Grpc.TransferServiceV2BlockingStub stubV2 =
TransferServiceV2Grpc.newBlockingStub(channel);
try {
TransferResponseV2 response = stubV2.transfer(request); // Same method name
// Process success
} catch (StatusRuntimeException e) {
Status.Code code = e.getStatus().getCode();
if (code == Status.Code.FAILED_PRECONDITION) {
// Extract business error details from google.rpc.Status
com.google.rpc.Status status = StatusProto.fromThrowable(e);
if (status != null && !status.getDetailsList().isEmpty()) {
for (Any detail : status.getDetailsList()) {
if (detail.is(TransferError.class)) {
TransferError error = detail.unpack(TransferError.class);
// Handle business error
}
}
}
}
}
references/ERROR_PATTERNS.md - Detailed error handling implementation patterns