Configure one origin and a bounded pool.
Flyology.HTTP.Client is origin-bound, concurrency-safe after configuration, and synchronous in both task lanes. Parse one normalized http:// or https:// origin, declare the maximum number of open plus connecting transports as the client discriminant, and call Configure 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 currently connecting transports. Requests wait for admission rather than creating an unbounded number of sockets.
Max_Idle- Caps retained reusable transports; zero disables reuse without disabling concurrent requests.
Idle_Timeout- Rotates a connection after that many idle seconds. A negative value disables this age check.
Max_Connection_Age- Bounds total reusable lifetime. A negative value disables the check.
Max_Requests_Per_Connection- Rotates after a request count; zero disables count-based rotation.
Parse_Origin accepts only a scheme, host, and optional port. It rejects user information, query, fragment, and nontrivial paths; omitted ports become 80 or 443. The client never mixes origins or TLS state, and there is no public connection checkout operation. Concurrent callers may share the configured client; mutable request values and body sources still require ordinary application synchronization or per-call ownership.
Build validated request metadata.
A new Request is GET /. Use the constants in Flyology.HTTP.Methods for standard methods, or To_Method for a validated extension token. Is_Safe and Is_Idempotent classify standard methods conservatively; unknown extensions are unsafe and non-idempotent. The current client rejects CONNECT because it has no tunnel handoff, and 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 target is HTTP origin-form; * is accepted only for OPTIONS. Absolute-form, authority-form, fragments, spaces, controls, non-ASCII bytes, and targets over 8 KiB are rejected. Repeated headers preserve insertion order. Add_Header rejects malformed fields and client-controlled framing, Host, connection, upgrade, and Expect fields. Set_Body accepts a contiguous byte array or a String with a one-character-to-one-octet mapping and retains owned bytes in the request.
Set_Expect_Continue explicitly generates Expect: 100-continue for a nonempty retained or streamed body. The client sends the head first and waits up to the configured continue interval. A 100 Continue sends the body, a final response suppresses it, and an interval with no response byte falls back to sending without restarting the whole deadline. A 417 Expectation Failed received before body transmission consumes the single automatic-retry budget and is retried on a fresh transport without Expect. If a partial response head has arrived, the client finishes it rather than interleaving 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.
For a small retained body, call Set_Body. For an upload that should not be retained as a second complete value, pass a limited Request_Body_Source to the streaming Execute overload.
The source owns its framing declaration. Execute queries Declared_Length once before the first read: a known length emits Content-Length, while Unknown_Length selects HTTP/1.1 chunked transfer coding. The caller does not repeat the length at the Execute call site. A positive known-length source that ends early, produces too many bytes, or reaches the declared count without setting Finished raises Request_Body_Error; a source that returns neither bytes nor Finished is rejected for making no progress.
Array_Source- Borrows a
Stream_Element_Array, including arrays with nondefault bounds, and declares its exact length. Byte_String_Source- Borrows an Ada
Stringusing the same one-character-to-one-octet mapping asSet_Body. It does not perform text encoding. Bytes_Source- Borrows
Flyology.Bytes.Unbounded_Byteswithout constructing another complete payload. Buffer_Source- Borrows one acquired
Unique_Buffer. The caller keeps its ownership token, and 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, call Rewind, and make one new attempt if no response byte arrived. A completed source otherwise stays exhausted; call Rewind explicitly before an application-directed later 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 (Source, Client.Known_Length (Total)) 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 one Trailer declaration in the request head and the physical fields after the terminating chunk. Request trailers are therefore accepted only with an unknown-length streaming source; retained and known-length bodies are rejected. Known fields that could change framing, routing, authentication, request semantics, or payload interpretation are prohibited, and repeated names are rejected because arbitrary extension fields are not necessarily list-valued. The caller must use a field whose definition permits trailers. Trailer values are fixed before Execute; compute a digest before the call when it must be carried as a trailer.
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 its body framing completes. Inspect the final status with Status, its HTTP/1.x text with Reason_Phrase, and the opaque negotiated protocol with Negotiated_Protocol. Header names and values retain wire order and repeated occurrences: iterate with Header_Count, Header_Name, and Header_Value, or retrieve a named occurrence with Header.
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));For bounded streaming, repeatedly call Read_Body. It removes fixed-length or chunked framing and reports Finished only when the complete message is safe. The original Execute deadline remains authoritative; it is never restarted by a body read. A token passed to Read_Body or Read_All is borrowed only for that call, and the response never retains it.
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;Completing a fixed-length, chunked, no-body, or valid close-delimited response releases or closes the lease as required. Chunked trailers become available only after completion 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.
Opt into bounded same-origin redirects.
Redirects are returned by default. Call Set_Redirects (Request, Default_Same_Origin_Redirects), or provide a Redirect_Configuration with an explicit Maximum_Hops, when automatic following is appropriate. The policy recognizes 301, 302, 303, 307, and 308. Relative, query-only, scheme-relative, and absolute HTTP(S) Location values are resolved with fragment removal and dot-segment normalization.
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. Intermediate bodies are drained before their transport is reused, and 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, joins user-id ":" password, and applies Base64 as specified by RFC 7617. Its 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 them while preserving all other request fields 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 code from logging secrets. Custom schemes remain available through Add_Header.
Observe and stop the pool explicitly.
Execute waits for an idle transport or a free creation slot inside the request deadline. An idempotent request may be retried once when a reused transport fails before any response byte arrives; the retry stays inside the same deadline. Empty and retained bodies can be replayed directly. A streamed body is eligible only through Rewindable_Request_Body_Source, whose nonblocking Rewind must restore exactly the same bytes and declared length. Non-idempotent and one-shot streamed requests are never replayed automatically.
Diagnostics returns one coherent 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 intentionally separate so the vocabulary can survive a later multiplexed protocol.
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 currently 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, but an application should call Shutdown where 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);Callers can distinguish pool shutdown (Client_Closed), resolution or address exhaustion (Connection_Error), source-contract failure (Request_Body_Error), oversized retained response data (Response_Too_Large), 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.