REXX Language implementation
The rxfnsg namespace supplies a bounded HTTP/1.1 client and a clear-text
buffered server for Level G programs. The client combines structured task
scopes, single-owner reusable connections, safe transferable headers and
endpoint-backed streaming. The server keeps sockets on one controller and
gives complete request values to ordinary task classes. HTTP is a Rexx library
built on concurrency; it is not a special RXAS instruction or channel provider
type.
The client is initial on develop and currently macOS-qualified. Linux
and Windows publication still requires the portable concurrency conformance
matrix.
User programs import rxfnsg and use these public Level G types:
| Purpose | Public types | Execution model |
|---|---|---|
| client | .httpclient, .httpheaders, .httppolicy, .httpbody, .httpresponse |
task calls over bounded single-owner connections |
| server | .httpserver, .httprequest, .httpservice, .httpresponse |
controller-owned sockets and sealed handler tasks |
| LLM providers | .ollama, .openai, .anthropic, .gemini |
the same Level G client and owner pool |
All three use _rxhttpcore, one private binary-oriented Level B framing,
parsing and codec backend. It is deliberately not a second public HTTP client:
Level B supplies the socket and binary machinery, while Level G owns HTTP
policy, resource management and typed values. The former public rxhttp
convenience client has been removed from the pre-release surface.
options levelg comments_dash
import rxfnsb
import concurrency
import rxfnsg
Create headers, a per-origin client and a task scope, then use ordinary method
syntax inside DO PARALLEL:
headers = .httpheaders.json()
call headers.add("Authorization", "Bearer " || token)
call headers.add("Idempotency-Key", request_id)
client = .httpclient.pooled("https://api.example", 4, 16, 1048576)
request_pool = .taskpool.local(2, 8)
request_scope = .taskscope.failfast(request_pool, 60000)
do parallel using request_scope
generation = client.post("/v1/chat/completions", generation_json, headers)
embedding = client.post("/v1/embeddings", embedding_json, headers)
end
if generation.succeeded() then say generation.body()
else say generation.transport_status() generation.error()
call request_pool.close()
call client.close()
post() is a task method. The task pool bounds Rexx task executions while the
HTTP client independently bounds reusable connections and queued admissions.
The complete loopback generation/embedding fixture is
ts_http_crexx_rag.crexx.
It verifies authorization and idempotency headers, request shapes, parallel
admission and bounded gzip/DEFLATE response decoding.
For a buffered GET use client.get(path, headers). For another HTTP method or
a binary request body use
client.request(verb, path, body_bytes, headers). All three forms return the
same .httpresponse; use body() for text and body_bytes() for exact bytes.
client = .httpclient.pooled(origin, connections, admission, maximum_response)
client = .httpclient.pooled(origin, connections, admission, maximum_response, policy)
| Argument | Contract |
|---|---|
origin |
http:// or https:// origin; IPv6 literals use brackets |
connections |
1..32 single-owner reusable connections |
admission |
1..256 queued fixed-size request descriptors |
maximum_response |
1..8388608 decoded bytes for buffered responses |
policy |
optional .httppolicy, default .httppolicy.safe() |
Request paths must begin with /, occupy at most 8192 UTF-8 bytes and contain
no CR or LF. Each configured connection has one long-lived .taskwork owner;
its socket integer never crosses an execution boundary.
Only the controller-owned client should perform final close(). That closes
admission, joins the connection owners and releases their pool. A client proxy
reconstructed for a task closes only its local endpoint view. Close the request
task pool separately, after every scope using it has closed.
from_channel() and to_channel() appear in the generated API because they
are the exact compiler transfer contract. Ordinary callers use pooled(),
post(), post_stream() and close().
The same rule applies to from_channel() / to_channel() on headers, policy,
body and response objects. httpresponse.new(), from_wire(),
from_stream_wire() and attach_stream() are library construction/framing
bridges. They remain documented because they are callable in the current
pre-release class surface, but normal request code should consume responses
returned by the client.
Construct a validated transferable snapshot with:
headers = .httpheaders.empty()
headers = .httpheaders.json()
headers = .httpheaders.media_type("application/cbor")
call headers.add("Authorization", "Bearer " || token)
call headers.add("Idempotency-Key", request_id)
Header names must be HTTP tokens. Values and Content-Type cannot contain CR
or LF. Host, Content-Length, Connection, Transfer-Encoding and
Accept-Encoding are library-owned. The absolute snapshot ceiling is 256
custom fields and 262144 bytes. The active policy may impose smaller limits.
Useful accessors are content_type(), count(), bytes() and
header(name). wire() is the validated CR/LF-terminated internal wire block.
to_channel() / from_channel() are transfer-contract members; a reconstructed
snapshot is fully validated again, so forged canonical data cannot bypass the
header rules.
policy = .httppolicy.safe()
call policy.set_timeouts(10000, 10000, 10000, 30000, 30000, 60000)
call policy.set_limits(64, 65536, 8388608)
call policy.set_replay(0, 0)
The timeout order is DNS, connect, TLS, request, response and total. The limit order is custom-header count, header bytes and buffered request bytes. Safe defaults are:
| Policy | Default | Valid range |
|---|---|---|
| DNS, connect, TLS | 10000 ms each | 1..600000 |
| request, response | 30000 ms each | 1..600000 |
| total observation | 60000 ms | 1..1800000 |
| custom header fields | 64 | 1..256 |
| header bytes | 65536 | 256..262144 |
| buffered request bytes | 8388608 | 1..8388608 |
| redirects, retries | 0 | 0..8 each |
Validation happens before mutation, so a rejected setter does not leave a partially changed policy. The present socket substrate performs name resolution, connect and TLS handshake as one owner-local operation; the client uses the sum of those three phase budgets for that atomic call. Request and response socket work use their individual budgets. The containing task scope should still express the caller’s real end-to-end deadline.
post(path, body, headers) accepts a buffered .string body and returns an
independent .httpresponse. It sends Accept-Encoding: gzip, deflate and
performs bounded decoding for:
The decoded result may not exceed maximum_response. Malformed streams,
unsupported content coding and decoded-size overflow are explicit transport
failures; the client does not return partial decoded data as success.
Inspect responses as follows:
if response.transport_status() <> 0 then do
say "transport/protocol failure:" response.transport_status() response.error()
end
else do
say "HTTP status:" response.status()
say "Content-Type:" response.header("Content-Type")
if response.succeeded() then say response.body()
end
succeeded() means transport status zero and HTTP status in 200..299.
attempts() and redirects() report network history.
ambiguous_outcome() remains true when bytes may have reached the peer without
a definitive response, even if an explicitly permitted retry later succeeds.
Automatic replay is disabled by default. set_replay() sets only bounded
maximums; it cannot make an unsafe POST idempotent.
Idempotency-Key;Callers must treat ambiguous_outcome() as delivery-history information. A
successful later response does not prove the earlier attempt was unseen.
Use .httpbody.fixed(content_length, capacity, response_capacity) when the
producer knows the exact length. Use
.httpbody.chunked(capacity, response_capacity) otherwise. Request capacity is
1..16777216; response capacity is 256..16777216.
The producer must run concurrently with post_stream() or a small endpoint
can correctly backpressure both sides. This fixed-length example is adapted
from the executable streaming fixture:
produce: task = .int
arg body = .httpbody
if body.write("hello " as .binary, 5000) <> 6 then return 1
if body.write("world" as .binary, 5000) <> 5 then return 2
call body.finish()
return 0
body = .httpbody.fixed(11, 8, 512)
scope = .taskscope.failfast(request_pool, 60000)
do parallel using scope
produced = produce(body)
response = client.post_stream("/upload", body, headers, 512)
end
if produced <> 0 then say "producer failed" produced
The response_capacity argument to post_stream() must exactly equal
body.response_capacity(). write(bytes, milliseconds) returns accepted bytes
or -1 after cancellation, deadline or failure. finish() half-closes the
request write side and publishes EOF. cancel(reason) cancels both paired
endpoints; close() releases both adapters.
A successful streaming response contains metadata plus a byte-endpoint reference. Drain it explicitly:
drain_response: procedure = .binary
arg response = .httpresponse
endpoint = .byteendpoint.from_encoded_reference(response.body_reference())
result = ''x as .binary
do forever
request = endpoint.start_read(4096, 5000)
outcome = request.wait(5000)
if outcome.available() = 0 | outcome.succeeded() = 0 then do
call endpoint.close()
return ''x as .binary
end
chunk = outcome.value().as_binary()
if binlength(chunk) > 0 then result = binconcat(result, chunk)
if outcome.details().field("eof").as_boolean() then leave
end
call endpoint.close()
return result
Call streaming() before choosing body() or body_reference(); using the
wrong accessor signals an error. Streaming responses currently require
identity content encoding. Buffered responses provide the bounded compression
support described above. The complete fixed/chunked request and streamed
response proof is
ts_http_streaming.crexx.
https:// origins use socketconnecttls(sock, host, port). Supported builds
verify the certificate chain and hostname using the configured platform
backend: Apple system TLS, OpenSSL on supported Unix-like systems, or SChannel
on Windows. A build without TLS support fails the request; it does not
downgrade to plaintext.
The live TLS fixture is environment-gated because it depends on external network and trust state. The ordinary loopback suite remains deterministic and covers pool ownership, policy, redirects, retries, ambiguity, streaming, compression and the cREXX-RAG request shapes.
An HTTP handler is a normal user class implementing .httpservice and
.taskwork. Write the typed handle(request, context) method for application
logic. The explicit run method is the bridge to the standard .taskwork ABI;
.httpservice.dispatch performs the checked request/response conversion.
options levelg
import rxfnsb
import concurrency
import rxfnsg
health_service: class implements .httpservice .taskwork
*: factory
return
handle: method = .httpresponse
arg request = .httprequest, context = .taskcontext
if request.verb() = "GET" & request.target() = "/health" then do
return .httpresponse.text(200, "ok")
end
return .httpresponse.text(404, "not found")
run: method = .channelvalue
arg encoded = .channelvalue, context = .taskcontext
service = self as .httpservice
return service.dispatch(encoded, context)
main: procedure = .int
server = .httpserver.local("127.0.0.1", 8080)
served = server.serve(task .health_service(), 1)
call server.close()
return 0
exit main()
This complete example deliberately serves one request so that normal cleanup
is visible. Omitting the second serve argument, or passing -1, keeps the
controller loop serving until its host is stopped. Detached/background server
lifecycle is not part of the current surface.
The task .health_service() expression is a compiler/linker-sealed factory
target. Each accepted request constructs task work according to that target;
the pool is a bounded executor, not a user-object factory. Factory arguments
may be added in the usual class syntax, subject to normal transferable task
argument rules.
.httprequest gives a handler these read-only views:
| Method | Result |
|---|---|
verb() |
validated uppercase request method |
target() |
origin-form target, including any query |
header(name) |
case-insensitive header lookup |
headers() |
raw validated header field block |
body() / body_bytes() |
buffered body as text / exact bytes |
remote() |
numeric peer endpoint text |
inbound(), routed(), from_channel(), to_channel(), routed_reply() and
reply_reference() appear in the generated pre-release API because they are
the exact controller/task transfer bridge. Normal handlers receive a request
from dispatch; they do not construct routed requests or use the private reply
capability themselves.
Return .httpresponse.text(status, text) for text or
.httpresponse.binary(status, bytes) for exact bytes. Response headers may be
supplied with the optional .httpheaders argument. The controller validates
the returned status, headers and body against server limits before any socket
write. A raised handler or an unusable response becomes a bounded HTTP 500; a
handler deadline becomes 504.
server = .httpserver.local(host, port, workers, admission, connections, maximum_header_bytes, maximum_headers, maximum_body_bytes, handler_timeout, request_timeout)
Defaults are 4 workers, 64 admitted tasks, 64 accepted connections,
65536 header bytes, 100 headers, 8388608 body bytes and 30000 ms for both
handler and request-read deadlines. Port 0 asks the operating system for an
ephemeral port; port() returns the selected value.
serve(target, maximum_requests) owns the controller loop and returns the
number completed by that call. Only complete requests cross to workers. The
controller retains the listener and every accepted socket, permits one
in-flight request per connection and uses bounded nonblocking scans with a
short idle wait. served(), scans() and idle_scans() are diagnostic
counters, not throughput guarantees. Call close() after a bounded serve
returns; it closes sockets and joins the private handler scope and pool.
The initial server accepts buffered clear-text HTTP/1.1 requests, with
HTTP/1.0 compatibility, canonical Content-Length, keep-alive and pipelining
boundaries. It rejects request Transfer-Encoding with 501. Server TLS,
HTTP/2, WebSockets, detached/background lifecycle, request streaming and
streaming handler responses are outside the present contract.
The executable service, parallel-client, binary-body, malformed-message,
deadline and failure examples are
ts_http_server.crexx
and
ts_http_server_failures.crexx.