view release on metacpan or search on metacpan
- Add header_table_size (SETTINGS_HEADER_TABLE_SIZE) and
max_header_list_size (SETTINGS_MAX_HEADER_LIST_SIZE) to
submit_settings(), allowing servers to advertise HPACK limits
- Add max_send_header_block_length session option to new_server(),
using nghttp2_option / nghttp2_session_server_new2 to limit
outgoing header block size
- Add HPACK settings tests to t/14-hpack-headers.t
0.004 2026-02-08
- submit_response() body parameter now accepts CODE ref (previously
only data_callback parameter was supported for streaming)
- Fix submit_data() to work correctly by reusing existing data provider
instead of creating a new one (avoids nghttp2 assertion failure)
- Add submit_data test suite (t/21-submit-data.t)
- Document all public methods and callbacks in Session.pm POD
- Fix author placeholder in POD
0.003 2026-02-08
- Support streaming body callback in submit_request() for client-side
bidirectional streams (e.g. Extended CONNECT / WebSocket over HTTP/2)
- submit_request() body parameter now accepts CODE ref in addition to
string; callback receives ($stream_id, $max_length) and returns
($data, $eof_flag) or undef to defer
- Add streaming request test suite (t/20-streaming-request.t)
0.002 2025-01-14
- Add RFC 8441 (Bootstrapping WebSockets with HTTP/2) support
- Add NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL constant (0x8)
- Add enable_connect_protocol setting to send_connection_preface()
- Export new constant via :settings tag
- Add comprehensive RFC 8441 extended CONNECT protocol tests
- Add SETTINGS_ENABLE_CONNECT_PROTOCOL to Test::HTTP2::Frame helper
- Test coverage for extended CONNECT vs regular CONNECT distinction
lib/Net/HTTP2/nghttp2.pm
lib/Net/HTTP2/nghttp2/Session.pm
Makefile.PL
MANIFEST This list of files
MANIFEST.SKIP
nghttp2.c
nghttp2.xs
README.md
t/00-load.t
t/01-session.t
t/02-streaming.t
t/10-basic-server.t
t/11-invalid-frames.t
t/12-flow-control.t
t/13-stream-states.t
t/14-hpack-headers.t
t/15-continuation.t
t/16-priority.t
t/17-client.t
t/18-tls-client.t
t/19-extended-connect.t
t/20-streaming-request.t
t/21-submit-data.t
t/22-rst-rate-limit.t
t/lib/Test/HTTP2/Frame.pm
t/lib/Test/HTTP2/HPACK.pm
META.yml Module YAML meta-data (added by MakeMaker)
META.json Module JSON meta-data (added by MakeMaker)
lib/Net/HTTP2/nghttp2.pm view on Meta::CPAN
my ($stream_id, $max_length) = @_;
return undef if !data_available(); # Defers
return (get_data(), $eof);
},
# Later, when data becomes available:
$session->resume_stream($stream_id);
=head2 Streaming Request
Client-side requests also support streaming body callbacks, using the same
callback signature as streaming responses. This is essential for bidirectional
protocols like WebSocket over HTTP/2 (RFC 8441), where the client stream must
remain open for ongoing data exchange.
my @send_queue;
my $eof = 0;
my $stream_id = $session->submit_request(
method => 'POST',
path => '/upload',
scheme => 'https',
lib/Net/HTTP2/nghttp2.pm view on Meta::CPAN
my ($stream_id, $name, $value) = @_;
if ($name eq ':method' && $value eq 'CONNECT') {
# Possible extended CONNECT
}
if ($name eq ':protocol' && $value eq 'websocket') {
# This is a WebSocket upgrade request
}
return 0;
},
# Accept with 200 (not 101) and a streaming body for server-to-client:
$session->submit_response($stream_id,
status => 200,
headers => [['sec-websocket-protocol', $subprotocol]],
body => sub {
my ($stream_id, $max_length) = @_;
# Return WebSocket frames to send to client
return undef; # Defer until data ready
},
);
B<Client side> - send extended CONNECT with streaming body:
my $stream_id = $session->submit_request(
method => 'CONNECT',
path => '/chat',
scheme => 'https',
authority => 'example.com',
headers => [[':protocol', 'websocket']],
body => sub {
my ($stream_id, $max_length) = @_;
# Return WebSocket frames to send to server
lib/Net/HTTP2/nghttp2/Session.pm view on Meta::CPAN
sub new_client {
my ($class, %args) = @_;
my $callbacks = delete $args{callbacks} // {};
my $user_data = delete $args{user_data};
return $class->_new_client_xs($callbacks, $user_data);
}
# High-level request submission (client-side)
# Body can be: undef (no body), string (static body), or CODE ref (streaming callback).
# Streaming callback receives ($stream_id, $max_length) and returns:
# ($data, $eof_flag) - send data, eof=1 closes stream
# undef - defer; call resume_stream() when data is ready
sub submit_request {
my ($self, %args) = @_;
my $method = delete $args{method} // 'GET';
my $path = delete $args{path} // '/';
my $scheme = delete $args{scheme} // 'https';
my $authority = delete $args{authority};
lib/Net/HTTP2/nghttp2/Session.pm view on Meta::CPAN
my $data_cb = delete $args{data_callback};
my $cb_data = delete $args{callback_data};
# Build pseudo-headers + regular headers
my @nv = (
[':status', $status],
@$headers,
);
if (defined $body && !ref($body)) {
# Static body - convert to simple streaming callback
my $sent = 0;
my $body_bytes = $body;
$data_cb = sub {
my ($stream_id, $max_len) = @_;
return ('', 1) if $sent; # EOF
$sent = 1;
return ($body_bytes, 1); # data + EOF
};
return $self->_submit_response_streaming($stream_id, \@nv, $data_cb, undef);
}
elsif (ref($body) eq 'CODE') {
# CODE ref body - streaming callback data provider
return $self->_submit_response_streaming($stream_id, \@nv, $body, $cb_data);
}
elsif ($data_cb) {
# Dynamic body - use callback-based data provider
return $self->_submit_response_streaming($stream_id, \@nv, $data_cb, $cb_data);
}
else {
# No body (e.g., 204 No Content, redirects)
return $self->_submit_response_no_body($stream_id, \@nv);
}
}
# Resume a deferred stream (call after data becomes available)
sub resume_stream {
my ($self, $stream_id) = @_;
lib/Net/HTTP2/nghttp2/Session.pm view on Meta::CPAN
HTTP status code. Default: C<200>.
=item headers
Arrayref of C<[$name, $value]> pairs.
=item body
Response body. Same types as C<submit_request>: C<undef> (no body),
string (static body), or CODE ref (streaming callback with identical
signature).
=item data_callback
Alternative to passing a CODE ref as C<body>. Callback with the same
streaming signature.
=item callback_data
Optional user data passed as third argument to the streaming callback.
=back
=head2 submit_push_promise
my $promised_stream_id = $session->submit_push_promise($stream_id, %args);
Submit a server push promise.
=head2 submit_data
$session->submit_data($stream_id, $data, $eof);
Push data directly onto an existing stream. The stream must already have
a data provider (established by C<submit_request> or C<submit_response>
with a CODE ref or C<data_callback>). This replaces the streaming callback
with a one-shot static body, then resumes the stream.
Arguments:
=over 4
=item C<$stream_id>
The stream to send data on.
=item C<$data>
The data to send. Can be C<undef> for an empty DATA frame.
=item C<$eof>
If true, the DATA frame will include END_STREAM, closing the stream.
=back
This is useful when you have data available outside the streaming callback
context and want to push it directly, such as forwarding WebSocket frames
received from another source.
=head2 resume_stream
$session->resume_stream($stream_id);
Resume data production for a deferred stream. Call this after a streaming
body callback has returned C<undef> and new data is available. Works for
both request and response streams.
=head2 terminate_session
$session->terminate_session($error_code);
Send a GOAWAY frame and terminate the session. The C<$error_code> should
be an nghttp2 error code (0 for C<NGHTTP2_NO_ERROR>).
lib/Net/HTTP2/nghttp2/Session.pm view on Meta::CPAN
$session->set_stream_user_data($stream_id, $data);
Associate arbitrary user data with a stream. Useful for storing
per-stream application state.
=head2 is_stream_deferred
my $bool = $session->is_stream_deferred($stream_id);
Returns true if the stream's data provider has been deferred (i.e. the
streaming callback returned C<undef>). The stream can be resumed with
C<resume_stream()>.
=head2 want_read
my $bool = $session->want_read();
Returns true if the session wants to read more data.
=head2 want_write
#include <nghttp2/nghttp2.h>
#include <string.h>
/*
* Net::HTTP2::nghttp2 - Perl XS bindings for nghttp2
*
* This module provides server-side HTTP/2 support via nghttp2.
*/
/* Per-stream data provider state for streaming responses */
typedef struct {
SV *callback; /* Perl callback to produce data */
SV *user_data; /* User data for callback */
int32_t stream_id; /* Stream ID */
int eof; /* End of data flag */
int deferred; /* Currently deferred */
} nghttp2_perl_data_provider;
/* Session wrapper structure */
typedef struct {
rv = nghttp2_session_terminate_session(ps->session, error_code);
RETVAL = rv;
#line 1759 "nghttp2.c"
XSprePUSH;
PUSHi((IV)RETVAL);
}
XSRETURN(1);
}
XS_EUPXS(XS_Net__HTTP2__nghttp2__Session__submit_response_streaming); /* prototype to pass -Wmissing-prototypes */
XS_EUPXS(XS_Net__HTTP2__nghttp2__Session__submit_response_streaming)
{
dVAR; dXSARGS;
if (items != 5)
croak_xs_usage(cv, "self, stream_id, headers_av, data_callback, cb_user_data");
{
SV * self = ST(0)
;
int stream_id = (int)SvIV(ST(1))
;
AV * headers_av;
dXSTARG;
STMT_START {
SV* const xsub_tmp_sv = ST(2);
SvGETMAGIC(xsub_tmp_sv);
if (SvROK(xsub_tmp_sv) && SvTYPE(SvRV(xsub_tmp_sv)) == SVt_PVAV){
headers_av = (AV*)SvRV(xsub_tmp_sv);
}
else{
Perl_croak_nocontext("%s: %s is not an ARRAY reference",
"Net::HTTP2::nghttp2::Session::_submit_response_streaming",
"headers_av");
}
} STMT_END
;
#line 1111 "nghttp2.xs"
ps = (nghttp2_perl_session *)SvIV(SvRV(self));
/* Build name-value array from Perl array of arrayrefs */
nvlen = av_len(headers_av) + 1;
Newxz(nva, nvlen, nghttp2_nv);
nva[i].namelen = name_len;
nva[i].value = (uint8_t *)SvPVbyte(*value_sv, value_len);
nva[i].valuelen = value_len;
nva[i].flags = NGHTTP2_NV_FLAG_NONE;
}
}
}
/* Check if we have a body to send */
if (SvOK(body_sv) && SvROK(body_sv) && SvTYPE(SvRV(body_sv)) == SVt_PVCV) {
/* CODE ref body: streaming callback data provider */
Newxz(dp, 1, nghttp2_perl_data_provider);
dp->stream_id = 0; /* Will be set after submit */
dp->eof = 0;
dp->deferred = 0;
dp->callback = newSVsv(body_sv);
data_prd.source.ptr = dp;
data_prd.read_callback = perl_data_source_read_callback;
data_prd_ptr = &data_prd;
}
newXS_deffile("Net::HTTP2::nghttp2::Session::mem_send", XS_Net__HTTP2__nghttp2__Session_mem_send);
newXS_deffile("Net::HTTP2::nghttp2::Session::want_read", XS_Net__HTTP2__nghttp2__Session_want_read);
newXS_deffile("Net::HTTP2::nghttp2::Session::want_write", XS_Net__HTTP2__nghttp2__Session_want_write);
newXS_deffile("Net::HTTP2::nghttp2::Session::submit_settings", XS_Net__HTTP2__nghttp2__Session_submit_settings);
newXS_deffile("Net::HTTP2::nghttp2::Session::_submit_response_with_body", XS_Net__HTTP2__nghttp2__Session__submit_response_with_body);
newXS_deffile("Net::HTTP2::nghttp2::Session::_submit_response_no_body", XS_Net__HTTP2__nghttp2__Session__submit_response_no_body);
newXS_deffile("Net::HTTP2::nghttp2::Session::resume_data", XS_Net__HTTP2__nghttp2__Session_resume_data);
newXS_deffile("Net::HTTP2::nghttp2::Session::get_stream_user_data", XS_Net__HTTP2__nghttp2__Session_get_stream_user_data);
newXS_deffile("Net::HTTP2::nghttp2::Session::set_stream_user_data", XS_Net__HTTP2__nghttp2__Session_set_stream_user_data);
newXS_deffile("Net::HTTP2::nghttp2::Session::terminate_session", XS_Net__HTTP2__nghttp2__Session_terminate_session);
newXS_deffile("Net::HTTP2::nghttp2::Session::_submit_response_streaming", XS_Net__HTTP2__nghttp2__Session__submit_response_streaming);
newXS_deffile("Net::HTTP2::nghttp2::Session::submit_data", XS_Net__HTTP2__nghttp2__Session_submit_data);
newXS_deffile("Net::HTTP2::nghttp2::Session::is_stream_deferred", XS_Net__HTTP2__nghttp2__Session_is_stream_deferred);
newXS_deffile("Net::HTTP2::nghttp2::Session::_clear_deferred", XS_Net__HTTP2__nghttp2__Session__clear_deferred);
newXS_deffile("Net::HTTP2::nghttp2::Session::_new_client_xs", XS_Net__HTTP2__nghttp2__Session__new_client_xs);
newXS_deffile("Net::HTTP2::nghttp2::Session::_submit_request_xs", XS_Net__HTTP2__nghttp2__Session__submit_request_xs);
newXS_deffile("Net::HTTP2::nghttp2::Session::submit_rst_stream", XS_Net__HTTP2__nghttp2__Session_submit_rst_stream);
newXS_deffile("Net::HTTP2::nghttp2::Session::submit_ping", XS_Net__HTTP2__nghttp2__Session_submit_ping);
newXS_deffile("Net::HTTP2::nghttp2::Session::submit_window_update", XS_Net__HTTP2__nghttp2__Session_submit_window_update);
#if PERL_VERSION_LE(5, 21, 5)
# if PERL_VERSION_GE(5, 9, 0)
#include <nghttp2/nghttp2.h>
#include <string.h>
/*
* Net::HTTP2::nghttp2 - Perl XS bindings for nghttp2
*
* This module provides server-side HTTP/2 support via nghttp2.
*/
/* Per-stream data provider state for streaming responses */
typedef struct {
SV *callback; /* Perl callback to produce data */
SV *user_data; /* User data for callback */
int32_t stream_id; /* Stream ID */
int eof; /* End of data flag */
int deferred; /* Currently deferred */
} nghttp2_perl_data_provider;
/* Session wrapper structure */
typedef struct {
PREINIT:
nghttp2_perl_session *ps;
int rv;
CODE:
ps = (nghttp2_perl_session *)SvIV(SvRV(self));
rv = nghttp2_session_terminate_session(ps->session, error_code);
RETVAL = rv;
OUTPUT:
RETVAL
# Submit response with streaming data callback
# Callback receives ($stream_id, $max_length, $user_data) and returns ($data, $eof)
# Return undef or empty list to defer (call resume_data later)
int
_submit_response_streaming(self, stream_id, headers_av, data_callback, cb_user_data)
SV *self
int stream_id
AV *headers_av
SV *data_callback
SV *cb_user_data
PREINIT:
nghttp2_perl_session *ps;
nghttp2_nv *nva;
size_t nvlen;
nghttp2_data_provider data_prd;
if (rv != 0) {
remove_data_provider(ps, stream_id);
croak("nghttp2_submit_response failed: %s", nghttp2_strerror(rv));
}
RETVAL = rv;
OUTPUT:
RETVAL
# Queue data to send on an existing stream.
# The stream must already have a data provider (from submit_request or
# submit_response with a streaming body callback). This sets the data
# provider's user_data to the given data and eof flag, clears the deferred
# state, and calls nghttp2_session_resume_data so the next mem_send will
# invoke the read callback which returns this data.
int
submit_data(self, stream_id, data_sv, eof)
SV *self
int stream_id
SV *data_sv
int eof
PREINIT:
nva[i].namelen = name_len;
nva[i].value = (uint8_t *)SvPVbyte(*value_sv, value_len);
nva[i].valuelen = value_len;
nva[i].flags = NGHTTP2_NV_FLAG_NONE;
}
}
}
/* Check if we have a body to send */
if (SvOK(body_sv) && SvROK(body_sv) && SvTYPE(SvRV(body_sv)) == SVt_PVCV) {
/* CODE ref body: streaming callback data provider */
Newxz(dp, 1, nghttp2_perl_data_provider);
dp->stream_id = 0; /* Will be set after submit */
dp->eof = 0;
dp->deferred = 0;
dp->callback = newSVsv(body_sv);
data_prd.source.ptr = dp;
data_prd.read_callback = perl_data_source_read_callback;
data_prd_ptr = &data_prd;
}
t/02-streaming.t view on Meta::CPAN
# Test callback directly
my ($data1, $eof1) = $data_callback->(1, 1024, undef);
is($data1, "Hello ", "First chunk");
ok(!$eof1, "Not EOF yet");
my ($data2, $eof2) = $data_callback->(1, 1024, undef);
is($data2, "World", "Second chunk");
};
# Test 3: Deferred streaming (for async data)
subtest 'Deferred streaming' => sub {
plan tests => 3;
my $data_ready = 0;
my $deferred_callback = sub {
my ($stream_id, $max_len) = @_;
if (!$data_ready) {
# No data available yet - return undef to defer
return; # Empty return = defer
}
t/02-streaming.t view on Meta::CPAN
# Simulate data becoming available
$data_ready = 1;
# Second call - should return data
my ($data, $eof) = $deferred_callback->(1, 1024);
is($data, "Async data!", "Data returned after ready");
ok($eof, "EOF flag set");
};
# Test 4: API check for streaming methods
subtest 'Streaming API methods' => sub {
plan tests => 3;
can_ok($session, 'submit_response');
can_ok($session, 'resume_stream');
can_ok($session, 'is_stream_deferred');
};
}
done_testing;
t/20-streaming-request.t view on Meta::CPAN
#!/usr/bin/env perl
# Tests for streaming request body callback support
# submit_request() should accept a CODE ref body that works like
# submit_response()'s data_callback â enabling bidirectional streaming
# for Extended CONNECT (WebSocket over HTTP/2, RFC 8441).
use strict;
use warnings;
use Test::More;
use lib 't/lib';
use Net::HTTP2::nghttp2;
use Net::HTTP2::nghttp2::Session;
use Test::HTTP2::Frame qw(:all);
t/20-streaming-request.t view on Meta::CPAN
$client->mem_recv($sd);
for (1..3) {
$sd = $server->mem_send; $client->mem_recv($sd) if length($sd);
$cd = $client->mem_send; $server->mem_recv($cd) if length($cd);
}
}
#==========================================================================
# Test: Streaming body callback sends HEADERS without END_STREAM
#==========================================================================
subtest 'streaming body callback: HEADERS without END_STREAM' => sub {
my @server_frames;
my @server_data;
my $server = Net::HTTP2::nghttp2::Session->new_server(
callbacks => {
on_begin_headers => sub { 0 },
on_header => sub { 0 },
on_frame_recv => sub {
push @server_frames, {
type => $_[0]{type},
t/20-streaming-request.t view on Meta::CPAN
callbacks => {
map { $_ => sub { 0 } }
qw(on_begin_headers on_header on_frame_recv
on_data_chunk_recv on_stream_close)
},
);
do_handshake($client, $server);
@server_frames = ();
# Submit request with streaming callback that defers immediately
my $stream_id = $client->submit_request(
method => 'CONNECT',
path => '/ws',
scheme => 'https',
authority => 'localhost',
headers => [[':protocol', 'websocket']],
body => sub {
my ($sid, $max_len) = @_;
return undef; # defer â no data yet
},
t/20-streaming-request.t view on Meta::CPAN
ok(@headers >= 1, 'Server received HEADERS frame');
ok(!($headers[0]{flags} & FLAG_END_STREAM),
'HEADERS does NOT have END_STREAM (stream stays open)');
done_testing;
};
#==========================================================================
# Test: Streaming callback sends data when resumed
#==========================================================================
subtest 'streaming body callback: data delivery on resume' => sub {
my @server_data;
my @server_frames;
my $server = Net::HTTP2::nghttp2::Session->new_server(
callbacks => {
on_begin_headers => sub { 0 },
on_header => sub { 0 },
on_frame_recv => sub {
push @server_frames, {
type => $_[0]{type},
t/21-submit-data.t view on Meta::CPAN
my $client = Net::HTTP2::nghttp2::Session->new_client(
callbacks => {
map { $_ => sub { 0 } }
qw(on_begin_headers on_header on_frame_recv
on_data_chunk_recv on_stream_close)
},
);
do_handshake($client, $server);
# Submit request with streaming callback that defers (keeps stream open)
my $stream_id = $client->submit_request(
method => 'CONNECT',
path => '/ws',
scheme => 'https',
authority => 'localhost',
headers => [[':protocol', 'websocket']],
body => sub { return undef }, # always defer
);
my $cd = $client->mem_send;
t/21-submit-data.t view on Meta::CPAN
my @client_frames;
my $server_stream_id;
my $server = Net::HTTP2::nghttp2::Session->new_server(
callbacks => {
on_begin_headers => sub { 0 },
on_header => sub { 0 },
on_frame_recv => sub {
my ($frame) = @_;
# When we get the full request, send a streaming response
if ($frame->{type} == FRAME_HEADERS
&& ($frame->{flags} & FLAG_END_STREAM)) {
$server_stream_id = $frame->{stream_id};
}
0;
},
on_data_chunk_recv => sub { 0 },
on_stream_close => sub { 0 },
},
);
t/21-submit-data.t view on Meta::CPAN
scheme => 'https',
authority => 'localhost',
);
my $cd = $client->mem_send;
$server->mem_recv($cd) if length($cd);
exchange($client, $server);
ok(defined $server_stream_id, 'Server received request');
# Server submits response with streaming callback that defers
$server->submit_response($server_stream_id,
status => 200,
headers => [['content-type', 'text/plain']],
body => sub { return undef }, # defer
);
my $sd = $server->mem_send;
$client->mem_recv($sd) if length($sd);
exchange($client, $server);
@client_data = ();