Skip to content

Commit 0b9d7cd

Browse files
committed
refactor: replace cowlib modules with hackney-native implementations
- Replace hackney_cow_http2_machine with hackney_http2_machine - Replace hackney_cow_http2 with hackney_http2 - Replace hackney_cow_deflate with hackney_deflate - Replace hackney_cow_ws with hackney_ws_proto - Remove hackney_cow_hpack (already replaced by hackney_hpack) Add h2spec HTTP/2 compliance testing: - h2spec_server.erl: Minimal HTTP/2 server for compliance testing - h2spec_SUITE.erl: CT suite for running h2spec tests - 95% compliance rate (139/146 tests pass) Add E2E testing against real HTTP/2 servers: - hackney_http2_e2e_SUITE.erl: Tests against nghttp2.org, Google, Cloudflare - Validates HTTP/2 works with production servers Add HTTP/2 machine benchmarks: - hackney_http2_machine_bench.erl: Benchmark suite for stream operations Key optimizations in hackney_http2_machine: - Stream caching for recently accessed streams - gb_sets for lingering streams (O(log N) vs O(N)) - IOList accumulation for header fragments
1 parent 21612ca commit 0b9d7cd

17 files changed

Lines changed: 1333 additions & 1293 deletions

Makefile

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,32 @@ quic-deps-update:
3838
quic-clean:
3939
@rm -rf _build/cmake
4040
@rm -f priv/hackney_quic.so priv/hackney_quic.dll
41+
42+
# h2spec HTTP/2 compliance testing
43+
H2SPEC_VERSION ?= 2.6.0
44+
45+
priv/h2spec:
46+
@mkdir -p priv
47+
@echo "Downloading h2spec v$(H2SPEC_VERSION)..."
48+
@OS=$$(uname -s | tr '[:upper:]' '[:lower:]'); \
49+
ARCH=$$(uname -m); \
50+
if [ "$$ARCH" = "x86_64" ]; then ARCH="amd64"; fi; \
51+
if [ "$$ARCH" = "aarch64" ] || [ "$$ARCH" = "arm64" ]; then \
52+
if [ "$$OS" = "darwin" ]; then ARCH="amd64"; else ARCH="arm64"; fi; \
53+
fi; \
54+
curl -sL "https://github.com/summerwind/h2spec/releases/download/v$(H2SPEC_VERSION)/h2spec_$${OS}_$${ARCH}.tar.gz" | \
55+
tar xz -C priv
56+
57+
download-h2spec: priv/h2spec
58+
@echo "h2spec installed at priv/h2spec"
59+
60+
h2spec-test: priv/h2spec compile
61+
@${REBAR} ct --suite=h2spec_SUITE
62+
63+
e2e-test: compile
64+
@${REBAR} ct --suite=hackney_http2_e2e_SUITE
65+
66+
http2-bench: compile
67+
@${REBAR} as test compile
68+
@erl -pa _build/test/lib/*/ebin -pa _build/test/lib/hackney/test -noshell \
69+
-eval 'hackney_http2_machine_bench:run().' -s init stop

src/hackney_conn.erl

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
%% HTTP/2 support
169169
%% Protocol negotiated via ALPN (http1, http2, or http3)
170170
protocol = http1 :: http1 | http2 | http3,
171-
%% HTTP/2 connection state machine (from hackney_cow_http2_machine)
171+
%% HTTP/2 connection state machine (from hackney_http2_machine)
172172
h2_machine :: tuple() | undefined,
173173
%% Map of active HTTP/2 streams: StreamId => {From, StreamState}
174174
%% StreamState: waiting_headers | waiting_body | done | {push, Headers}
@@ -2256,7 +2256,7 @@ do_tcp_connect(From, Data) ->
22562256
init_h2_connection(Socket, Data, From) ->
22572257
#conn_data{transport = Transport} = Data,
22582258
%% Initialize HTTP/2 state machine in client mode
2259-
{ok, Preface, H2Machine} = hackney_cow_http2_machine:init(client, #{}),
2259+
{ok, Preface, H2Machine} = hackney_http2_machine:init(client, #{}),
22602260
%% Send HTTP/2 connection preface (includes SETTINGS frame)
22612261
case Transport:send(Socket, Preface) of
22622262
ok ->
@@ -2280,7 +2280,7 @@ init_h2_connection(Socket, Data, From) ->
22802280
init_h2_after_upgrade(SslSocket, Data, From) ->
22812281
#conn_data{transport = Transport} = Data,
22822282
%% Initialize HTTP/2 state machine in client mode
2283-
{ok, Preface, H2Machine} = hackney_cow_http2_machine:init(client, #{}),
2283+
{ok, Preface, H2Machine} = hackney_http2_machine:init(client, #{}),
22842284
%% Send HTTP/2 connection preface (includes SETTINGS frame)
22852285
case Transport:send(SslSocket, Preface) of
22862286
ok ->
@@ -2312,7 +2312,7 @@ do_h2_request(From, Method, Path, Headers, Body, Data) ->
23122312

23132313
%% Initialize a new stream
23142314
MethodBin = to_binary(Method),
2315-
{ok, StreamId, H2Machine1} = hackney_cow_http2_machine:init_stream(MethodBin, H2Machine0),
2315+
{ok, StreamId, H2Machine1} = hackney_http2_machine:init_stream(MethodBin, H2Machine0),
23162316

23172317
%% Build pseudo-headers
23182318
Scheme = case Transport of
@@ -2343,9 +2343,9 @@ do_h2_request(From, Method, Path, Headers, Body, Data) ->
23432343
end,
23442344

23452345
%% Prepare headers using h2_machine
2346-
{ok, _IsFin2, HeaderBlock, H2Machine2} = hackney_cow_http2_machine:prepare_headers(StreamId, H2Machine1, IsFin, PseudoHeaders, H2Headers),
2346+
{ok, _IsFin2, HeaderBlock, H2Machine2} = hackney_http2_machine:prepare_headers(StreamId, H2Machine1, IsFin, PseudoHeaders, H2Headers),
23472347
%% Build and send HEADERS frame
2348-
HeadersFrame = hackney_cow_http2:headers(StreamId, IsFin, HeaderBlock),
2348+
HeadersFrame = hackney_http2:headers(StreamId, IsFin, HeaderBlock),
23492349
case Transport:send(Socket, HeadersFrame) of
23502350
ok ->
23512351
%% Send body if present
@@ -2392,7 +2392,7 @@ do_h2_request_async(From, Method, Path, Headers, Body, AsyncMode, StreamTo, _Fol
23922392

23932393
%% Initialize a new stream
23942394
MethodBin = to_binary(Method),
2395-
{ok, StreamId, H2Machine1} = hackney_cow_http2_machine:init_stream(MethodBin, H2Machine0),
2395+
{ok, StreamId, H2Machine1} = hackney_http2_machine:init_stream(MethodBin, H2Machine0),
23962396

23972397
%% Build pseudo-headers
23982398
Scheme = case Transport of
@@ -2423,9 +2423,9 @@ do_h2_request_async(From, Method, Path, Headers, Body, AsyncMode, StreamTo, _Fol
24232423
end,
24242424

24252425
%% Prepare headers using h2_machine
2426-
{ok, _IsFin2, HeaderBlock, H2Machine2} = hackney_cow_http2_machine:prepare_headers(StreamId, H2Machine1, IsFin, PseudoHeaders, H2Headers),
2426+
{ok, _IsFin2, HeaderBlock, H2Machine2} = hackney_http2_machine:prepare_headers(StreamId, H2Machine1, IsFin, PseudoHeaders, H2Headers),
24272427
%% Build and send HEADERS frame
2428-
HeadersFrame = hackney_cow_http2:headers(StreamId, IsFin, HeaderBlock),
2428+
HeadersFrame = hackney_http2:headers(StreamId, IsFin, HeaderBlock),
24292429
case Transport:send(Socket, HeadersFrame) of
24302430
ok ->
24312431
%% Send body if present
@@ -2468,7 +2468,7 @@ send_h2_body(Transport, Socket, StreamId, Body, H2Machine) ->
24682468
%% For now, send entire body in one DATA frame
24692469
%% TODO: Respect flow control window and split large bodies
24702470
BodyBin = iolist_to_binary(Body),
2471-
DataFrame = hackney_cow_http2:data(StreamId, fin, BodyBin),
2471+
DataFrame = hackney_http2:data(StreamId, fin, BodyBin),
24722472
case Transport:send(Socket, DataFrame) of
24732473
ok -> {ok, H2Machine};
24742474
{error, Reason} -> {error, Reason}
@@ -2543,7 +2543,7 @@ handle_h2_data(RecvData, Data) ->
25432543

25442544
%% @private Parse HTTP/2 frames from buffer
25452545
parse_h2_frames(Buffer, Data) ->
2546-
case hackney_cow_http2:parse(Buffer) of
2546+
case hackney_http2:parse(Buffer) of
25472547
{ok, Frame, Rest} ->
25482548
case handle_h2_frame(Frame, Data) of
25492549
{ok, NewData} ->
@@ -2563,10 +2563,10 @@ parse_h2_frames(Buffer, Data) ->
25632563
handle_h2_frame({settings, Settings}, Data) ->
25642564
%% Server SETTINGS frame - acknowledge and update machine
25652565
#conn_data{h2_machine = H2Machine, transport = Transport, socket = Socket} = Data,
2566-
case hackney_cow_http2_machine:frame({settings, Settings}, H2Machine) of
2566+
case hackney_http2_machine:frame({settings, Settings}, H2Machine) of
25672567
{ok, H2Machine2} ->
25682568
%% Send SETTINGS ACK
2569-
SettingsAck = hackney_cow_http2:settings_ack(),
2569+
SettingsAck = hackney_http2:settings_ack(),
25702570
case Transport:send(Socket, SettingsAck) of
25712571
ok -> {ok, Data#conn_data{h2_machine = H2Machine2}};
25722572
{error, Reason} -> {error, Reason, Data}
@@ -2578,7 +2578,7 @@ handle_h2_frame({settings, Settings}, Data) ->
25782578
handle_h2_frame(settings_ack, Data) ->
25792579
%% Server acknowledged our SETTINGS
25802580
#conn_data{h2_machine = H2Machine} = Data,
2581-
case hackney_cow_http2_machine:frame(settings_ack, H2Machine) of
2581+
case hackney_http2_machine:frame(settings_ack, H2Machine) of
25822582
{ok, H2Machine2} ->
25832583
{ok, Data#conn_data{h2_machine = H2Machine2}};
25842584
{error, Reason, _H2Machine} ->
@@ -2599,7 +2599,7 @@ handle_h2_frame({headers, StreamId, IsFin, HeadFin, Exclusive, DepStreamId, Weig
25992599
handle_h2_frame({data, StreamId, IsFin, BodyData}, Data) ->
26002600
%% Response body data received
26012601
#conn_data{h2_machine = H2Machine} = Data,
2602-
case hackney_cow_http2_machine:frame({data, StreamId, IsFin, BodyData}, H2Machine) of
2602+
case hackney_http2_machine:frame({data, StreamId, IsFin, BodyData}, H2Machine) of
26032603
{ok, {data, StreamId, RespIsFin, RespData}, H2Machine2} ->
26042604
%% h2_machine may return processed data
26052605
do_handle_h2_data_body(StreamId, RespIsFin, RespData, H2Machine2, Data);
@@ -2613,9 +2613,9 @@ handle_h2_frame({data, StreamId, IsFin, BodyData}, Data) ->
26132613
handle_h2_frame({ping, Opaque}, Data) ->
26142614
%% Respond to PING with PING ACK
26152615
#conn_data{transport = Transport, socket = Socket, h2_machine = H2Machine} = Data,
2616-
case hackney_cow_http2_machine:frame({ping, Opaque}, H2Machine) of
2616+
case hackney_http2_machine:frame({ping, Opaque}, H2Machine) of
26172617
{ok, H2Machine2} ->
2618-
PingAck = hackney_cow_http2:ping_ack(Opaque),
2618+
PingAck = hackney_http2:ping_ack(Opaque),
26192619
Transport:send(Socket, PingAck),
26202620
{ok, Data#conn_data{h2_machine = H2Machine2}};
26212621
{error, Reason, _H2Machine} ->
@@ -2625,7 +2625,7 @@ handle_h2_frame({ping, Opaque}, Data) ->
26252625
handle_h2_frame({ping_ack, _Opaque}, Data) ->
26262626
%% PING ACK received - ignore
26272627
#conn_data{h2_machine = H2Machine} = Data,
2628-
case hackney_cow_http2_machine:frame({ping_ack, _Opaque}, H2Machine) of
2628+
case hackney_http2_machine:frame({ping_ack, _Opaque}, H2Machine) of
26292629
{ok, H2Machine2} ->
26302630
{ok, Data#conn_data{h2_machine = H2Machine2}};
26312631
{error, Reason, _H2Machine} ->
@@ -2635,7 +2635,7 @@ handle_h2_frame({ping_ack, _Opaque}, Data) ->
26352635
handle_h2_frame({goaway, LastStreamId, ErrorCode, _DebugData}, Data) ->
26362636
%% Server is going away
26372637
#conn_data{h2_machine = H2Machine} = Data,
2638-
case hackney_cow_http2_machine:frame({goaway, LastStreamId, ErrorCode, _DebugData}, H2Machine) of
2638+
case hackney_http2_machine:frame({goaway, LastStreamId, ErrorCode, _DebugData}, H2Machine) of
26392639
{ok, {goaway, _, _, _}, H2Machine2} ->
26402640
{error, {goaway, ErrorCode}, Data#conn_data{h2_machine = H2Machine2}};
26412641
{ok, H2Machine2} ->
@@ -2647,7 +2647,7 @@ handle_h2_frame({goaway, LastStreamId, ErrorCode, _DebugData}, Data) ->
26472647
handle_h2_frame({window_update, Increment}, Data) ->
26482648
%% Connection-level window update
26492649
#conn_data{h2_machine = H2Machine} = Data,
2650-
case hackney_cow_http2_machine:frame({window_update, Increment}, H2Machine) of
2650+
case hackney_http2_machine:frame({window_update, Increment}, H2Machine) of
26512651
{ok, H2Machine2} ->
26522652
{ok, Data#conn_data{h2_machine = H2Machine2}};
26532653
{error, Reason, _H2Machine} ->
@@ -2657,7 +2657,7 @@ handle_h2_frame({window_update, Increment}, Data) ->
26572657
handle_h2_frame({window_update, StreamId, Increment}, Data) ->
26582658
%% Stream-level window update
26592659
#conn_data{h2_machine = H2Machine} = Data,
2660-
case hackney_cow_http2_machine:frame({window_update, StreamId, Increment}, H2Machine) of
2660+
case hackney_http2_machine:frame({window_update, StreamId, Increment}, H2Machine) of
26612661
{ok, H2Machine2} ->
26622662
{ok, Data#conn_data{h2_machine = H2Machine2}};
26632663
{error, Reason, _H2Machine} ->
@@ -2681,13 +2681,13 @@ handle_h2_frame({push_promise, StreamId, HeadFin, PromisedStreamId, HeaderBlock}
26812681
#conn_data{h2_machine = H2Machine, transport = Transport, socket = Socket,
26822682
enable_push = EnablePush, h2_streams = Streams} = Data,
26832683
Frame = {push_promise, StreamId, HeadFin, PromisedStreamId, HeaderBlock},
2684-
case hackney_cow_http2_machine:frame(Frame, H2Machine) of
2684+
case hackney_http2_machine:frame(Frame, H2Machine) of
26852685
{ok, {push_promise, OriginStreamId, PromisedStreamId2, Headers, PseudoHeaders}, H2Machine2} ->
26862686
handle_push_promise(EnablePush, OriginStreamId, PromisedStreamId2, Headers, PseudoHeaders,
26872687
Streams, H2Machine2, Transport, Socket, Data);
26882688
{ok, H2Machine2} ->
26892689
%% Machine processed but didn't decode headers - reject
2690-
RstFrame = hackney_cow_http2:rst_stream(PromisedStreamId, refused_stream),
2690+
RstFrame = hackney_http2:rst_stream(PromisedStreamId, refused_stream),
26912691
Transport:send(Socket, RstFrame),
26922692
{ok, Data#conn_data{h2_machine = H2Machine2}};
26932693
{error, Reason, _H2Machine} ->
@@ -2702,7 +2702,7 @@ handle_h2_frame(_Frame, Data) ->
27022702
handle_push_promise(false, _OriginStreamId, PromisedStreamId, _Headers, _PseudoHeaders,
27032703
_Streams, H2Machine, Transport, Socket, Data) ->
27042704
%% Push disabled - reject with RST_STREAM (REFUSED_STREAM = 0x7)
2705-
RstFrame = hackney_cow_http2:rst_stream(PromisedStreamId, refused_stream),
2705+
RstFrame = hackney_http2:rst_stream(PromisedStreamId, refused_stream),
27062706
Transport:send(Socket, RstFrame),
27072707
{ok, Data#conn_data{h2_machine = H2Machine}};
27082708
handle_push_promise(PushHandler, OriginStreamId, PromisedStreamId, Headers, PseudoHeaders,
@@ -2799,31 +2799,31 @@ do_handle_h2_data_body(StreamId, IsFin, BodyData, H2Machine0, Data) ->
27992799
%% Type can be 'connection' for connection-level or StreamId for stream-level
28002800
maybe_send_window_update(connection, DataLen, H2Machine, Transport, Socket) ->
28012801
%% Check connection-level window
2802-
case hackney_cow_http2_machine:ensure_window(DataLen, H2Machine) of
2802+
case hackney_http2_machine:ensure_window(DataLen, H2Machine) of
28032803
ok ->
28042804
{H2Machine, ok};
28052805
{ok, Increment, H2Machine2} ->
28062806
%% Send connection-level WINDOW_UPDATE
2807-
Frame = hackney_cow_http2:window_update(Increment),
2807+
Frame = hackney_http2:window_update(Increment),
28082808
_ = Transport:send(Socket, Frame),
28092809
{H2Machine2, ok}
28102810
end;
28112811
maybe_send_window_update(StreamId, DataLen, H2Machine, Transport, Socket) when is_integer(StreamId) ->
28122812
%% Check stream-level window
2813-
case hackney_cow_http2_machine:ensure_window(StreamId, DataLen, H2Machine) of
2813+
case hackney_http2_machine:ensure_window(StreamId, DataLen, H2Machine) of
28142814
ok ->
28152815
{H2Machine, ok};
28162816
{ok, Increment, H2Machine2} ->
28172817
%% Send stream-level WINDOW_UPDATE
2818-
Frame = hackney_cow_http2:window_update(StreamId, Increment),
2818+
Frame = hackney_http2:window_update(StreamId, Increment),
28192819
_ = Transport:send(Socket, Frame),
28202820
{H2Machine2, ok}
28212821
end.
28222822

28232823
%% @private Handle HTTP/2 headers response
28242824
do_handle_h2_headers(StreamId, _IsFin, Frame, Data) ->
28252825
#conn_data{h2_machine = H2Machine, h2_streams = Streams} = Data,
2826-
case hackney_cow_http2_machine:frame(Frame, H2Machine) of
2826+
case hackney_http2_machine:frame(Frame, H2Machine) of
28272827
{ok, {headers, StreamId, RespIsFin, Headers, PseudoHeaders, _Len}, H2Machine2} ->
28282828
%% Response headers received
28292829
%% PseudoHeaders contains #{status => Status} for responses
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1919
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
2020

21-
-module(hackney_cow_deflate).
21+
-module(hackney_deflate).
2222

2323
-export([inflate/3]).
2424

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1818
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1919

20-
-module(hackney_cow_http2).
20+
-module(hackney_http2).
2121

2222
%% Parsing.
2323
-export([parse_sequence/1]).

0 commit comments

Comments
 (0)