Configure one origin and a bounded pool.
Flyology.HTTP.Client is origin-bound. It provides ordinary synchronous calls and owner-driven composable exchanges in both task lanes. After configuration, concurrent callers can share one client.
Parse one normalized http:// or https:// origin. Set the maximum number of open and connecting transports in the Client discriminant. Call the basic Configure overload once before concurrent use. Configuration retains policy but starts no DNS, socket, TLS, task, or event-loop work.
with Flyology.HTTP;
with Flyology.HTTP.Client;
package Client renames Flyology.HTTP.Client;
HTTP : aliased Client.Client (Capacity => 8);
Client.Configure
(HTTP,
Flyology.HTTP.Parse_Origin ("http://127.0.0.1:8080"),
(Max_Idle => 4,
Idle_Timeout => 30.0,
Max_Connection_Age => 300.0,
Max_Requests_Per_Connection => 1_000));Capacity- Bounds open and connecting transports. Requests wait for admission instead of creating sockets without a bound.
Max_Idle- Limits retained reusable transports. Zero disables reuse but does not disable concurrent requests.
Idle_Timeout- Rotates a connection after the specified idle time. A negative value disables this check.
Max_Connection_Age- Bounds total reusable lifetime. A negative value disables this check.
Max_Requests_Per_Connection- Rotates a connection after the specified request count. Zero disables this check.
Parse_Origin accepts only a scheme, host, and optional port. It rejects user information, queries, fragments, and nontrivial paths. Omitted ports become 80 or 443.
For a complete endpoint URL, follow the identifier guide to derive the origin without splitting strings by hand. The client never mixes origins or TLS state, and it has no public connection-checkout operation. Mutable requests and body sources still require application synchronization or per-call ownership.
Keep the HTTP authority separate from a Unix socket.
A Unix_Socket_Transport selects a pathname Unix-domain stream socket without changing the HTTP origin. Construct it with Unix_Socket, then pass it to the Unix transport Configure overload. The origin still supplies the HTTP/1.1 Host field and HTTP/2 :authority.
HTTP : aliased Client.Client (Capacity => 2);
Request : Client.Request;
Client.Configure
(HTTP,
Flyology.HTTP.Parse_Origin ("http://localhost"),
Client.Unix_Socket ("/var/run/docker.sock"));
Client.Set_Target (Request, "/_ping");
declare
Response : Client.Response :=
Client.Execute (HTTP, Request, Timeout => 5.0);
begin
pragma Assert (Client.Status (Response) = 200);
Consume (Client.Read_All (Response));
end;The same request, response streaming, deadline, cancellation, stale-connection retry, diagnostics, and shutdown operations apply to Unix transports. The pool belongs to one configured client, so its identity includes both the retained socket pathname and the retained HTTP authority. When a daemon closes pooled connections and replaces its socket entry, the next safe stale retry opens the configured pathname again.
Unix transport configuration supports cleartext HTTP/1.1 and HTTP_2_Prior_Knowledge. It does not add HTTP/3, TLS negotiation, or an HTTP/1.1 Upgrade handshake. Pathname Unix sockets are supported on macOS and Linux; they are not a portable Windows transport.
The maintained CLI provides an optional Docker Engine smoke check: ./showcases/run_http_client_cli.sh --unix-socket /var/run/docker.sock http://localhost/version. The automated suite uses local test daemons and does not require Docker.
Discover HTTP/3 without changing request code.
Negotiate_HTTP_3 starts on authenticated TLS/TCP and offers h2 followed by http/1.1. Use the matching Configure overload with a pinned HTTP/3 certificate.
When a response carries a same-origin Alt-Svc: h3=":port" field, the client records the bounded UDP alternative. Later requests prefer HTTP/3 while healthy TCP capacity remains available. Request, response, deadline, cancellation, redirect, authentication, and body-reading APIs do not change.
HTTP : aliased Client.Client (Capacity => 2);
OpenSSL.Initialize_Client (Backend);
Client.Configure
(HTTP,
Flyology.HTTP.Parse_Origin ("https://api.example.com"),
Backend'Access,
Client.Negotiate_HTTP_3,
HTTP_3_Certificate_DER => Pinned_Certificate_DER,
Pool =>
(Max_Idle => 2,
Idle_Timeout => 30.0,
Max_Connection_Age => 300.0,
Max_Requests_Per_Connection => 0));
-- The first response uses h2 or HTTP/1.1 and can discover H3.
declare
First : Client.Response := Client.Execute (HTTP, Request);
begin
Consume (Client.Read_All (First));
end;
-- A valid, unexpired alternative is preferred for later exchanges.
declare
Second : Client.Response := Client.Execute (HTTP, Request);
begin
pragma Assert
(Client.Negotiated_Protocol (Second) =
Flyology.HTTP.HTTP_3_Protocol);
Consume (Client.Read_All (Second));
end;The example retains each exchange in a limited Response. It calls the retained-body Execute overload and Read_All. Negotiated_Protocol can then report HTTP_3_Protocol.
The QUIC peer is authenticated against the exact certificate supplied as 1 through 4,096 DER bytes. This pin is separate from the TCP provider's certificate-chain and hostname verification. The pool retains healthy TCP and QUIC transports together: an available H3 transport is preferred, while a concurrent exchange uses retained HTTP/2 or HTTP/1.1 when the H3 lane is busy or connecting. One application request is never duplicated across protocols. Set client capacity and Max_Idle to at least two, as above, to keep both stacks warm while idle. A failed discovered H3 establishment clears the alternative and makes one TCP attempt within the original exchange deadline.
Address-family dual stack is independent of protocol selection. When DNS returns IPv4 and IPv6, the owner-driven exchange tries permitted addresses serially in resolver order. Every TCP or QUIC attempt uses the same filter, cancellation sources, and absolute deadline. An immediate connection or handshake failure advances to the next address. ma=0 and Alt-Svc: clear remove a learned alternative; advertised lifetimes are capped at 31 days.
Require QUIC directly
Use Require_HTTP_3 when the HTTPS origin's UDP port is known to serve H3. Its direct Configure overload needs no TCP TLS provider and never falls back:
Client.Configure
(HTTP,
Flyology.HTTP.Parse_Origin ("https://api.example.com"),
Client.Require_HTTP_3,
HTTP_3_Certificate_DER => Pinned_Certificate_DER);The maintained qualification gate runs the Ada client against pinned aioquic and quic-go servers and runs both independent clients against the routed Ada server. Published RFC vectors separately cover deterministic QUIC packet protection, TLS, and the supported static-QPACK profile. Upstream h3spec is server-oriented, so its gate qualifies the Ada server; the client uses independent aioquic wire scenarios and deterministic response/QPACK adapters instead. Run ./scripts/test-http3-interop.sh all; see tests/http3-conformance.md for the exact evidence boundary.
Build validated request metadata.
A new Request is GET /. Use the constants in Flyology.HTTP.Methods for standard methods. Use To_Method for a validated extension token.
Is_Safe and Is_Idempotent classify standard methods conservatively. Unknown extensions are unsafe and non-idempotent. The client rejects CONNECT because it has no tunnel handoff, and it rejects a body on TRACE.
with Flyology.HTTP.Methods;
Request : Client.Request;
Client.Set_Method (Request, Flyology.HTTP.Methods.POST);
Client.Set_Target (Request, "/objects?validate=true");
Client.Add_Header (Request, "Content-Type", "application/octet-stream");
Client.Add_Header (Request, "X-Trace", "first");
Client.Add_Header (Request, "X-Trace", "second");
Client.Set_Body (Request, Payload);The example calls Set_Method with the standard POST constant. Set_Target validates the origin-form target.
The target is HTTP origin-form. * is accepted only for OPTIONS. The client rejects absolute-form, authority-form, fragments, spaces, controls, non-ASCII bytes, and targets over 16 KiB. For a complete endpoint, use Flyology_IRI.Target to produce the normalized path and query without the fragment.
Repeated headers preserve insertion order. Add_Header rejects malformed fields and client-controlled framing, Host, connection, upgrade, and Expect fields. The Set_Body overloads retain either a contiguous byte array or a one-character-to-one-octet String.
Set_Expect_Continue generates Expect: 100-continue for a nonempty retained or streamed body. The client sends the head first and waits for the configured interval. A 100 Continue sends the body, while a final response suppresses it. If no response byte arrives, the client sends the body without restarting the whole deadline.
A 417 Expectation Failed received before body transmission consumes the single automatic retry. The client retries on a fresh transport without Expect. If a partial response head has arrived, the client finishes it before sending more request bytes.
Call the ordinary Execute overload for an empty or retained body. The result is available after the final response head. The response body remains a leased stream:
Reply : Client.Response :=
Client.Execute
(HTTP, Request, Timeout => 5.0, Token => Cancellation'Access);The monotonic timeout begins before pool admission and continues through DNS, every address attempt, connection and TLS setup, request transmission, final response-head parsing, and later response-body reads. Negative is unlimited and zero is immediate. The token passed to Execute is borrowed only through the final response head.
Choose a bounded request-body source.
The Request_Bodies package supplies memory adapters. Its Files and Channels children supply positional-file and bounded-channel sources.
For a small retained body, call Set_Body. For an upload that must not be retained as a second complete value, pass a limited Request_Body_Source to the streaming Execute overload. Each source implements the bounded Read operation.
The source owns its framing declaration. Declared_Length is queried once before the first read. A known length emits Content-Length; Unknown_Length selects HTTP/1.1 chunked transfer coding.
The caller does not repeat the length at the call site. A known-length source must produce exactly its declared bytes and then report completion. A mismatch or no-progress read raises Request_Body_Error.
Array_Source- Borrows a
Stream_Element_Array, including arrays with nondefault bounds, and declares its exact length. Byte_String_Source- Borrows an Ada
Stringwith the same one-character-to-one-octet mapping asSet_Body. It does not encode text. Bytes_Source- Borrows
Flyology.Bytes.Unbounded_Byteswithout constructing another complete payload. Buffer_Source- Borrows one acquired
Unique_Buffer. The caller keeps its ownership token; the adapter never releases it. Files.Range_Source- Streams an exact positional range from an open descriptor without changing its file position or closing it.
Channels.Channel_Source- Consumes a bounded unique-buffer channel for generated bodies while preserving producer backpressure.
with Ada.Streams;
with Flyology.HTTP.Client;
with Flyology.HTTP.Client.Request_Bodies;
package Client renames Flyology.HTTP.Client;
package Bodies renames Flyology.HTTP.Client.Request_Bodies;
Payload : aliased constant Ada.Streams.Stream_Element_Array := ...;
Source : Bodies.Array_Source (Payload'Access);
Reply : Client.Response :=
Client.Execute (HTTP, Request, Source, Timeout => 30.0);Memory adapters borrow rather than copy. Their payload must outlive the source and remain unchanged until Execute returns. Ada access-discriminant accessibility rejects a source that would escape a local payload.
Memory and file adapters implement Rewindable_Request_Body_Source. For an idempotent request on a stale reused transport, the client may discard that transport and call Rewind. It makes one new attempt only if no response byte arrived. Otherwise, a completed source stays exhausted; rewind it explicitly before a later application-directed execution.
Stream one file range
with Flyology.HTTP.Client.Request_Bodies.Files;
with Flyology.IO.Files;
package Body_Files renames
Flyology.HTTP.Client.Request_Bodies.Files;
package Files renames Flyology.IO.Files;
File : aliased Files.File_Descriptor := Files.Open ("payload.bin");
Source : Body_Files.Range_Source
(File'Access, Offset => 4_096, Count => 1_048_576);
Reply : Client.Response := Client.Execute (HTTP, Request, Source);
Files.Close (File);The descriptor remains application-owned and must stay open through Execute. Reads are positional and receive the remaining whole-exchange timeout and cancellation token. A lightweight timeout waits until kernel cancellation has released the staging buffer. Native pread cannot be interrupted after it enters the kernel, so timeout delivery may wait for that syscall to return. Reaching end of file before the configured range completes is a declared-length error.
Generate under channel backpressure
with Flyology.Buffers;
with Flyology.Buffers.Channels;
with Flyology.HTTP.Client.Request_Bodies.Channels;
Pool : aliased Flyology.Buffers.Pool
(Block_Size => 16 * 1_024, Capacity => 4);
Queue : aliased Flyology.Buffers.Channels.Channel
(Owner => Pool'Access, Capacity => 3);
Source : Flyology.HTTP.Client.Request_Bodies.Channels.Channel_Source
(Pool'Access, Queue'Access);
-- A producer acquires buffers from Pool and sends them with Send_Move.
-- It closes Queue after the final buffer.
Reply : Client.Response := Client.Execute (HTTP, Request, Source);A channel source defaults to Unknown_Length; close the channel after the final buffer so the adapter can emit the terminating chunk. If the total is known beforehand, call Set_Declared_Length with Known_Length before Execute. The length cannot change after reading starts, and producers must send exactly that many bytes—the client does not drain surplus queued buffers after the declared count. The channel and every sent buffer must belong to the supplied pool. The adapter holds at most one received buffer, copies bounded pieces into the transport staging array, and releases that buffer when consumed. Channel waits observe the exchange deadline and cancellation token without polling. Because received buffers cannot be reconstructed after release, this adapter is one-shot and never opts into streamed retry.
Finish a chunked upload with request trailers
Add_Trailer retains a trailer name and value in the request. The client emits the declaration required by HTTP/1.1 and terminates an unknown-length HTTP/1.1, HTTP/2, or HTTP/3 upload with the retained trailer field section. Retained and known-length bodies are rejected.
The client prohibits known fields that can change framing, routing, authentication, request semantics, or payload interpretation. It also rejects repeated names because extension fields are not necessarily list-valued. Use only a field whose definition permits trailers. Trailer values are fixed before Execute. Precompute a checksum or use a repeatable source when the trailer depends on the body.
Client.Add_Trailer (Request, "X-Checksum", Checksum);
Client.Add_Trailer (Request, "X-Upload-Result", "complete");
declare
Reply : Client.Response :=
Client.Execute (HTTP, Request, Unknown_Length_Source);
begin
null;
end;See the API reference for the complete contracts of Flyology.HTTP.Client.Request_Bodies and its Files and Channels children.
Consume or deliberately abandon the response.
Response is limited and owns one exchange lease until body framing completes. Inspect the final status with Status. Read HTTP/1.x text with Reason_Phrase and the selected protocol with Negotiated_Protocol.
Header names, values, and repeated occurrences retain wire order. Use the header enumeration and lookup operations documented in the client API.
Use Read_All when retaining a bounded complete body is appropriate. Maximum is an application limit on decoded representation bytes, not a hint. Exceeding it raises Response_Too_Large.
Reply : Client.Response := Client.Execute (HTTP, Request, Timeout => 5.0);
Body : Flyology.Bytes.Unbounded_Bytes :=
Client.Read_All (Reply, Maximum => 64 * 1_024);
pragma Assert (Client.Status (Reply) = 200);
pragma Assert (Client.Body_Complete (Reply));Reuse synchronous results in a long-running loop
Use the procedural Execute and Read_All overloads when one caller scope performs many synchronous exchanges. These overloads keep one response object and one body object in that scope. They avoid successive function-result temporaries while using the same composable exchange engine.
Reply : Client.Response;
Body : Flyology.Bytes.Unbounded_Bytes;
loop
Client.Execute (HTTP, Request, Reply, Timeout => 5.0);
Client.Read_All (Reply, Body, Maximum => 64 * 1_024);
Consume (Reply, Body);
end loop;The procedural Execute finalizes the previous response before it starts the next exchange. An incomplete previous response therefore closes its HTTP/1.1 or HTTP/3 transport, or resets its HTTP/2 stream. The procedural Read_All clears its destination before reading and leaves it empty if body consumption raises.
For bounded streaming, repeatedly call Read_Body. It removes fixed-length or chunked framing and reports completion only when the complete message is safe. The original Execute deadline remains authoritative; a body read never restarts it. A token passed to a body-reading call is borrowed only for that call.
loop
Client.Read_Body
(Reply, Buffer, Last, Finished, Token => Cancellation'Access);
if Last >= Buffer'First then
Consume (Buffer (Buffer'First .. Last));
end if;
exit when Finished;
end loop;Body_Complete reports when fixed-length, chunked, no-body, or valid close-delimited framing has completed and the lease was released or closed as required. Chunked trailers then become available through Trailer_Count, Trailer_Name, Trailer_Value, and Trailer. Finalizing an incomplete response does not drain unknown application data: it closes that transport so partial framing cannot contaminate a later exchange. Ada accessibility also rejects a response that would outlive its aliased client.
Compose one complete exchange in a parent operation.
The composable overloads in Client drive the request head, request body, response head, and complete response body on the completion-set owner stack. Exchange_Operation becomes terminal only after every protocol stream, pool lease, and borrowed input is released or detached. Synchronous and composable calls are colocated because the provider owns both forms of the same state machine; the limited operation and its completion set express the scoped lifetime.
Use Exchange_To_Buffer for a bounded complete body from one exchange. The destination must hold an acquired writable Flyology.Buffers.Unique_Buffer. Successful start moves its token into the operation and leaves the caller handle vacant.
Buffers.Acquire (Destination);
declare
Set : aliased Operations.Completion_Set (3);
Exchange : Client.Exchange_Operation :=
Client.Exchange_To_Buffer
(Set'Access, HTTP'Access, Request'Access, Destination,
Client.Deadline_After (5.0), Cancellation'Access);
Result : Client.Exchange_Result;
Reply : Client.Response;
begin
Operations.Wait_All (Set);
Client.Finish (Exchange, Result, Reply, Destination);
if Client.Kind (Result) = Client.Response_Complete then
Consume (Destination, Reply);
end if;
end;Create one Monotonic_Deadline before signing or other parent preparation. Deadline_After covers admission, DNS, every address attempt, TLS or QUIC setup, both message bodies, and cancellation drain. The token must outlive the operation and its drain.
The bounded Exchange_Result reports expected environmental outcomes without raising. Inspect its result with Kind. Finish is the sole buffer-restoration authority. It restores the exact token and preserves its pool identity. Only a Response_Complete result commits readable bytes. Every other moved-buffer result restores readable length zero. Start-time validation or slot failure restores the original handle before returning.
Use the established Exchange_To_Buffer overload inside a parent provider. The parent calls Operations.Continue_After and finishes the child after the dependency event. Parent cancellation cancels the child, waits for its drain, and only then publishes the parent result. The child adds no second visible root result.
Client.Exchange_To_Buffer
(HTTP'Access, Request'Access, Destination, Deadline,
Cancellation'Access, Parent.HTTP_Child);
Operations.Continue_After (Parent, Parent.HTTP_Child);
-- In the later Dependency_Changed drive:
Client.Finish
(Parent.HTTP_Child, Result, Reply, Destination);An Operation_Request_Body_Source supplies a one-shot upload without owning a completion-set slot. Its Source_Wait_Source descriptor composes with transport readiness. A blocked source therefore cannot hide an early final response. Composable source requests do not follow redirects or retry because the source has no rewind capability.
Exchange_To_Sink uses an immediate Response_Body_Sink for bodies larger than one configured buffer. A later failure does not roll back bytes already delivered to the sink. Write into unpublished scratch state when partial visibility is unsafe.
The Admission_Certainty state is monotonic. Not_Admitted proves that no request head byte, frame, or datagram was handed off. Possibly_Admitted means that a mutation can have reached the server. Treat cancellation, timeout, transport loss, or an invalid response in that state as an unknown mutation outcome. Response_Observed is diagnostic only. A conditional mutation is conclusive only when the result is Response_Complete and the application successfully parses the complete service response.
Opt into bounded same-origin redirects.
Redirects are returned by default. To follow them, call Set_Redirects with Default_Same_Origin_Redirects. A custom Redirect_Configuration can set an explicit hop limit.
The policy recognizes 301, 302, 303, 307, and 308. It resolves relative, query-only, scheme-relative, and absolute HTTP(S) Location values. Resolution removes fragments and normalizes dot segments.
Client.Set_Redirects
(Request,
(Mode => Client.Follow_Same_Origin,
Maximum_Hops => 3));
declare
Reply : Client.Response :=
Client.Execute (HTTP, Request, Timeout => 5.0);
begin
null;
end;The origin-bound client never follows a redirect to a different scheme, host, or port; it returns that response unchanged, so credentials are not forwarded to another authority. For 301 and 302, POST becomes GET. A 303 becomes GET except that HEAD remains HEAD. A 307 or 308 preserves the method and body. Method rewriting removes the request body, trailers, Expect, and content-specific fields.
A retained body is directly replayable. A streamed body on a method-preserving redirect must implement Rewindable_Request_Body_Source; a one-shot source raises Redirect_Error. The same exception reports invalid or duplicate locations, cycles, and exhausted hop limits. The client drains intermediate bodies before reuse. One monotonic Execute deadline covers the complete chain.
Add explicit Basic or Bearer credentials.
Flyology.HTTP.Client.Authentication contains preemptive request helpers. Set_Bearer validates the RFC 6750 token form. Set_Basic rejects controls and a colon in the user ID. It joins user-id ":" password and applies Base64 as specified by RFC 7617.
The string inputs are already encoded octets. Supply normalized UTF-8 bytes when honoring a UTF-8 challenge.
with Flyology.HTTP.Client.Authentication;
package Client_Auth renames Flyology.HTTP.Client.Authentication;
Client_Auth.Set_Bearer (Request, Access_Token);
-- Or: Client_Auth.Set_Basic (Request, User_Id, Password);
-- Later: Client_Auth.Clear (Request);Each setter atomically replaces every existing Authorization field. Clear removes those fields while preserving all others and their order. The request retains the generated authorization value.
These helpers do not discover protection spaces, parse challenges, refresh tokens, retry 401 responses, or prevent application logs from exposing secrets. Custom schemes remain available through Add_Header.
Observe and stop the pool explicitly.
Execute is a blocking wrapper over the same completion-set exchange engine used by the composable overloads. It waits for an idle transport or a free creation slot inside one absolute request deadline. A safe GET or HEAD may replace one stale reused transport before any response observation. An unconditional idempotent request may be replayed once only when its body is retained or supplied by Rewindable_Request_Body_Source, whose nonblocking Rewind must restore exactly the same bytes and declared length. Conditional mutations, non-idempotent requests, and one-shot streamed requests are never replayed after possible admission.
Diagnostics returns one coherent Client_Diagnostics snapshot. Current counts distinguish pending transports, active exchanges, reusable and closing transports, and admission waiters. Cumulative counts report creations, reuses, closes, stale retries, and admission timeouts.
Exchange and transport counts are separate because synchronous and composable HTTP/2 and HTTP/3 exchanges can share one transport. Every client transport is owner-driven by the active exchange; the client creates no connector or protocol-pump helper task.
Snapshot : constant Client.Client_Diagnostics := Client.Diagnostics (HTTP);
Client.Prune_Idle (HTTP); -- active responses are unaffected
Client.Shutdown (HTTP, 5.0); -- reject admission and drain leasesPrune_Idle closes idle connections without disturbing active leases. Shutdown is terminal. It rejects new admission, interrupts admitted transport operations, closes idle transports, and waits for connecting slots and response leases to drain.
A shutdown timeout leaves the client stopping and may be retried. Finalization also requests shutdown. Call Shutdown explicitly when a bounded, observable drain matters.
Configure authenticated HTTPS and handle explicit limits.
An HTTPS origin requires the Configure overload that receives an initialized provider-neutral TLS backend. The client retains independently owned provider state, so the original backend object may be finalized after configuration. With the optional OpenSSL provider, normal client initialization performs certificate-chain and hostname verification; the maintained command-line client deliberately exposes no insecure bypass.
Flyology.IO.TLS.OpenSSL.Initialize_Client (Backend);
Client.Configure
(HTTP,
Flyology.HTTP.Parse_Origin ("https://example.com"),
Backend'Access,
Client.Default_Pool_Configuration);Default_Pool_Configuration supplies the documented retention defaults. Pass an explicit record when the deployment requires different bounds.
For a complete HTTPS endpoint, derive both the normalized value passed to Parse_Origin and the request target as shown in the URI, IRI, and URL guide.
Callers can distinguish pool shutdown (Client_Closed), resolution or address exhaustion (Connection_Error), source-contract failure (Request_Body_Error), and oversized retained response data (Response_Too_Large). They can also distinguish malformed protocol input (Protocol_Error), timeout, cancellation, established I/O failure, socket failure, and TLS failure. Source-defined exceptions propagate after the leased transport is discarded.
The same API and outcome contracts are tested from native and lightweight tasks. Run ./scripts/http-client-conformance.sh for deterministic request, response, parser, pooling, lifetime, cancellation, timeout, TLS, adapter, and lane-parity coverage. Address cases cover IPv4 and bracketed IPv6 literals, default and explicit ports, IPv6-to-IPv4 resolution fallback, and all-address exhaustion under one deadline. A test-only one-byte receive cap forces every response and chunk/trailer delimiter across distinct client receive calls; TLS cases distinguish an orderly close_notify from transport truncation while streaming fixed-length, chunked, and close-delimited bodies. The parser campaign includes 42 named RFC 9110/9112 response seeds plus 10,000 fixed-seed random or corpus-derived mutated inputs through the production parser oracle. For interactive use, ./showcases/run_http_client_cli.sh -v https://example.com/ builds and runs the maintained curl-like example with complete outbound and inbound headers on standard error.
Own one bounded WebSocket session.
Flyology.HTTP.WebSocket_Client defines a separate origin-bound lifecycle. It does not expose an HTTP pool upgrade. Its Parse_Origin accepts ws:// and wss:// origins, with default ports 80 and 443.
Configure a ws origin without a TLS provider, or retain a provider for authenticated wss. A Request retains the target and handshake metadata. Older overloads that take Flyology.HTTP.Origin remain available for source compatibility. One Client owns at most one transport and must have one active caller. It may reconnect after a completed or aborted connection.
with Flyology.Bytes;
with Flyology.HTTP.WebSocket_Client;
package WebSockets renames Flyology.HTTP.WebSocket_Client;
Socket : WebSockets.Client;
Request : WebSockets.Request;
WebSockets.Configure
(Socket, WebSockets.Parse_Origin ("wss://example.com"),
Backend'Access);
WebSockets.Set_Target (Request, "/events?topic=builds");
WebSockets.Set_Origin (Request, "https://app.example");
WebSockets.Offer_Protocol (Request, "events.v1");
WebSockets.Add_Header (Request, "Authorization", "Bearer " & Token);
WebSockets.Connect (Socket, Request, Timeout => 5.0);
WebSockets.Send (Socket, "ready", Timeout => 5.0);
WebSockets.Receive
(Socket, Kind, Data, Closed,
Max_Message => 256 * 1_024, Timeout => 30.0);
if not Closed then
WebSockets.Close
(Socket, Code => 1_000, Reason => "complete", Timeout => 5.0);
end if;The example uses the WebSocket-origin TLS Configure overload. It sets the request target with Set_Target and the browser origin with Set_Origin. Offer_Protocol adds a subprotocol, and Add_Header adds an application-owned handshake field.
Connect establishes the connection. The string Send overload writes text, Receive returns one bounded message, and Close performs the close handshake.
The endpoint, origin-form target, and browser origin can all come from parsed URLs. See the WebSocket section of the identifier guide.
The handshake generates Host, upgrade fields, version 13, and a fresh operating-system-random key. Add_Header retains end-to-end fields such as credentials and cookies but rejects fields owned by the protocol engine. A request retains at most 16 subprotocol tokens, 256 bytes per token, and four KiB for the serialized offer; the complete serialized request head is capped at 48 KiB before a transport is opened. A present server selection must be one nonempty offered token. The response must be HTTP/1.1 status 101 with the exact accept digest, well-formed upgrade token lists, no message-body framing, no unoffered extension, and no oversized or malformed retained header block. Metadata access becomes available only after all upgrade validation succeeds. Redirects, proxying, authentication challenges, and extension negotiation are not followed implicitly.
Every client frame uses a fresh operating-system-random masking key. The Send overloads accept final text or binary messages up to 16 MiB. They reject oversized strings before constructing byte-array representations and validate text before writing.
Receive accepts only unmasked server frames. It reassembles fragments under the caller's Max_Message bound, validates UTF-8 after text reassembly, answers ping, ignores pong, and bounds control-frame churn. The default message limit is one MiB; the absolute supported limit is 16 MiB. Compression is not offered.
One monotonic deadline covers DNS, each address attempt, optional TLS, and the complete upgrade. Send, receive, and close each use a separate whole-operation deadline. A timeout or cancellation during a frame terminates the connection. Partially consumed framing cannot resume as another value.
Close sends a validated masked status and waits for the peer close. It discards complete data messages that crossed the close before the peer observed it. Peer-initiated close is acknowledged automatically and retained through the close metadata operations. Abort_Connection performs terminal cleanup without a handshake.
The behavioral suite runs the same public client from native and lightweight tasks against the production server engine. It checks Origin and subprotocol negotiation, response metadata, request and message bounds, masking, fragmented text with interleaved ping/pong, text and binary exchange, malformed server masking, negative upgrade status/digest/protocol/extension/header cases, registered close codes, crossed data during close, saturated-send deadlines, operation cancellation, partial-frame timeouts, both closing directions, and reconnection. A separate OpenSSL fixture checks authenticated WSS exchange and bidirectional TLS close notification in both task lanes. The SPARK suite proves exact fragmentation/message-bound actions as well as the scalar policy for canonical frame lengths, server masking and opcode validation, and close-code validity; socket, TLS, entropy, hashing, parsing, UTF-8, and byte I/O remain behavioral verification boundaries.