Skip to content

Commit 0441ea8

Browse files
authored
feat: add error details support (#442)
Signed-off-by: Yordis Prieto <[email protected]>
1 parent 123ee51 commit 0441ea8

File tree

17 files changed

+820
-1730
lines changed

17 files changed

+820
-1730
lines changed

lib/google/api/annotations.pb.ex

Lines changed: 0 additions & 8 deletions
This file was deleted.

lib/google/api/http.pb.ex

Lines changed: 0 additions & 43 deletions
This file was deleted.

lib/grpc/client/adapters/gun.ex

Lines changed: 55 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -155,24 +155,59 @@ defmodule GRPC.Client.Adapters.Gun do
155155
) do
156156
%{channel: %{adapter_payload: adapter_payload}, payload: payload} = stream
157157

158-
with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts) do
159-
response = response_stream(is_fin, stream, opts)
158+
case recv_headers(adapter_payload, payload, opts) do
159+
{:ok, headers, :fin} ->
160+
handle_fin_response(headers, opts)
160161

161-
if(opts[:return_headers]) do
162-
{:ok, response, %{headers: headers}}
163-
else
164-
{:ok, response}
165-
end
162+
{:ok, headers, :nofin} ->
163+
handle_streaming_nofin_response(stream, headers, opts)
164+
165+
{:error, _} = error ->
166+
error
166167
end
167168
end
168169

169170
def receive_data(stream, opts) do
170171
%{payload: payload, channel: %{adapter_payload: adapter_payload}} = stream
171172

172-
with {:ok, headers, _is_fin} <- recv_headers(adapter_payload, payload, opts),
173-
{:ok, body, trailers} <- recv_body(adapter_payload, payload, opts),
173+
case recv_headers(adapter_payload, payload, opts) do
174+
{:ok, headers, :fin} ->
175+
handle_fin_response(headers, opts)
176+
177+
{:ok, headers, :nofin} ->
178+
handle_nofin_response(adapter_payload, payload, stream, headers, opts)
179+
180+
{:error, _} = error ->
181+
error
182+
end
183+
end
184+
185+
defp handle_fin_response(headers, opts) do
186+
# Trailers-only response: headers contain trailers, check status
187+
with :ok <- parse_trailers(headers) do
188+
if opts[:return_headers] do
189+
{:ok, [], %{headers: headers}}
190+
else
191+
{:ok, []}
192+
end
193+
end
194+
end
195+
196+
defp handle_streaming_nofin_response(stream, headers, opts) do
197+
response = response_stream(:nofin, stream, opts)
198+
199+
if opts[:return_headers] do
200+
{:ok, response, %{headers: headers}}
201+
else
202+
{:ok, response}
203+
end
204+
end
205+
206+
defp handle_nofin_response(adapter_payload, payload, stream, headers, opts) do
207+
# Regular response: fetch body and trailers
208+
with {:ok, body, trailers} <- recv_body(adapter_payload, payload, opts),
174209
{:ok, response} <- parse_response(stream, headers, body, trailers) do
175-
if(opts[:return_headers]) do
210+
if opts[:return_headers] do
176211
{:ok, response, %{headers: headers, trailers: trailers}}
177212
else
178213
{:ok, response}
@@ -230,25 +265,7 @@ defmodule GRPC.Client.Adapters.Gun do
230265
{:response, :fin, status, headers} ->
231266
if status == 200 do
232267
headers = GRPC.Transport.HTTP2.decode_headers(headers)
233-
234-
case headers["grpc-status"] do
235-
nil ->
236-
{:error,
237-
GRPC.RPCError.exception(
238-
GRPC.Status.internal(),
239-
"shouldn't finish when getting headers"
240-
)}
241-
242-
"0" ->
243-
{:response, headers, :fin}
244-
245-
_ ->
246-
{:error,
247-
GRPC.RPCError.exception(
248-
String.to_integer(headers["grpc-status"]),
249-
headers["grpc-message"]
250-
)}
251-
end
268+
{:response, headers, :fin}
252269
else
253270
{:error,
254271
GRPC.RPCError.exception(
@@ -260,16 +277,7 @@ defmodule GRPC.Client.Adapters.Gun do
260277
{:response, :nofin, status, headers} ->
261278
if status == 200 do
262279
headers = GRPC.Transport.HTTP2.decode_headers(headers)
263-
264-
if headers["grpc-status"] && headers["grpc-status"] != "0" do
265-
{:error,
266-
GRPC.RPCError.exception(
267-
String.to_integer(headers["grpc-status"]),
268-
headers["grpc-message"]
269-
)}
270-
else
271-
{:response, headers, :nofin}
272-
end
280+
{:response, headers, :nofin}
273281
else
274282
{:error,
275283
GRPC.RPCError.exception(
@@ -349,8 +357,6 @@ defmodule GRPC.Client.Adapters.Gun do
349357
end
350358
end
351359

352-
defp response_stream(:fin, _stream, _opts), do: []
353-
354360
defp response_stream(
355361
:nofin,
356362
%{
@@ -453,7 +459,14 @@ defmodule GRPC.Client.Adapters.Gun do
453459
if status == GRPC.Status.ok() do
454460
:ok
455461
else
456-
{:error, %GRPC.RPCError{status: status, message: trailers["grpc-message"]}}
462+
rpc_error =
463+
GRPC.RPCError.from_grpc_status_details_bin(%{
464+
status: status,
465+
message: trailers["grpc-message"],
466+
encoded_details_bin: trailers["grpc-status-details-bin"]
467+
})
468+
469+
{:error, rpc_error}
457470
end
458471
end
459472

lib/grpc/client/adapters/mint/stream_response_process.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,13 @@ defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do
182182
if status == GRPC.Status.ok() do
183183
{type, decoded_trailers}
184184
else
185-
rpc_error = %GRPC.RPCError{status: status, message: decoded_trailers["grpc-message"]}
185+
rpc_error =
186+
GRPC.RPCError.from_grpc_status_details_bin(%{
187+
status: status,
188+
message: decoded_trailers["grpc-message"],
189+
encoded_details_bin: decoded_trailers["grpc-status-details-bin"]
190+
})
191+
186192
{:error, rpc_error}
187193
end
188194
end

lib/grpc/google/rpc.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule GRPC.Google.RPC do
2+
@moduledoc false
3+
4+
@spec encode_status(Google.Rpc.Status.t()) :: String.t()
5+
def encode_status(%Google.Rpc.Status{} = status) do
6+
status
7+
|> Google.Rpc.Status.encode()
8+
|> Base.encode64(padding: true)
9+
end
10+
11+
@spec decode_status(String.t()) :: {:ok, Google.Rpc.Status.t()} | {:error, term()}
12+
def decode_status(encoded_details_bin) when is_binary(encoded_details_bin) do
13+
{:ok,
14+
encoded_details_bin
15+
|> decode64()
16+
|> Google.Rpc.Status.decode()}
17+
rescue
18+
e -> {:error, e}
19+
end
20+
21+
defp decode64(str) when rem(byte_size(str), 4) == 0 do
22+
Base.decode64!(str, padding: true)
23+
end
24+
25+
defp decode64(str) do
26+
Base.decode64!(str, padding: false)
27+
end
28+
end

lib/grpc/rpc_error.ex

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,15 @@ defmodule GRPC.RPCError do
5151
See `GRPC.Status` for more details on possible statuses.
5252
"""
5353

54-
defexception [:status, :message]
54+
defexception [:status, :message, :details]
5555

5656
defguard is_rpc_error(e, status) when is_struct(e, __MODULE__) and e.status == status
5757

58-
@type t :: %__MODULE__{status: GRPC.Status.t(), message: String.t()}
58+
@type t :: %__MODULE__{
59+
status: GRPC.Status.t(),
60+
message: String.t(),
61+
details: [Google.Protobuf.Any.t()]
62+
}
5963

6064
alias GRPC.Status
6165

@@ -68,7 +72,11 @@ defmodule GRPC.RPCError do
6872
def exception(args) when is_list(args) do
6973
error = parse_args(args, %__MODULE__{})
7074

71-
%{error | message: error.message || Status.status_message(error.status)}
75+
%{
76+
error
77+
| message: error.message || Status.status_message(error.status),
78+
details: error.details
79+
}
7280
end
7381

7482
defp parse_args([], acc), do: acc
@@ -88,6 +96,11 @@ defmodule GRPC.RPCError do
8896
parse_args(t, acc)
8997
end
9098

99+
defp parse_args([{:details, details} | t], acc) when is_list(details) do
100+
acc = %{acc | details: details}
101+
parse_args(t, acc)
102+
end
103+
91104
@spec exception(status :: Status.t() | atom(), message :: String.t()) :: t()
92105
def exception(status, message) when is_atom(status) do
93106
%GRPC.RPCError{status: apply(GRPC.Status, status, []), message: message}
@@ -96,4 +109,28 @@ defmodule GRPC.RPCError do
96109
def exception(status, message) when is_integer(status) do
97110
%GRPC.RPCError{status: status, message: message}
98111
end
112+
113+
@doc false
114+
def from_grpc_status_details_bin(%{
115+
status: status,
116+
message: message,
117+
encoded_details_bin: encoded_details_bin
118+
})
119+
when is_binary(encoded_details_bin) do
120+
case GRPC.Google.RPC.decode_status(encoded_details_bin) do
121+
{:ok, rpc_status} ->
122+
%__MODULE__{
123+
status: status,
124+
message: rpc_status.message,
125+
details: rpc_status.details
126+
}
127+
128+
{:error, _} ->
129+
%__MODULE__{status: status, message: message}
130+
end
131+
end
132+
133+
def from_grpc_status_details_bin(%{status: status, message: message}) do
134+
%__MODULE__{status: status, message: message}
135+
end
99136
end

lib/grpc/server/adapters/cowboy/handler.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -664,7 +664,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
664664
defp preflight?(_), do: false
665665

666666
defp send_error(req, error, state, reason) do
667-
trailers = HTTP2.server_trailers(error.status, error.message)
667+
trailers = HTTP2.server_trailers(error.status, error.message, error.details)
668668

669669
status =
670670
if state.access_mode == :http_transcoding,

lib/grpc/transport/http2.ex

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,27 @@ defmodule GRPC.Transport.HTTP2 do
2121
%{"content-type" => "application/grpc+#{codec_name(codec)}"}
2222
end
2323

24-
@spec server_trailers(integer, String.t()) :: map
25-
def server_trailers(status \\ Status.ok(), message \\ "") do
24+
@spec server_trailers(integer, String.t(), [Google.Protobuf.Any.t()] | nil) :: map
25+
def server_trailers(status \\ Status.ok(), message \\ "", details \\ nil) do
2626
%{
2727
"grpc-status" => Integer.to_string(status),
2828
"grpc-message" => URI.encode(message)
2929
}
30+
|> put_details_bin_grpc_status(status, message, details)
31+
end
32+
33+
defp put_details_bin_grpc_status(trailers, _status, _message, nil), do: trailers
34+
defp put_details_bin_grpc_status(trailers, _status, _message, []), do: trailers
35+
36+
defp put_details_bin_grpc_status(trailers, status, message, details) when is_list(details) do
37+
encoded_details =
38+
GRPC.Google.RPC.encode_status(%Google.Rpc.Status{
39+
code: status,
40+
message: message,
41+
details: details
42+
})
43+
44+
Map.put(trailers, "grpc-status-details-bin", encoded_details)
3045
end
3146

3247
@doc """
@@ -141,7 +156,7 @@ defmodule GRPC.Transport.HTTP2 do
141156
end
142157

143158
defp encode_metadata_pair({key, val}) do
144-
val = if String.ends_with?(key, "-bin"), do: Base.encode64(val), else: val
159+
val = if String.ends_with?(key, "-bin"), do: Base.encode64(val, padding: true), else: val
145160
{String.downcase(to_string(key)), val}
146161
end
147162

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ defmodule GRPC.Mixfile do
3535

3636
defp deps do
3737
[
38+
{:googleapis, "~> 0.1.0"},
3839
{:cowboy, "~> 2.10"},
3940
{:flow, "~> 1.2"},
4041
{:gun, "~> 2.0"},

mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"ex_parameterized": {:hex, :ex_parameterized, "1.3.7", "801f85fc4651cb51f11b9835864c6ed8c5e5d79b1253506b5bb5421e8ab2f050", [:mix], [], "hexpm", "1fb0dc4aa9e8c12ae23806d03bcd64a5a0fc9cd3f4c5602ba72561c9b54a625c"},
88
"flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"},
99
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
10+
"googleapis": {:hex, :googleapis, "0.1.0", "13770f3f75f5b863fb9acf41633c7bc71bad788f3f553b66481a096d083ee20e", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1989a7244fd17d3eb5f3de311a022b656c3736b39740db46506157c4604bd212"},
1011
"gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"},
1112
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
1213
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},

0 commit comments

Comments
 (0)