|
42 | 42 | wt_send_datagram/2, |
43 | 43 | wt_session_info/1]). |
44 | 44 |
|
| 45 | +%% HTTP/2 bidirectional (gRPC-style) stream API |
| 46 | +-export([h2_open/2, h2_open/3, h2_open/4, |
| 47 | + h2_send/2, h2_send/3, |
| 48 | + h2_send_trailers/2, |
| 49 | + h2_recv/1, h2_recv/2, |
| 50 | + h2_consume/2, |
| 51 | + h2_setopts/2, |
| 52 | + h2_close/1]). |
| 53 | + |
45 | 54 | -export([redirect_location/1, location/1]). |
46 | 55 |
|
47 | 56 | -export([get_version/0]). |
@@ -1023,6 +1032,131 @@ wt_session_info(WtPid) when is_pid(WtPid) -> |
1023 | 1032 | shutdown_wt(WtPid) -> |
1024 | 1033 | try exit(WtPid, shutdown) catch _:_ -> ok end. |
1025 | 1034 |
|
| 1035 | +%%==================================================================== |
| 1036 | +%% HTTP/2 bidirectional (gRPC-style) stream API |
| 1037 | +%%==================================================================== |
| 1038 | + |
| 1039 | +%% @doc Open a full-duplex HTTP/2 stream (gRPC-style bidirectional streaming). |
| 1040 | +%% Establishes a dedicated HTTP/2 connection (ALPN, so an https URL) and opens |
| 1041 | +%% one stream on it. Returns a pid driven with h2_send/h2_recv etc. The method |
| 1042 | +%% defaults to POST. |
| 1043 | +%% |
| 1044 | +%% Options: connect_timeout, recv_timeout, connect_options, ssl_options, |
| 1045 | +%% {flow_control, auto | manual}, {active, true | false | once}, |
| 1046 | +%% {max_recv_buffer, bytes | infinity}. |
| 1047 | +-spec h2_open(binary() | string(), list()) -> {ok, pid()} | {error, term()}. |
| 1048 | +h2_open(URL, Opts) -> |
| 1049 | + h2_open(post, URL, [], Opts). |
| 1050 | + |
| 1051 | +-spec h2_open(binary() | string(), list(), list()) -> {ok, pid()} | {error, term()}. |
| 1052 | +h2_open(URL, Headers, Opts) -> |
| 1053 | + h2_open(post, URL, Headers, Opts). |
| 1054 | + |
| 1055 | +-spec h2_open(atom() | binary() | string(), binary() | string(), list(), list()) -> |
| 1056 | + {ok, pid()} | {error, term()}. |
| 1057 | +h2_open(Method, URL, Headers, Opts) -> |
| 1058 | + #hackney_url{ |
| 1059 | + transport = Transport, |
| 1060 | + scheme = Scheme, |
| 1061 | + host = Host, |
| 1062 | + port = Port, |
| 1063 | + path = Path0, |
| 1064 | + qs = Query |
| 1065 | + } = hackney_url:parse_url(URL), |
| 1066 | + case Transport of |
| 1067 | + hackney_ssl -> |
| 1068 | + Path = case Query of |
| 1069 | + <<>> -> Path0; |
| 1070 | + _ -> <<Path0/binary, "?", Query/binary>> |
| 1071 | + end, |
| 1072 | + H2Opts = #{ |
| 1073 | + method => h2_method_bin(Method), |
| 1074 | + host => Host, |
| 1075 | + port => Port, |
| 1076 | + transport => Transport, |
| 1077 | + path => Path, |
| 1078 | + headers => Headers, |
| 1079 | + connect_timeout => proplists:get_value(connect_timeout, Opts, 8000), |
| 1080 | + recv_timeout => proplists:get_value(recv_timeout, Opts, infinity), |
| 1081 | + connect_options => proplists:get_value(connect_options, Opts, []), |
| 1082 | + ssl_options => proplists:get_value(ssl_options, Opts, []), |
| 1083 | + flow_control => proplists:get_value(flow_control, Opts, auto), |
| 1084 | + active => proplists:get_value(active, Opts, false), |
| 1085 | + max_recv_buffer => proplists:get_value(max_recv_buffer, Opts, 16#4000000) |
| 1086 | + }, |
| 1087 | + case hackney_h2_stream:start_link(H2Opts) of |
| 1088 | + {ok, Pid} -> |
| 1089 | + Timeout = maps:get(connect_timeout, H2Opts), |
| 1090 | + try hackney_h2_stream:connect(Pid, Timeout) of |
| 1091 | + ok -> |
| 1092 | + {ok, Pid}; |
| 1093 | + {error, Reason} -> |
| 1094 | + shutdown_h2(Pid), |
| 1095 | + {error, Reason} |
| 1096 | + catch |
| 1097 | + exit:{timeout, _} -> |
| 1098 | + shutdown_h2(Pid), |
| 1099 | + {error, connect_timeout}; |
| 1100 | + exit:{noproc, _} -> |
| 1101 | + {error, {h2_process_died, noproc}} |
| 1102 | + end; |
| 1103 | + {error, Reason} -> |
| 1104 | + {error, Reason} |
| 1105 | + end; |
| 1106 | + _ -> |
| 1107 | + {error, {scheme_not_supported, Scheme}} |
| 1108 | + end. |
| 1109 | + |
| 1110 | +%% @doc Send a DATA frame on the stream (no END_STREAM). |
| 1111 | +-spec h2_send(pid(), iodata()) -> ok | {error, term()}. |
| 1112 | +h2_send(Pid, Data) when is_pid(Pid) -> |
| 1113 | + hackney_h2_stream:send(Pid, Data). |
| 1114 | + |
| 1115 | +%% @doc Send a DATA frame, optionally half-closing the send side (`fin'). |
| 1116 | +-spec h2_send(pid(), iodata(), fin | nofin) -> ok | {error, term()}. |
| 1117 | +h2_send(Pid, Data, Fin) when is_pid(Pid) -> |
| 1118 | + hackney_h2_stream:send(Pid, Data, Fin). |
| 1119 | + |
| 1120 | +%% @doc Send trailing HEADERS, half-closing the send side (gRPC trailers). |
| 1121 | +-spec h2_send_trailers(pid(), list()) -> ok | {error, term()}. |
| 1122 | +h2_send_trailers(Pid, Trailers) when is_pid(Pid) -> |
| 1123 | + hackney_h2_stream:send_trailers(Pid, Trailers). |
| 1124 | + |
| 1125 | +%% @doc Receive the next inbound message: {response, Status, Headers} | |
| 1126 | +%% {data, Data} | {trailers, Trailers} | done. After done, returns |
| 1127 | +%% {error, closed}. Passive mode only. |
| 1128 | +-spec h2_recv(pid()) -> {ok, hackney_h2_stream:h2_msg()} | {error, term()}. |
| 1129 | +h2_recv(Pid) when is_pid(Pid) -> |
| 1130 | + hackney_h2_stream:recv(Pid). |
| 1131 | + |
| 1132 | +-spec h2_recv(pid(), timeout()) -> {ok, hackney_h2_stream:h2_msg()} | {error, term()}. |
| 1133 | +h2_recv(Pid, Timeout) when is_pid(Pid) -> |
| 1134 | + hackney_h2_stream:recv(Pid, Timeout). |
| 1135 | + |
| 1136 | +%% @doc Acknowledge N consumed bytes (manual flow control only). |
| 1137 | +-spec h2_consume(pid(), non_neg_integer()) -> ok | {error, term()}. |
| 1138 | +h2_consume(Pid, NBytes) when is_pid(Pid) -> |
| 1139 | + hackney_h2_stream:consume(Pid, NBytes). |
| 1140 | + |
| 1141 | +%% @doc Set options. Supported: [{active, true | false | once}]. |
| 1142 | +-spec h2_setopts(pid(), list()) -> ok | {error, term()}. |
| 1143 | +h2_setopts(Pid, Opts) when is_pid(Pid) -> |
| 1144 | + hackney_h2_stream:setopts(Pid, Opts). |
| 1145 | + |
| 1146 | +%% @doc Cancel the stream and tear down its connection. |
| 1147 | +-spec h2_close(pid()) -> ok. |
| 1148 | +h2_close(Pid) when is_pid(Pid) -> |
| 1149 | + hackney_h2_stream:close(Pid). |
| 1150 | + |
| 1151 | +%% @private Normalize an HTTP method to an uppercase binary. |
| 1152 | +h2_method_bin(M) when is_binary(M) -> M; |
| 1153 | +h2_method_bin(M) when is_atom(M) -> list_to_binary(string:to_upper(atom_to_list(M))); |
| 1154 | +h2_method_bin(M) when is_list(M) -> list_to_binary(string:to_upper(M)). |
| 1155 | + |
| 1156 | +%% @private Signal the HTTP/2 stream process to shut down, ignoring errors. |
| 1157 | +shutdown_h2(Pid) -> |
| 1158 | + try exit(Pid, shutdown) catch _:_ -> ok end. |
| 1159 | + |
1026 | 1160 | %% @private Reject CR/LF/NUL in the authority, request path, or any |
1027 | 1161 | %% caller-supplied header used in the WebTransport CONNECT request |
1028 | 1162 | %% (GHSA-f9vr analog). |
|
0 commit comments