Problem
Most tapd RPC handlers return plain fmt.Errorf / wrapped errors rather than status.Error(codes.X, ...). gRPC then surfaces these as codes.Unknown, leaving clients with no programmatic way to distinguish bad input from a missing resource from an internal failure — the error string is the only signal.
Caught while wiring typed errors through tap-sdk's REST transport (lightninglabs/tap-sdk#80). REST and gRPC now agree on the code, but that agreement is trivially Unknown == Unknown for every case we tried:
| RPC |
Input |
Actual |
Expected |
DecodeAddr |
"not-a-tap-addr" |
Unknown: unable to decode addr: address: invalid bech32m string |
InvalidArgument |
DecodeProof |
[0x00, 0x01] |
Unknown: invalid raw proof, could not identify decoding format |
InvalidArgument |
ExportProof |
zero asset_id + zero script_key |
Unknown: invalid script key: ... x coordinate 0x00..00 is not on the secp256k1 curve |
InvalidArgument (bad key) or NotFound (missing proof) |
Why it matters
SDK callers (Go, tap-sdk REST, any grpc-gateway client) want to branch on the error type — retry on Unavailable, surface InvalidArgument to the user, treat NotFound as a non-error, etc. Today they have to string-match, which is fragile and locale-sensitive.
Proposal
Audit the RPC surface and replace plain fmt.Errorf returns in handler paths with status.Errorf(codes.X, ...). Obvious mappings:
- Argument parsing/decoding (bech32, proof bytes, keys, hex/base64) →
codes.InvalidArgument
- Explicit "not in DB" lookups →
codes.NotFound
- Permission/macaroon →
codes.PermissionDenied
- Shutdown / context canceled →
codes.Canceled / codes.Unavailable
- Genuine internals (DB transaction failure, etc.) can keep the default
Unknown/Internal
A stricter pattern is to introduce a small helper that wraps known sentinel errors into status codes at the RPC boundary, keeping handler internals free of grpc imports.
Problem
Most tapd RPC handlers return plain
fmt.Errorf/ wrapped errors rather thanstatus.Error(codes.X, ...). gRPC then surfaces these ascodes.Unknown, leaving clients with no programmatic way to distinguish bad input from a missing resource from an internal failure — the error string is the only signal.Caught while wiring typed errors through tap-sdk's REST transport (lightninglabs/tap-sdk#80). REST and gRPC now agree on the code, but that agreement is trivially
Unknown == Unknownfor every case we tried:DecodeAddr"not-a-tap-addr"Unknown: unable to decode addr: address: invalid bech32m stringInvalidArgumentDecodeProof[0x00, 0x01]Unknown: invalid raw proof, could not identify decoding formatInvalidArgumentExportProofUnknown: invalid script key: ... x coordinate 0x00..00 is not on the secp256k1 curveInvalidArgument(bad key) orNotFound(missing proof)Why it matters
SDK callers (Go, tap-sdk REST, any grpc-gateway client) want to branch on the error type — retry on
Unavailable, surfaceInvalidArgumentto the user, treatNotFoundas a non-error, etc. Today they have to string-match, which is fragile and locale-sensitive.Proposal
Audit the RPC surface and replace plain
fmt.Errorfreturns in handler paths withstatus.Errorf(codes.X, ...). Obvious mappings:codes.InvalidArgumentcodes.NotFoundcodes.PermissionDeniedcodes.Canceled/codes.UnavailableUnknown/InternalA stricter pattern is to introduce a small helper that wraps known sentinel errors into status codes at the RPC boundary, keeping handler internals free of grpc imports.