Turning gRPC failures into the %TypeDB.Error{} the sibling driver returns.
This module exists so that the two transports are interchangeable at the call
site. An application that routes failures by kind, by code, or through
TypeDB.Error.retryable?/1 keeps every branch it wrote when it switches, and
that is the whole reason typedb_grpc depends on typedb rather than
defining error types of its own.
Where a TypeDB error code lives on this transport
Nowhere obvious, which is why this is a module and not a function. A gRPC
failure arrives as a GRPC.RPCError whose message is a category
("Unauthenticated", "Request generated error") rather than anything about
the request. The useful parts are in details, as two google.rpc messages
that TypeDB attaches by hand:
ErrorInfo.reason— the TypeDB code,"AUT1","DBD1","STC2". The same codes the HTTP API puts in its JSON body.DebugInfo.stack_entries— the human messages, most specific first.
Measured against 3.12.1: deleting a database that does not exist arrives as
gRPC status 3 with reason: "DBD1" and stack entries
["[DBD1] Cannot delete database since it does not exist.", "[SRV13] Unable to delete database."].
Why :status carries an HTTP status on a transport that has none
%TypeDB.Error{} documents :status as the HTTP status, and there is no HTTP
here. It is filled anyway, with the standard gRPC-to-HTTP equivalence that
grpc-gateway and every other bridge uses, because TypeDB.Error.retryable?/1
judges :server errors by status — and a nil there would quietly make the
same failure retryable over one transport and terminal over the other. That
divergence is exactly what sharing the struct is meant to prevent.
The gRPC status is not lost: it is in :reason, as {:grpc_status, integer}.
The one status whose kind depends on more than the status
INTERNAL (13) is :server when it carries details and :transport when it
carries none. TypeDB attaches ErrorInfo and DebugInfo to every error it
generates, so an INTERNAL with neither did not come from TypeDB — it is the
gRPC stack reporting a failure of its own, and the one this rule was written
for is a connection that never came up. A plaintext client against a TLS port
is the reproducible case: it arrives as INTERNAL with
:connection_error: {:protocol_error, :"Invalid connection preface received"}
and no details, and calling that :server says the server answered when
nothing answered at all.
It also made one misconfiguration report two different kinds depending on
which side of a race won — :timeout when the TLS handshake simply hung,
:server when the client's HTTP/2 layer got far enough to reject the bytes.
Both are now :transport, which is what from_reason/2 has always called an
unestablished connection.
Summary
Functions
Converts anything else a gRPC call can fail with.
Converts a GRPC.RPCError into a %TypeDB.Error{}.
The HTTP status this driver reports for a gRPC status code.
Functions
@spec from_reason(term(), String.t()) :: TypeDB.Error.t()
Converts anything else a gRPC call can fail with.
The adapter reports a connection that could not be established, or a stream
that died, as plain terms rather than as GRPC.RPCError. They are transport
failures in the sense %TypeDB.Error{} means: the request produced no answer,
and trying again could produce one.
@spec from_rpc_error(GRPC.RPCError.t(), String.t()) :: TypeDB.Error.t()
Converts a GRPC.RPCError into a %TypeDB.Error{}.
context is prepended to the message when the server gave nothing better —
it names the operation, so that a bare "Request generated error" still tells
the reader what was being attempted.
@spec http_status(integer()) :: pos_integer()
The HTTP status this driver reports for a gRPC status code.
iex> TypeDB.GRPC.Error.http_status(16)
401
iex> TypeDB.GRPC.Error.http_status(14)
503Unknown codes become 500: a gRPC status this driver has not seen is the
server behaving in a way nobody predicted, which is what 500 means.