Start the HTTP server.
The maintained kitchen-sink server uses the unified application listener. It generates temporary development credentials, redirects cleartext HTTP on port 18081 to HTTPS on port 18082, binds TLS/TCP and QUIC/UDP to the secure port, and sends HTTP/1.1, HTTP/2, and HTTP/3 through one router and shared ingress budget. Its HTML, CSS, JavaScript, brand mark, and variable font are read through Flyology.IO.Files.Read_At, while its UI drives SSE, WebSocket chat, streamed upload, middleware, structured request work, and live application introspection.
OpenSSL.Initialize_Server
(Backend,
Certificate_File => Certificates.TLS_Certificate_File (Generated),
Private_Key_File => Certificates.TLS_Private_Key_File (Generated),
Protocols => ALPN."&" (ALPN.Offer ("h2"), "http/1.1"));
State.Routes.Serve
(State.Application,
HTTP_Endpoint => Sockets.Network_Endpoint
(Sockets.Loopback_IPv4, HTTP_Port),
HTTPS_Endpoint => Sockets.Network_Endpoint
(Sockets.Loopback_IPv4, Port),
HTTPS_Origin => Flyology.HTTP.Parse_Origin
("https://127.0.0.1:18082"),
TLS_Backend => Backend,
Certificate_DER => Certificate_DER,
Private_Key => Private_Key,
Handler_Model => Model,
TCP_Capacity => Capacity,
HTTP_3_Capacity => HTTP_3_Capacity,
Timeout => 120.0,
Header_Timeout => 10.0,
Ingress => State.Budget'Access,
Token => Stop'Access);This example calls the single-address-family HTTP+HTTPS Routing.Serve overload. It uses Parse_Origin for the redirect origin and the development identity's TLS_Certificate_File and TLS_Private_Key_File.
The complete-request deadline remains absolute, but the shorter header deadline rejects an incremental slow header first. Admitted routes narrow the longer deadline for ordinary work; explicitly long-lived SSE and WebSocket routes retain bounded lifecycle time.
Build with the prepared RTS, then choose the handler lane explicitly. In the command below, argument 1 selects the lane, argument 2 is the CPU aspect, argument 3 is the secure port, argument 4 is handler capacity, and argument 5 is the asset root. Argument 6 sets H3 connection capacity; it defaults to 128 and may not exceed 256. Optional argument 7 changes the cleartext redirect port.
cd showcases && alr build && cd ..
./showcases/bin/http_application_server lightweight 0 18082 256 . 128
./showcases/bin/http_application_server native 0 18082 256 . 128
# optional seventh argument changes the cleartext port from 18081
# open https://127.0.0.1:18082/ and accept the development certificateThe selected handler model applies to H1/H2 connection workers and H3 connection workers. The showcase build prepares one automatic execution group per online host CPU, capped at 128. Lightweight handler tasks without an explicit CPU aspect are assigned round-robin, so the server can execute requests on separate event-loop pthreads in parallel. Set FLYOLOGY_LOOP_POOL_SIZE before building to make a smaller topology reproducible.
Keep the low-level connection API available.
Flyology.HTTP.Server is the protocol foundation. An application may call Read_Request, inspect headers, explicitly accept a body, and stream decoded bytes. It can write fixed, chunked, SSE, or WebSocket responses without a router.
Connection_Handlers supplies the smallest persistent-loop adapter. The example below uses the public Connection and Request types and the fixed-body Respond operation.
procedure Route
(Item : in out Flyology.HTTP.Server.Connection;
Value : Flyology.HTTP.Server.Request) is
begin
Flyology.HTTP.Server.Respond
(Item, 200, "text/plain; charset=utf-8", "hello" & ASCII.LF);
end Route;
package Handler is new
Flyology.HTTP.Server.Connection_Handlers (Route);The connection is sole-writer state. It must not be retained past the handler, shared among producer tasks, or closed by a borrowed transport.
When one low-level task must wait for HTTP with other Flyology operations, use the composable overloads directly in Server. These overloads produce bounded request-head and body operations beside their synchronous forms. The composable operations guide explains transport support, slot sizing, typed Finish, cancellation, and borrowed lifetimes.
Introduce a borrowed Exchange.
Applications.Exchange borrows the parsed request, connection, application context, cancellation token, peer, and absolute deadline. It also holds response state, route metadata, body policy, request ID, principal, and helpers. Neither the exchange nor any value reached through it may escape the handler call.
procedure Show_User
(State : in out Application_Context;
X : in out App.Exchange) is
begin
Complete (State);
X.Text (200, "user " & X.Parameter ("id") & ASCII.LF);
end Show_User;The example calls Text and Parameter through Ada's prefixed notation. The exchange does not force buffering. X.Read_Body reads a streamed route incrementally. Begin_Stream, Write_Chunk, and End_Stream preserve synchronous transport backpressure.
The same Applications.Exchange exposes the exact validated authority, original target octets, and physical request fields in occurrence order. Use Request_Authority, indexed Request_Header_Name/Request_Header_Value, or the occurrence overload of Request_Header when canonicalization must not depend on comma joining. After Body_Complete, the corresponding Request_Trailer operations expose terminal fields in physical order. Calling them earlier raises Program_Error. These APIs do not expose connection ownership.
Use the Content_Length overload of Begin_Stream when the representation length is known. HTTP/1.1 emits Content-Length without chunked transfer coding; HTTP/2 and HTTP/3 carry the equivalent field. The server rejects an overrun before writing that chunk and fails an underrun at End_Stream. A HEAD response advertises the declared representation length while suppressing body writes, so handlers need not generate or retain the representation.
X.Begin_Stream
(Status => 200,
Content_Type => "application/octet-stream",
Content_Length => Object_Length);
-- Write borrowed chunks under transport backpressure.
X.End_Stream;Request metadata stays bounded: the ordinary client accepts targets through 16 KiB, HTTP/1 retains at most 32 KiB for a request head or trailer section, and ordered header collections retain their configured field-count and byte limits. Route body limits and shared ingress budgets remain independent, so admitting a long presigned target does not enlarge buffered payload storage.
Route by method and decoded path.
A Routing router matches static segments, {name}, and one final {*remainder}. Its Get operation registers GET routes. The router provides automatic 404, 405 with Allow, and HEAD fallback to GET. It also declares strict, ignored, or redirected trailing-slash behavior.
Path percent-decoding rejects encoded separators, NUL, controls, malformed UTF-8, empty ambiguous segments, and malformed escapes. + is literal in paths.
State.Routes.Get ("/", Home'Access, Name => "home");
State.Routes.Get
("/users/{id}", Show_User'Access, Name => "users.show");
Admin_Routes.Get
("/status", Admin_Status'Access, Name => "status");
State.Routes.Mount
("/admin", Admin_Routes, Name_Prefix => "admin.");Mount copies the subrouter's routes and middleware when called. Complete a subrouter before mounting it. Registering a route or middleware on an already-mounted router raises Route_Error instead of leaving mounted copies incomplete.
Query parsing is lazy. Repeated values retain occurrence order, invalid percent escapes are rejected, and + becomes a space only in the query component.
After setup, Route_Count, Describe_Route, and middleware-description operations return owned metadata in deterministic registration order. Applications can build diagnostics without exposing handler access values.
Each router introspection call reads the generation published when that call runs. A count from one call and an index used in the next can therefore straddle a commit. Take one Snapshot with Take_Snapshot and traverse that instead. Every operation on one snapshot reads the same generation. Dispatch does not allocate descriptions or perform a name lookup.
Traverse one generation at a time.
Route_Count and Describe_Route on a snapshot describe the captured generation, not the published one. A diagnostics endpoint can therefore enumerate routes while a control plane commits.
declare
View : Routing.Snapshot;
begin
State.Routes.Take_Snapshot (View);
for Index in 1 .. Routing.Route_Count (View) loop
Report (Routing.Describe_Route (View, Index));
end loop;
end;Publish a complete router update.
The registration overloads can return an opaque Route_ID or Middleware_ID. Keep these values when the application will update the router. IDs remain stable across replacements and are never reused after removal. Route names remain useful for diagnostics and external lookup, but they are not the runtime identity.
The Add_Route_Middleware overload below accepts a route ID and returns the middleware ID. The global Add_Middleware overload can also return a middleware ID.
Users_Show : Routing.Route_ID;
Audit_Layer : Routing.Middleware_ID;
State.Routes.Get
("/users/{id}", Show_User'Access,
Route => Users_Show,
Name => "users.show");
State.Routes.Add_Route_Middleware
(Route => Users_Show,
Component => Audit'Access,
Middleware => Audit_Layer,
Middleware_Name => "audit");For a runtime change, declare an Update and call Begin_Update. This call clones the current immutable generation. Apply Replace_Handler, Set_Policy, and Replace_Middleware to the unpublished candidate. One Commit validates and atomically publishes the complete candidate.
loop
declare
Change : Routing.Update;
begin
State.Routes.Begin_Update (Change);
Routing.Replace_Handler
(Change, Users_Show, Show_User_V2'Access);
Routing.Set_Policy
(Change, Users_Show, New_User_Policy);
Routing.Replace_Middleware
(Change, Audit_Layer, Audit_V2'Access);
State.Routes.Commit (Change);
exit;
exception
when Routing.Stale_Update =>
null; -- Rebuild from the newer generation.
end;
end loop;If another writer publishes first, Stale_Update rejects the older candidate. Stale_Update also releases the candidate, so the same update object can be rebuilt from the new generation and committed again. A candidate that fails validation stays active instead, so the application can correct it and commit it. Abandon discards a candidate the application no longer wants. The candidate Add and Remove operations change routes. Remove_Middleware changes middleware chains. Mounting copies a middleware registration into the mounted routes without changing its identity, so one candidate operation on that identity reaches the mounted copies too.
Each request performs one acquire load of the current generation and uses that generation for the complete dispatch. A concurrent commit therefore affects only later dispatches.
The first dispatch seals the router. A direct registration procedure or setter called after that raises Route_Error and names the update path. The seal keeps a late registration from changing a generation while a request reads it. Complete direct registration before the server starts.
The router retains every superseded generation, so an in-flight request and a live snapshot never read released configuration. Retained_Generations reports how many generations the router holds. Each commit adds one, and the router releases them at finalization. Reclaim releases every superseded generation and keeps the published one. That retention is what keeps in-flight readers valid, so Reclaim states a precondition the router cannot check: no dispatch is in progress, and no snapshot of a superseded generation is still in use. Call it on a drained server, between serving cycles.
-- After the listener returned and every request task completed.
if State.Routes.Retained_Generations > Generation_Budget then
State.Routes.Reclaim;
end if;Keep routes and exchanges when the protocol changes.
The high-level API is shared by HTTP/1.1 and HTTP/2. Pass the same router, application context, and accepted Flyology connection to the protocol-selecting Routing.Serve overload. Middleware, route policies, request-body reads, fixed and streamed responses, and SSE retain synchronous Ada semantics.
Sockets.Bind_Socket
(Listener,
Sockets.Network_Endpoint (Sockets.Loopback_IPv4, 8_443));
-- Connection has already completed TLS with h2/http/1.1 ALPN offers.
State.Routes.Serve
(State.Application, Connection, Peer,
Mode => Flyology.HTTP.Server.ALPN_Negotiated,
Timeout => 30.0,
Token => Cancellation);The Protocol_Mode literal HTTP_1_Only selects the HTTP/1.x engine. HTTP_2_Only selects prior knowledge or an already selected h2 TLS connection. ALPN_Negotiated reads an upgraded TLS channel's selection and chooses h2 or HTTP/1.x. See the HTTP/2 guide for the complete call shape and limits.
Serve HTTP/1.1, HTTP/2, and HTTP/3 through one router.
The secure single-endpoint Routing.Serve overload binds TLS/TCP and QUIC/UDP to the same concrete endpoint. TLS ALPN selects h2 or http/1.1 on TCP; UDP carries HTTP/3 over the Ada-native flyology_quic transport. All three protocols dispatch through the same registered routes, middleware, body policies, and application context. A handler can inspect X.Request_Protocol only when its behavior genuinely differs by protocol.
The example declares a Routing.Router with the Strict_Slashes policy before it registers routes.
Routes : aliased Routing.Router
(Capacity => 8, Slashes => Routing.Strict_Slashes);
Application : aliased Application_Context;
Backend : aliased OpenSSL.OpenSSL_Provider;
Stop : aliased Flyology.Cancellation.Token;
Routes.Get ("/hello/{name}", Hello'Access, Name => "hello");
OpenSSL.Initialize_Server
(Backend, "server-cert.pem", "server-key.pem",
Protocols => ALPN."&" (ALPN.Offer ("h2"), "http/1.1"));
Routes.Serve
(Application,
Sockets.Network_Endpoint (Sockets.Any_IPv4, 443),
Backend,
Certificate_DER => Certificate_DER,
Private_Key => Private_Key,
Token => Stop'Access);The Routing.Serve call owns the TCP and UDP listeners until the supplied cancellation token is requested. TLS/TCP HTTP/1.1 and HTTP/2 remain live while UDP HTTP/3 serves the same router; clients can use both protocol stacks at the same time. Complete direct route and middleware registration before entering the call. Use router updates for later changes. TCP connection handling and the fixed-capacity HTTP/3 worker set run concurrently, so mutable fields in the shared application context must provide their own synchronization. The drain timeout bounds cooperative shutdown only; a handler that does not reach a cancellation-aware operation can delay return.
Serve or redirect cleartext HTTP beside HTTPS.
The single-family HTTP+HTTPS Serve overload adds a distinct cleartext TCP endpoint. Redirect_To_HTTPS is the default policy. It reads one valid HTTP/1.x request and returns a method-preserving 308 to the configured HTTPS_Origin. It never builds the redirect authority from the request's Host field. Serve_Cleartext instead routes cleartext HTTP/1.x through the same application. Cleartext direct responses do not advertise HTTP/3.
Routes.Serve
(Application,
HTTP_Endpoint => Sockets.Network_Endpoint (Configured_IPv4, 80),
HTTPS_Endpoint => Sockets.Network_Endpoint (Configured_IPv4, 443),
HTTPS_Origin => Flyology.HTTP.Parse_Origin
("https://www.example.com"),
TLS_Backend => Backend,
Certificate_DER => Certificate_DER,
Private_Key => Private_Key,
Cleartext => Routing.Redirect_To_HTTPS,
Cleartext_Capacity => 64,
TCP_Capacity => 64,
HTTP_3_Capacity => 128,
Token => Stop'Access);HTTP and HTTPS must use different concrete ports; the server does not sniff cleartext and TLS on one TCP port. A request handler can inspect X.Request_Scheme as Plain_HTTP or Secure_HTTPS. Scheme is separate from request protocol: HTTP/1.1 can arrive through either endpoint, while HTTP/2 may be cleartext prior knowledge or secure ALPN and HTTP/3 is secure. Separate cleartext, secure TCP, and H3 capacities prevent one transport class from consuming another's admission budget.
Bind HTTP and HTTPS on IPv4 and IPv6 together.
The four-endpoint Serve overload owns cleartext TCP plus secure TCP and UDP listeners for both address families. The IPv4 and IPv6 cleartext endpoints share one port, the IPv4 and IPv6 secure endpoints share another, and the two ports must differ. Cleartext_Capacity, TCP_Capacity, and HTTP_3_Capacity are independent totals divided between the address families; each total must be at least two.
Routes.Serve
(Application,
IPv4_HTTP_Endpoint =>
Sockets.Network_Endpoint (Configured_IPv4, 80),
IPv6_HTTP_Endpoint =>
Sockets.Network_Endpoint (Configured_IPv6, 80),
IPv4_HTTPS_Endpoint =>
Sockets.Network_Endpoint (Configured_IPv4, 443),
IPv6_HTTPS_Endpoint =>
Sockets.Network_Endpoint (Configured_IPv6, 443),
HTTPS_Origin => Flyology.HTTP.Parse_Origin
("https://www.example.com"),
TLS_Backend => Backend,
Certificate_DER => Certificate_DER,
Private_Key => Private_Key,
Cleartext => Routing.Redirect_To_HTTPS,
Cleartext_Capacity => 64,
TCP_Capacity => 64,
HTTP_3_Capacity => 128,
Token => Stop'Access);When a deployment does not expose cleartext HTTP, the secure-only dual-stack Serve overload takes just an IPv4 HTTPS endpoint followed by an IPv6 HTTPS endpoint. Both use the same concrete port so one Alt-Svc authority remains valid, and each endpoint binds TLS/TCP plus QUIC/UDP.
Routes.Serve
(Application,
Sockets.Network_Endpoint (Configured_IPv4, 443),
Sockets.Network_Endpoint (Configured_IPv6, 443),
Backend,
Certificate_DER => Certificate_DER,
Private_Key => Private_Key,
TCP_Capacity => 64,
HTTP_3_Capacity => 128,
Token => Stop'Access);Let HTTP/1.1 and HTTP/2 clients discover HTTP/3.
Every routed HTTP/1.1 and HTTP/2 response from the unified server automatically advertises the active UDP endpoint. On the standard HTTPS port the field is exactly:
Alt-Svc: h3=":443"; ma=86400The authority port follows the endpoint supplied to Serve, so a server on port 4_433 advertises h3=":4433". The secure Serve overload's Alt_Svc_Max_Age parameter changes the lifetime; passing zero asks clients to remove the cached alternative. HTTP/3 responses omit this discovery field because the request already used the advertised service. Applications that use the accepted-connection Serve overload can supply an explicit Alt_Svc value instead.
Generate temporary credentials for local development.
The public Flyology.HTTP.Server.Development_Certificates package creates a self-signed RSA identity for TLS/TCP and a self-signed Ed25519 identity for the current QUIC profile. Declare an Identity and call Generate. Read its QUIC_Certificate_DER and QUIC_Private_Key, and let the TLS provider load the files returned by TLS_Certificate_File and TLS_Private_Key_File. Call Discard before entering the blocking server. Finalization is a cleanup fallback.
package Certificates renames
Flyology.HTTP.Server.Development_Certificates;
Credentials : Certificates.Identity;
Certificates.Generate (Credentials);
declare
Certificate_DER : constant Ada.Streams.Stream_Element_Array :=
Certificates.QUIC_Certificate_DER (Credentials);
Private_Key : constant Flyology.QUIC.Connections.Ed25519_Private_Key :=
Certificates.QUIC_Private_Key (Credentials);
begin
OpenSSL.Initialize_Server
(Backend,
Certificates.TLS_Certificate_File (Credentials),
Certificates.TLS_Private_Key_File (Credentials),
Protocols => ALPN."&" (ALPN.Offer ("h2"), "http/1.1"));
Certificates.Discard (Credentials);
Routes.Serve
(Application,
Sockets.Network_Endpoint (Sockets.Any_IPv4, 4_433),
Backend,
Certificate_DER => Certificate_DER,
Private_Key => Private_Key,
Token => Stop'Access);
end;Generate covers localhost and 127.0.0.1 for one day. OpenSSL with Ed25519 support must be installed; an explicit command may be passed to Generate, and conventional OpenSSL 3 paths plus FLYOLOGY_HTTP_OPENSSL are checked before PATH. The package is deliberately development-only and does not replace production certificate issuance, trust, rotation, or private-key storage.
Supply deployment identities in the forms required by TLS and QUIC.
The ALPN-capable TLS provider reads the certificate and private key used for HTTP/1.1 and HTTP/2. The current QUIC profile accepts the Ed25519 certificate as DER plus the matching raw 32-byte private key. A deployment may use different certificates for TCP and QUIC, but clients must be able to validate each one for the requested hostname.
Use the bounded listener or single-peer adapter when you own UDP.
Serve_HTTP_3_Listener runs the fixed-capacity connection registry and worker set on an application-owned, unconnected UDP socket. The source-generated-identifier Serve_HTTP_3 overload remains the smaller single-connection adapter; a second overload accepts an application-supplied connection identifier. These lower-level calls are useful when another component owns binding or lifecycle, but they do not start the TLS/TCP listener or automatically derive the same-port discovery value.
The maintained http3_application_server uses the public development-certificate API and runs the unified shape. Clients must explicitly accept its self-signed certificate. Its ordinary development command is:
./showcases/bin/http3_application_server \
4433 4080The first port is HTTPS for TLS/TCP and QUIC/UDP; the second is cleartext HTTP and defaults to a 308 redirect to the configured secure origin. For a stable identity, pass TLS_CERT.pem TLS_KEY.pem QUIC_CERT.der QUIC_KEY.raw [HTTPS_PORT [HTTP_PORT]]. Both certificates must cover the hostname, while the current QUIC key must be Ed25519.
curl -i \
http://127.0.0.1:4080/hello/testcurl --version # Features must include HTTP3
curl -k --http3-only \
https://127.0.0.1:4433/hello/testThe -k flag accepts the development certificate; do not use it with production services. macOS's system curl does not currently include HTTP/3, so install an HTTP/3-enabled curl or use the maintained Ada interoperability clients. A normal request negotiating HTTP/2 does not test H3. Use 127.0.0.1 because the showcase listener binds IPv4.
Verify the implemented profile against independent peers.
./scripts/test.sh
./scripts/test-http3-interop.sh all
./scripts/test-http3-h3spec.sh
./scripts/test-http3-stress.shThe mandatory interoperability command runs Ada client and server roles against pinned aioquic and quic-go implementations. It covers QUIC v1/TLS 1.3 establishment, HTTP/3 control streams and SETTINGS, routed GET and POST requests, concurrent request streams, request bodies, response metadata, and retained-connection reuse within the current static-QPACK profile. The Ada driver accepts UDP payloads through 1,350 bytes; peers retain the required 1,200-byte client Initial padding. The deterministic suite separately consumes published QUIC, TLS, cryptographic, and QPACK vectors.
Select body behavior per route.
Every route explicitly rejects, streams, buffers, or discards a request body. Protocol body limits and byte counters use the 64-bit-safe Body_Size type. The server-wide Max_Request_Body is 50 TB, matching the current S3 object ceiling, and each route may narrow it with Max_Body. This streaming ceiling does not authorize object-sized retention: on HTTP/1.x, buffered bodies and retained WebSocket messages reserve from a server-wide shared ingress budget before allocation. HTTP/2 uses fixed per-stream receive buffers; the accepted-connection Ingress parameter does not yet account for HTTP/2 retention. Streaming writes directly into the caller's bounded buffer and still observes the maximum decoded size and original request deadline.
The example registers two Post routes. Each derives a Route_Policy from Default_Route_Policy and selects Buffer_Body or Stream_Body.
State.Routes.Post
("/echo", Buffered_Echo'Access, Name => "echo",
Policy =>
(Routing.Default_Route_Policy with delta
Body_Handling => App.Buffer_Body,
Max_Body => 64 * 1_024));
State.Routes.Post
("/upload", Upload'Access, Name => "upload",
Policy =>
(Routing.Default_Route_Policy with delta
Body_Handling => App.Stream_Body));The default rejects bodies. Discard is available only when an endpoint deliberately opts in; it is never hidden middleware behavior.
Compose deterministic around middleware.
A middleware component receives the typed context, exchange, and a single-use Next_Handler. Code before Next.Call runs inward; code after it runs outward. A component may short circuit with a response. Global components wrap route-local components in registration order. Request-head components run before body acceptance and delayed 100 Continue; application-stage components run afterward.
Add_Middleware registers each global component in the maintained order shown below.
State.Routes.Add_Middleware
(Error_Middleware.Call'Access, Name => "errors");
State.Routes.Add_Middleware
(Request_ID_Middleware.Call'Access, Name => "request-id");
State.Routes.Add_Middleware
(Logging_Middleware.Call'Access, Name => "access-log");
State.Routes.Add_Middleware
(Metrics_Middleware.Call'Access, Name => "metrics");
State.Routes.Add_Middleware
(CORS_Middleware.Call'Access, Name => "cors");
State.Routes.Add_Middleware
(Rate_Middleware.Call'Access, Name => "rate-limit");
State.Routes.Add_Middleware
(Bulkhead_Middleware.Call'Access, Name => "bulkhead");
State.Routes.Add_Middleware
(Security_Middleware.Call'Access, Stage => Routing.Application,
Name => "security-headers");Routing.Application places the security-header component in the application stage.
Map errors only while framing is safe.
Error middleware distinguishes protocol failures, timeouts, cancellation, ingress exhaustion, rate rejection, bulkhead rejection, expected application errors, and unexpected exceptions. Detailed exception information goes only to the configured sink. An unexpected exception becomes a generic closing 500 before response start. Once bytes may have been written, the connection is marked failed and closed; middleware never attempts a replacement response over ambiguous framing.
Application scopes may install an expected-error mapper. Mappers are ordinary callbacks and can be instantiated separately for different routers.
Keep identity and cross-origin policy application-owned.
Authentication middleware parses the authorization scheme and credential, then delegates verification to an application hook. The example token is intentionally only an example. Credentials are never passed to logging or metrics. A route's Authentication field selects an Authentication_Mode. Required routes receive a generic challenge when the hook does not install a principal.
Add_Route_Middleware attaches the authentication component to the named route.
State.Routes.Get
("/private", Private_Profile'Access, Name => "private",
Policy =>
(Routing.Default_Route_Policy with delta
Authentication => Routing.Required_Authentication,
CORS_Policy => 1));
State.Routes.Add_Route_Middleware
("private", Authentication_Middleware.Call'Access);Routing.Required_Authentication selects the required-authentication route policy.
CORS policies use explicit origins, methods, and headers. Wildcard origin plus credentials is rejected. Preflights run before body admission and emit the required Vary fields. Browser security headers are configurable; HSTS remains off unless the deployment is intentionally HTTPS-only.
Bound rate, concurrency, and time.
The token-bucket limiter uses a bounded, sharded key table and a monotonic clock. The application chooses the client key; forwarded proxy headers are not trusted implicitly. Bulkheads reject without waiting and release controlled permits during normal return, exceptions, cancellation, abort, and finalization. Route and middleware deadlines may only narrow the original absolute deadline.
Policy =>
(Routing.Default_Route_Policy with delta
Timeout => 1.0,
Concurrency => 8,
Rate_Per_Second => 10)Rate rejection is 429 and bulkhead rejection is 503. Both can emit Retry-After and bounded denial metrics.
Observe stable route names, not attacker paths.
Request-ID middleware writes the accepted identifier to Exchange.Request_ID. It generates a bounded safe identifier unless an explicit trust policy accepts a valid inbound value. Its protected fly-N counter remains the zero-configuration default. An optional callback receives application state and a read-only borrowed exchange, so an application can provide a task-safe UUID, trace-context, or deployment-specific generator. Returned values must still pass the middleware's character and 128-byte checks before reaching response headers, logs, or metrics.
procedure Generate_Request_ID
(State : in out Application_Context;
X : App.Exchange;
Value : out Ada.Strings.Unbounded.Unbounded_String)
is
pragma Unreferenced (X);
begin
State.Request_IDs.Generate (Value);
end Generate_Request_ID;
package Request_ID_Middleware is new
Flyology.HTTP.Server.Middleware_Request_IDs
(Application_Context, Routing.Components,
Trust_Inbound => False,
Generate => Generate_Request_ID'Access);The Middleware_Request_IDs generic binds the application's state type to the router's Routing.Components instance.
The showcase keeps uuids in its own Alire manifest rather than imposing that choice on core Flyology applications. Access logs include method, route name, optional safe target, status, request ID, peer, byte counts, and monotonic elapsed time. They do not receive authorization, cookies, or bodies. The default in-memory metrics sink caps route/method/status-class series and reports dropped series rather than growing without bound.
The maintained /metrics endpoint demonstrates reading a snapshot without coupling the server to a monitoring format. Applications can instead implement the sink interface for their metrics system.
State.Application.Introspection :=
Ada.Strings.Unbounded.To_Unbounded_String
(Build_Routing_JSON (State.Routes));The kitchen sink exposes that immutable registry at /introspection: route methods, patterns, stable names, body and admission policies, upgrades, and named middleware. Its /runtime/events SSE endpoint samples the stack pool, bounded HTTP metrics, and Flyology.Observability.Snapshot for already-created execution groups every 750 milliseconds. Reading the feed neither creates configured groups nor instruments each dispatch.
Execution-group members include parked lightweight handler tasks provisioned by server capacity, while active HTTP requests are reported separately. Native handlers do not appear as execution-group members. Route and policy metadata can reveal application structure, so exposing these endpoints outside a development or protected operational surface is an application deployment decision.
Stream uploads under one deadline.
loop
X.Read_Body (Buffer, Last, Finished);
if Last >= Buffer'First then
Total := Total + Flyology.HTTP.Body_Size (Last - Buffer'First + 1);
end if;
exit when Finished;
end loop;Routing, authorization, body policy, ingress capacity, and other request-head middleware finish before 100 Continue is sent. Header and complete-request clocks both begin before the first header byte. A route can narrow the latter after admission, but sending one byte at a time cannot restart either deadline. Fixed Content-Length and chunked transfer coding follow the same policy.
The browser demo also exercises the response direction: maintained static assets are read into a bounded 32 KiB Stream_Element_Array and passed directly to the binary X.Write_Chunk overload.
curl --http1.1 -H 'Expect: 100-continue' \
-k --data-binary @large.bin https://127.0.0.1:18082/uploadSerialize SSE through a bounded mailbox.
The raw SSE procedures remain available. The optional SSE_Handlers lifecycle lets producers publish to a fixed-capacity, byte-bounded, closeable channel. Its Session discriminants set capacity and byte limits; Default_Session_Bytes supplies the default per-session limit.
The maintained browser feed starts a producer concurrently with Run. It sends six named flight events, emits heartbeats during one pause, and finishes with a complete event. The handler that calls Run is the only connection writer. Transport backpressure remains synchronous. A send failure closes admission and requests producer cancellation.
Session : Flyology.HTTP.Server.SSE_Handlers.Session
(Capacity => 3,
Byte_Limit => Flyology.HTTP.Server.SSE_Handlers.Default_Session_Bytes,
Budget => null);
Flyology.HTTP.Server.SSE_Handlers.Run
(X, Session, Metrics'Access,
Idle_Quantum => 0.05, Heartbeat => 0.5);Give WebSocket writes one owner.
The WebSocket_Handlers package provides the bounded Session used by the lifecycle adapter. WebSocket lifecycle endpoints require an explicit browser-origin policy. Incoming messages are reassembled under the shared ingress budget and bounded maximum. Producers receive only the session mailbox; the owner drains it, serializes all frames, processes open, message, and close callbacks, and performs a clean close. The maintained example starts two scoped producers without exposing the exchange or connection to either task.
Compression is explicit. Selecting Permessage_Deflate negotiates RFC 7692 with no context takeover; the pure-Ada decoder charges decompressed bytes to the same ingress policy. The default declines extension offers, which remains appropriate for secret-bearing messages and applications that do not need compressed-client interoperability.
Application payloads are byte-native. Receive_WebSocket returns the WebSocket_Data_Kind and owned Flyology.Bytes.Unbounded_Bytes because the reassembled length is known only at runtime. The Send_WebSocket array overload accepts Ada.Streams.Stream_Element_Array directly, and To_Array produces that contiguous form. Binary_Frame identifies a binary message. The wrapper uses a definite vector internally and hides its index and capacity policy. Text helpers perform an explicitly named one-to-one byte-string mapping; they do not reinterpret arbitrary binary data as characters.
Data : Flyology.Bytes.Unbounded_Bytes;
Kind : HTTP.WebSocket_Data_Kind;
X.Receive_WebSocket (Kind, Data, Closed);
if not Closed and then Kind = HTTP.Binary_Frame then
X.Send_WebSocket (Kind, Flyology.Bytes.To_Array (Data));
end if;The WS.Lifecycle generic binds the open, message, and close callbacks. GNATdoc exposes its lifecycle operations on the parent unit page.
package Chat_Lifecycle is new
WS.Lifecycle (WS_Open, WS_Message, WS_Close);
Expected_Origin : constant String :=
"http://"
& Ada.Characters.Handling.To_Lower
(Request_Helpers.Authority (X));
Chat_Lifecycle.Run
(X, Session,
Origin_Policy => HTTP.Require_Exact_Origin,
Allowed_Origin => Expected_Origin,
Metric_Output => Metrics'Access);Run owns the WebSocket connection for the lifecycle instance. The example passes Require_Exact_Origin and derives the expected origin from the core-validated request authority. The same binary therefore works at either localhost or 127.0.0.1 without allowing arbitrary origins. When allowed origins come from configured URLs, use the normalization and role separation described in the URI, IRI, and URL guide. A TLS deployment would use https://; applications behind a trusted proxy must derive public authority and scheme only through an explicit trusted-hop policy.
The raw RFC 6455 API remains usable for applications needing a different lifecycle. Client masking, UTF-8, close codes, control frames, fragmentation, size limits, and terminal protocol failures remain core invariants.
Run request work in structured lightweight scopes.
Request_Tasks borrows the exchange cancellation token through an Ada access discriminant. It inherits the current absolute deadline into a bounded scope. Child operations receive input, a scope-owned token, and the deadline, but never the exchange or connection.
The adapter exposes a task-scope Operations instance. Its inherited Scope, Operation_Handle, Spawn, and Join declarations come from that instance. The HTTP adapter adds Configure. Join collects results and exception identities. Scope finalization cancels and joins unfinished children, so no detached task can retain borrowed request state.
Scope : Request_Operations.Scope (2, X.Cancellation);
User, Orders : Request_Operations.Operation_Handle;
Request_Work.Configure (Scope, X);
Request_Operations.Spawn (Scope, 20, User);
Request_Operations.Spawn (Scope, 1, Orders);
Request_Operations.Join (Scope);Configure the scope after all deadline-narrowing middleware. Parent cancellation is linked downward into a scope-owned token. Sibling failure and scope exit request only that child token, so unrelated request work is not cancelled upward.
Offload selected route work explicitly.
Flyology.HTTP.Server.Native_Routes adapts one routed handler to an application-owned Flyology.Native_Executors pool. Its generic formals bind App_Context, Input_Type, Result_Type, an executor Operations instance, and the borrowed Executor. The lightweight request task retains networking, parsing, middleware, routing, cancellation, and response I/O.
Prepare copies detached input from the borrowed exchange. A native worker calls Execute with only that value. After the original request task resumes, Render receives the detached result. The request task never changes its lightweight or native designation.
package CPU_Work is new Flyology.Native_Executors
(Work_Input, Work_Result, Execute);
Pool : aliased CPU_Work.Executor
(Workers => 4, Capacity => 68);
package CPU_Route is new Flyology.HTTP.Server.Native_Routes
(App_Context => Application_Context,
Input_Type => Work_Input,
Result_Type => Work_Result,
Operations => CPU_Work,
Executor => Pool'Access,
Prepare => Prepare,
Render => Render);
CPU_Work.Start (Pool); -- application setup
Routes.Get
("/cpu/{size}", CPU_Route.Handle'Access, Name => "cpu");
-- After the HTTP server has drained:
CPU_Work.Shutdown (Pool);Workers fixes native pthread parallelism. Capacity bounds queued, running, and completed-but-unclaimed operations; choose it as the intended worker-plus-queue limit. Submission never waits for capacity. A full or stopping route pool receives HTTP 503, and CPU_Work.Statistics (Pool) exposes accepted, rejected, successful, failed, abandoned, current, and peak operation counts.
The adapter propagates the exchange's absolute deadline and cancellation source into submission and cooperative waiting. Native exceptions retain their identity and pass through the router's normal exception mapper. A cancelled or unwound request abandons its single-use result; the executor requests worker cancellation and reclaims the bounded slot when execution returns. Input_Type and Result_Type must not contain an exchange, connection, or access value aliasing mutable request state. A worker must never read or write the live exchange or connection.
Keep short calculations inline because submission and cross-thread wakeup have a fixed cost. Use a routed native pool when selected CPU-heavy work must run in parallel without occupying event-loop pthreads. Prefer a fully native HTTP server when most request work is CPU-bound and its pthread count is acceptable.
Executor shutdown requests cancellation and joins every worker. Execute must therefore check its token and deadline where practical. A foreign function that cannot be interrupted may delay shutdown and needs process isolation when a bounded stop is required.
Size every retained resource.
The ingress budget bounds buffered bodies and retained/reassembled WebSocket messages. Outgoing SSE/WebSocket mailboxes have a 64 KiB per-message ceiling, a 256 KiB default per-session limit, and a shared 64 MiB default outbound budget charged until send or drop. These budgets do not cover kernel socket buffers, TLS-provider allocations, descriptors, per-connection parser/header state, task or fiber stacks, application buffers, rate-limit keys, metrics series, or native executor storage. Handler capacity, event-loop count, every bounded table, and upstream bulkheads must be chosen together.
- Set distinct slow-header, complete-request, route, and connection deadlines for the deployment, not only defaults.
- Keep header, body, trailer, cookie, WebSocket, and mailbox limits explicit.
- Budget TLS and kernel memory at maximum accepted connection count.
- Put slow upstream systems behind their own bounded scope or bulkhead.
- Drain on shutdown; do not detach work holding borrowed state.
Response compression currently exposes only a bounded provider boundary. Middleware remains deferred until it can preserve streaming, HEAD/no-body rules, partial-write safety, and sensitive-data policy.
Review the security boundary before deployment.
- Terminate TLS with certificate and key policy appropriate to the deployment; enable HSTS only for intentional HTTPS-only service.
- Keep the server's absolute slow-header and complete-request deadlines plus decoded limits enabled.
- Use shared ingress and outbound budgets plus bounded mailbox, metric, rate, and routing capacities.
- Do not trust inbound request IDs or proxy client headers without an explicit trusted-hop policy.
- Use exact CORS origins when credentials are involved and an explicit WebSocket origin policy.
- Never log authorization fields, cookies, bodies, or unbounded raw paths by default.
- Never let child tasks retain an exchange or write the connection.
- Close after partial-response failure; do not attempt replacement framing.
- Keep blocking foreign calls off event-loop pthreads.
- Run the behavioral, stress, documentation, proof, and platform checks for the changed boundary.
Benchmark reproducibly with oha.
The benchmark runner builds the release profile, prepares a fixed event-loop count, starts identical native and lightweight handler capacities, warms each lane, alternates trial order, and prints the exact oha command and version. Use at least three trials and compare medians and latency tails, not one run.
./showcases/run_http_benchmark.sh \
100000 500 500 18080 16 3 10000 release 20 500The final two arguments are lane cooldown and warm-up concurrency; this command warms and measures at 500 connections. Record CPU count, build profile, warm-up, request count, connection concurrency, handler capacity, event-loop count, success/error counts, throughput, average latency, and p50/p90/p99 tails. Use identical method, body, route, and limits for both lanes. The maintained campaign covers routed GET, a middleware-heavy route, a small buffered body, a streamed upload, and admission-control overhead. SSE and WebSocket require protocol-aware tools rather than treating an open stream as an ordinary completed HTTP request.
The corrected August 2026 comparison is the current published snapshot. It supersedes the preliminary Flyology, AWS, EWS, and ServletAda results after fixing runtime selection and CPU-placement defects, and adds tiered Rust fixtures with raw observations, resource costs, and explicit limits. The earlier snapshots remain linked from the correction as an archival record.
The separate routed-offload runner calibrates deterministic CPU work, verifies byte-identical inline and native results, rotates inline/pool-size/fully-native order, records executor admission and process resources, and adds a mixed CPU/control-route experiment:
HTTP_BENCH_LOOPS=8 \
HTTP_BENCH_SERVER_CPUSET="0-7" \
HTTP_BENCH_CLIENT_CPUSET="8-15" \
HTTP_HYBRID_TRIALS=7 \
HTTP_HYBRID_DURATION=30s \
./showcases/http-comparison/scripts/run-linux-docker-hybrid.shThe runner stops on a failed request or non-200 response rather than including it in throughput. Its timestamped directory retains calibration, host and toolchain metadata, unmodified oha JSON, process samples, executor counters, CSV, and Markdown summaries.