HTTP2::Connection
Owns one HTTP/2 transport, its ordered writer, reader, and stream registry.
Constants
Upper bound on how many WriteCommands the writer fiber stages
into the transport buffer before forcing a flush (#writer_loop,
#flush_batch), even if more immediately-available work remains.
Keeps a single flush's completion latency bounded under sustained
load instead of growing unboundedly with queue depth.
Constructors
Opens a cleartext connection using HTTP/2 prior knowledge.
read_timeout and write_timeout default to nil (no transport
deadlines). Against untrusted or unreliable peers, set them and/or
enable Configuration#keepalive_interval; otherwise a silent or
write-stalled peer can hold blocked callers indefinitely.
HTTP2::Client sets write_timeout by default and bounds the
handshake with a per-wait deadline instead of read_timeout; it
enables keepalive by default to detect a silent peer once active.
Opens a verified TLS connection that requires ALPN to select h2.
read_timeout and write_timeout default to nil (no transport
deadlines). Against untrusted or unreliable peers, set them and/or
enable Configuration#keepalive_interval; otherwise a silent or
write-stalled peer can hold blocked callers indefinitely.
handshake_read_timeout bounds only the TLS handshake itself (see
start_tls) without leaving a persistent read_timeout armed
afterward. HTTP2::Client sets write_timeout by default and
bounds the TLS and HTTP/2 handshakes with per-wait deadlines
(handshake_read_timeout and wait_until_active, respectively)
instead of a persistent read_timeout; it enables keepalive by
default to detect a silent peer once active.
context, whether supplied or defaulted, is configured for ALPN
"h2" IN PLACE, unconditionally, on every call — see start_tls's
doc comment for the full contract, why no private copy is made
instead, and the caller-visible consequence of configuring in
place.
Creates and starts a connection over a caller-supplied duplex IO.
TLS callers: prefer connect_tls/start_tls, or HTTP2::Client.
If transport is a caller-built OpenSSL::SSL::Socket, this
connection's tls_raw_transport stays nil forever — only
start_tls can set it (its setter is protected, so there is no
way for a caller to supply it here), and Crystal's
OpenSSL::SSL::Socket exposes no accessor for the raw IO behind
its BIO, so the library has no way to discover it after the fact
either. That leaves #close/#terminate exposed to the unbounded
SSL_shutdown close hang described on #close_transport's doc
comment: with no raw socket to force-close instead, shutdown falls
back to the TLS wrapper's own close, which can block indefinitely
against a stalled peer if transport has no write_timeout set.
connect_tls, start_tls, and HTTP2::Client are unaffected —
they dial the raw socket themselves and record it. A caller who
must use this constructor with their own TLS socket should set a
write_timeout on it directly to bound that close.
Wraps a supplied transport in verified TLS and starts HTTP/2.
handshake_read_timeout, when given, bounds each individual read
during the TLS handshake (per read, like read_timeout elsewhere —
not a cumulative deadline for the whole handshake) by setting it as
transport's read_timeout for the duration of the handshake only;
the transport's previous read_timeout (nil, or whatever a caller
set directly) is reinstated once the handshake and ALPN check are
done, before HTTP/2 starts — so it never lingers as a persistent
transport-level deadline afterward, and a caller who also set a
persistent read_timeout directly on transport gets it back
unchanged. Silently ignored if transport doesn't support
read_timeout= (its static type is the untyped IO).
connect_prior_knowledge has no equivalent parameter because
Connection#start performs no synchronous read of its own — the
cleartext dial path never blocks on a read before wait_until_active
is already covering the wait.
context is configured for ALPN "h2" IN PLACE, unconditionally, on
every call — never on a private copy.
OpenSSL::SSL::Context::Client wraps a bare SSL_CTX* behind
@handle, and Crystal's default #dup only shallow-copies
instance variables, so a dup'd context would share that SAME
@handle with the original (verified while investigating this:
both report an identical to_unsafe pointer address). Mutating
the "copy" would mutate the original's underlying C state too,
buying no isolation — and BOTH Crystal objects would independently
call LibSSL.ssl_ctx_free(@handle) from their own #finalize,
freeing the same handle twice. SSL_CTX also has no deep-copy
operation in OpenSSL itself, so a safe copy is not available by
any route; ALPN "h2" is mandatory for this library regardless, so
the mutation cannot be avoided either way.
#alpn_protocol= internally frees its previous protos buffer and
mem-dups the new one (SSL_CTX_set_alpn_protos), so setting it
again on every dial is cheap and safe, not merely tolerated.
Setting it unconditionally, every dial, is deliberate, not an
oversight: it is SELF-HEALING against anything else that changes
context.alpn_protocol between dials — the next dial through this
library re-asserts "h2" regardless of what it finds. The
symmetric, caller-visible consequence: do not share one context
between an HTTP2::Client/connect_tls/start_tls caller and a
DIFFERENT consumer that needs a different, stable ALPN protocol on
it — every dial through this library overwrites alpn_protocol
back to "h2" unconditionally, even if that other consumer set it
to something else in between.
Class methods
Builds the TLS client context used when a caller does not supply
one: connect_tls's and start_tls's default context: argument,
and HTTP2::Client's default tls_context, all call this, so
every internally created context stays in lockstep.
Disables TLS 1.0 and TLS 1.1 explicitly — RFC 9113 §9.2:
"deployments of HTTP/2 ... MUST NOT use TLS 1.1 or lower" — rather
than relying on OpenSSL::SSL::Context's own constructor already
disabling both by default (true since Crystal 0.35.0, for every
context, including a caller-supplied one — see the task report for
the upstream commit). That stdlib default is not part of this
method's documented contract and not something a future Crystal or
alternate OpenSSL binding is obligated to preserve, so this library
asserts its own RFC floor instead of depending on it silently.
A context a CALLER supplies directly to connect_tls/start_tls/
HTTP2::Client.new is never passed through this method and is
never modified this way — only the internally created default is.
Instance methods
Atomically claims and closes an active connection only if it has no registered streams or pending request reservations.
:nodoc:
A bounded stream of structured connection events. Producers never block;
inspect #dropped_diagnostic_count to detect a slow consumer.
Diagnostic emission is gated off the frame path and only turns on the
first time this accessor is called (@diagnostics_enabled) -- frames,
errors, and lifecycle events observed before that first call are
deliberately never captured. Call this (even without receiving from
the returned channel right away) before driving any traffic whose
diagnostics you need to see.
Sends GOAWAY(NO_ERROR), refuses new streams, and lets established streams finish until the deadline. The deadline is shortened, never extended, by concurrent graceful-close or peer-GOAWAY requests.
Converts a request-slot reservation into an idle client stream without an admission-accounting gap.
:nodoc:
Sends a PING and waits for the matching acknowledgement. Concurrent PINGs with identical payloads are matched in submission order.
Returns receive-window credit after application bytes leave a bounded stream body. Connection credit is always restored; stream credit is omitted once the peer has ended that stream.
Credit always accumulates, but the writer is only woken once pending
credit reaches a half-window watermark (connection or stream scope) —
coalescing what would otherwise be a WINDOW_UPDATE pair on every body
read. A writer woken for any OTHER reason still flushes all pending
credit unconditionally (see take_pending_window_updates); this is by
design, not a bug — the watermark only gates the wake, not the send.
:nodoc:
Idempotently releases a pending request-slot reservation.
:nodoc:
Sends application data through the flow-control scheduler.
Under the default single-threaded runtime, data is sliced, not
copied: submit_data (via #command.wait) blocks until each
chunk's frame has been memcpy'd into the transport buffer and its
batch has either flushed or failed (see #flush_batch) before
returning, so by the time this call returns, nothing internal to
the connection still reads from data. See #stage_scheduled_data's
comment for the invariants that guarantee this.
Under -Dpreview_mt, #owned_for_write takes a private copy of
each chunk instead: #terminate is reachable from fibers not
pinned to the writer's thread (#close, and the drain-monitor,
keepalive, and settings-timer loops, all plain ::spawn unlike
#spawn_transport_fiber's pinned reader/writer fibers), and it
can unblock this call's #command.wait on a different OS thread
while the writer thread is still copying the chunk into the
transport buffer.
Either way, the caller must leave data unmodified until this
call returns; it is never retained afterward.
Streams DATA from the source's current position without rewinding it.
Reads into two reusable buffers instead of allocating one per chunk.
This relies on #send_data(Bytes) being synchronous: it blocks
until the chunk it was given has been handed to the transport
before returning, so a buffer is never refilled while a prior call
might still be reading from it (default single-threaded runtime),
or is refilled only after #owned_for_write has already taken a
private copy of the in-flight chunk (-Dpreview_mt; see
#send_data(Bytes)'s doc comment). current and following swap
roles each iteration; the loop always reads into the buffer that
was NOT just handed to #send_data.
Preserves explicit DATA padding while still applying flow control. A padded frame is atomic because splitting it would change its wire shape.
:nodoc:
Encodes ordered name/value pairs with the default field policy.
HPACK-encodes one ordered field section on the writer fiber and sends its complete HEADERS/CONTINUATION sequence atomically.
Sends a SETTINGS update and tracks its ordered acknowledgement.
Starts the writer, emits the complete client preface atomically, then starts the continuous reader.
Enqueues one ordered field section onto the writer's FIFO
@write_queue without waiting for it to reach the transport (the
HPACK encoding itself is deferred further still — to the writer
fiber, once it dequeues this command — so this method only ever
does admission-order bookkeeping). Returns the WriteCommand so a
caller that holds an external ordering lock across multiple
same-connection submitters (see Client#open_request_stream's
opening_mutex) can release that lock once this command is
admitted to @write_queue in the correct position, instead of
holding it for the blocking wait too. #send_headers is the fused
submit+wait convenience most callers want.
Takes its own defensive copy of fields (see #submit_owned_headers)
because encoding happens later, on the writer fiber — a caller that
mutated its own Enumerable after this method returns must not be
able to change what eventually gets encoded.
:nodoc:
Atomically admits one future client request without allocating a stream ID or sending bytes. Ordinary lack of capacity returns nil.
:nodoc:
Waits for a peer-imposed concurrent-stream slot to possibly have
freed up, for cancellation to fire, or for timeout to elapse —
whichever happens first. Returns in all three cases without
raising: the caller is expected to retry the #send_headers call
that raised ConcurrentStreamLimitError (and to check its own
cancellation/deadline), so a stale or spurious wakeup here is
harmless — it costs at most one extra retry. This remains available
to raw Connection users; HTTP2::Client now performs pool-wide
request-slot acquisition instead.
:nodoc:
Writes a frame batch without allowing another command to interleave. Validates every frame in array order and raises on the first one that's invalid to send this way; a batch containing more than one kind of invalid frame raises whichever category's error the first invalid frame belongs to, not a fixed category priority.
Writes one frame through the connection's sole writer fiber.
Nested types
- HTTP2::Connection::CanceledError
- HTTP2::Connection::ClosedError
- HTTP2::Connection::ClosedStream
- HTTP2::Connection::ConcurrentStreamLimitError
- HTTP2::Connection::Configuration
- HTTP2::Connection::Diagnostic
- HTTP2::Connection::DrainTimeoutError
- HTTP2::Connection::DrainedError
- HTTP2::Connection::DrainingError
- HTTP2::Connection::Error
- HTTP2::Connection::FieldBlock
- HTTP2::Connection::FieldBlockAssembler
- HTTP2::Connection::FieldSection
- HTTP2::Connection::GoAwayTerminationError
- HTTP2::Connection::HeaderBlockFramer
- HTTP2::Connection::InboundFrameRateLimiter
- HTTP2::Connection::InvalidStateError
- HTTP2::Connection::KeepaliveTimeoutError
- HTTP2::Connection::OpenStreamLimitError
- HTTP2::Connection::PingLimitError
- HTTP2::Connection::PoolStateSubscription
- HTTP2::Connection::QueueFullError
- HTTP2::Connection::RequestCapacity
- HTTP2::Connection::RequestSlotReservation
- HTTP2::Connection::ResourceLimitError
- HTTP2::Connection::RetryableInvalidStateError
- HTTP2::Connection::SettingsState
- HTTP2::Connection::State
- HTTP2::Connection::StreamIDAllocator
- HTTP2::Connection::StreamIDExhaustedError
- HTTP2::Connection::StreamResetError
- HTTP2::Connection::TLSNegotiationError
- HTTP2::Connection::TLSVerificationError
- HTTP2::Connection::TimeoutError
- HTTP2::Connection::UnprocessedStreamError