Net-HTTP2-nghttp2

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

        - 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

MANIFEST  view on Meta::CPAN

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

nghttp2.c  view on Meta::CPAN


#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 {

nghttp2.c  view on Meta::CPAN

        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;

nghttp2.c  view on Meta::CPAN

	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);

nghttp2.c  view on Meta::CPAN

                    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;
        }

nghttp2.c  view on Meta::CPAN

        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)

nghttp2.xs  view on Meta::CPAN


#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 {

nghttp2.xs  view on Meta::CPAN

    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;

nghttp2.xs  view on Meta::CPAN

        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:

nghttp2.xs  view on Meta::CPAN

                    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   = ();



( run in 1.451 second using v1.01-cache-2.11-cpan-140bd7fdf52 )