nupp.io.http

nupp.io.http sends HTTP requests through the selected host implementation without blocking the caller's frame. Reach for it when a program needs a client that works the same in a CLI, a scheduler and a game host.

local http = nupp.io.http
local uri = nupp.io.uri

local client = http.newClient()
local response = assert(client:send(new http.Request(
    url = assert(uri.newURI("https://example.com/"))
)))
print(response.status)
response:close()
client:close()

Calls suspend through nupp.suspension: a CLI blocks on the provider's condvar, while a scheduler or frame host only drives the nonblocking source.

A response status is an answer, 4xx and 5xx included. Transport and body failures are returned as reasons instead. Response bodies and generic request readers are progressive and bounded, so a large body is read in pieces against a limit rather than assembled first.

Provider capabilities#

The selected provider shares the request options and body declarations on this page. http.capabilities() reports whether it supports streaming requests, streaming responses, transport policy, connection policy, and protocol-version reporting. Check those guarantees before depending on a host-specific facility.

The native provider supports all five. Browser Fetch accepts byte-string uploads and buffers the response before returning it. The browser controls connections, redirects, proxies, certificate policy, and decompression; options that try to configure those policies are rejected. Browser responses report version = nil because Fetch does not expose the negotiated protocol version.

Requests that go out together#

Requests share a client and go out together when something drives them together. A task scope runs each body in a coroutine of its own, and the one waiting on the network is what lets the next one send:

local http = nupp.io.http
local tasks = nupp.tasks
local uri = nupp.io.uri

local function storeStatus(client: alias(http.Client), codes: {integer}, index: integer, url: string): nil
    local status = client:with(function(borrows open: http.Client): integer
        local response = assert(open:send(new http.Request(url = assert(uri.newURI(url)))))
        local code = response.status
        response:close()

        return code
    end)
    codes[index] = assert(status)
end

local client = nupp.manage(http.newClient())
local codes: {integer} = {}
with scope = tasks.open() do
    scope:spawn(client:alias(), codes, 1, "https://example.com/", storeStatus)
    scope:spawn(client:alias(), codes, 2, "https://example.org/", storeStatus)
end

print(codes[1], codes[2])

A child cannot be handed an owner or a borrow, so the client goes into a managed cell and each child gets a copyable alias to it. with lends the client for one callback, and the cell closes the client when client leaves its scope.

Two bounds decide how much of that happens at once. maxConnectionsPerHost bounds what one host is asked to carry, and a scope's limit bounds how many requests are in flight at all.

Ownership#

A client, request, response, streaming request body, and response body are owners, so each closes at its lexical boundary unless it is transferred. A file body is an immutable path value with no cleanup obligation. Closing a request also closes a reader body transferred into it.

Submodules

ModuleDescription
nupp.io.http.spiShared interfaces for HTTP implementations.

Module contents

Types

TypeKindDescription
BodyinterfaceA forward-only byte source.
CapabilitiesrecordTransport capability flags describing observable HTTP guarantees.
Clientinterface
FileBodyrecordA request body read from a file as it is sent.
OptionstypeWhat a client applies to every request it sends.
ReaderinterfaceA portable byte source for an HTTP upload, with consuming teardown.
ReaderBodyrecordA request body streamed from a reader rather than held in memory.
RequestrecordA request, as http.Client:send takes it.
RequestBodytypeEverything a typed request body may own: immutable bytes, a reader stream, or a file path.
Responseinterface
VersiontypeThe protocol version a response arrived over.

Functions

FunctionKindDescription
filefunctionBuilds a request body read from a file as it is sent.
readerfunctionBuilds a request body that streams from a reader.

Types#

Bodyinterface#

interface Body is nupp.Closeable ...

A forward-only byte source.

An interface rather than the concrete things that satisfy it. A buffer's reader, an open file and an HTTP response body are all one of these, so code written against the contract works over any of them without knowing which it has.

Members

NameKindDescription
readmethodReads up to count bytes.
readSpanmethodReads directly into a checked writable span.
readIntomethodReads into a buffer.
transferTomethodWrites everything left to a writer.

readmethod#

read: function(exclusive self: Reader, count: integer): (string?, string?)

Reads up to count bytes.

An empty answer is the end.

Arguments
NameTypeDescription
exclusive selfBody

this reader

countinteger

the most bytes to read

Returns
TypeDescription
string?

the bytes, or nil when the reader is closed

string?

why it could not read, when unsuccessful

Raises
TypeCondition
string

when count is not a positive integer

readSpanmethod#

readSpan: function(exclusive self: Reader, exclusive destination: span.Writable<uint8>): (integer?, string?)

Reads directly into a checked writable span. A zero answer is the end.

Arguments
NameTypeDescription
exclusive selfBody

this reader

exclusive destinationspan.Writable<uint8>

the positive-sized range to fill

Returns
TypeDescription
integer?

how many bytes were read, or nil when the reader is closed

string?

why it could not read, when unsuccessful

readIntomethod#

readInto: function(
    exclusive self: Reader,
    exclusive destination: Buffer,
    offset: integer?,
    count: integer?
): (integer?, string?)

Reads into a buffer.

A zero answer is the end.

Arguments
NameTypeDescription
exclusive selfBody

this reader

exclusive destinationBuffer

the buffer to write into

offsetinteger?

where in the destination to start, or the beginning

countinteger?

the most bytes to read

Returns
TypeDescription
integer?

how many bytes were read, or nil when the reader is closed

string?

why it could not read, when unsuccessful

transferTomethod#

transferTo: function(exclusive self: Reader, exclusive destination: Writer): (integer?, string?)

Writes everything left to a writer.

Arguments
NameTypeDescription
exclusive selfBody

this reader

exclusive destinationWriter

the writer to fill

Returns
TypeDescription
integer?

how many bytes moved, or nil on failure

string?

why it could not, when unsuccessful

Capabilitiesrecord#

record Capabilities ...

Transport capability flags describing observable HTTP guarantees.

A true flag promises the corresponding behavior; callers must not infer support from a provider name or the presence of unrelated methods.

Members

NameKindDescription
streamingResponsefieldResponse bytes become available before the whole response arrives.
streamingRequestfieldRequest bodies may be read incrementally from a reader or file.
transportPolicyfieldProxy and per-host certificate policy can be configured by the caller.
connectionPolicyfieldRedirect and connection limits can be enforced by the caller.
protocolVersionfieldThe negotiated HTTP version is observable.

streamingResponsefield#

streamingResponse: boolean

Response bytes become available before the whole response arrives.

streamingRequestfield#

streamingRequest: boolean

Request bodies may be read incrementally from a reader or file.

transportPolicyfield#

transportPolicy: boolean

Proxy and per-host certificate policy can be configured by the caller.

connectionPolicyfield#

connectionPolicy: boolean

Redirect and connection limits can be enforced by the caller.

protocolVersionfield#

protocolVersion: boolean

The negotiated HTTP version is observable.

Clientinterface#

interface Client is nupp.Closeable ...

Members

NameKindDescription
pumpmethodServices outstanding client work, waiting up to the given milliseconds for some of it to move, while holding...
pendingmethodReturns the number of outstanding requests or transfers.
sendmethodBorrows the Request while sending and returns an owned Response or an error.

pumpmethod#

pump: function(exclusive self: Client, timeoutMs: integer?): nil
@readonly

Services outstanding client work, waiting up to the given milliseconds for some of it to move, while holding exclusive client access.

Arguments
NameTypeDescription
exclusive selfClient
timeoutMsinteger?
Returns
TypeDescription
nil

pendingmethod#

pending: function(self: Client): integer
@readonly

Returns the number of outstanding requests or transfers.

Arguments
NameTypeDescription
selfClient
Returns
TypeDescription
integer

sendmethod#

send: function(self: Client, borrows request: Request): (Response?, Error?)
@readonly

Borrows the Request while sending and returns an owned Response or an error.

Arguments
NameTypeDescription
selfClient
borrows requestRequest
Returns
TypeDescription
Response?
Error?

FileBodyrecord#

record FileBody ...

A request body read from a file as it is sent.

Built by nupp.io.http.file. The path must name a regular file: a device, FIFO or directory has no length to send, and the native transport refuses it.

Members

NameKindDescription
pathfieldThe file to send, named as anything nupp.io.files accepts: text, a nupp.io.path.Path, or an application path.
contentTypefieldThe content-type to send, unless the request names one itself.

pathfield#

path: Path

The file to send, named as anything nupp.io.files accepts: text, a nupp.io.path.Path, or an application path.

contentTypefield#

contentType: string?

The content-type to send, unless the request names one itself.

Optionstype#

type Options = {
    --- The `user-agent` header sent with every request.
    userAgent: string?,

    --- Headers sent with every request, which a request's own headers replace by
    --- name.
    headers: {[string]: string}?,

    --- How long a request may take end to end, 30000 by default.
    timeoutMs: integer?,

    --- How long establishing a connection may take, 10000 by default.
    connectTimeoutMs: integer?,

    --- How long a response body may make no progress before the transfer fails,
    --- 0 to allow any pause and the default. The wait for the response headers
    --- and a streamed upload are bounded by `timeoutMs` alone.
    stallTimeoutMs: integer?,

    --- How many redirects a request may follow, 5 by default.
    maxRedirects: integer?,

    --- How many requests may be in flight at once, 256 by default. A request past
    --- the bound waits for admission rather than failing.
    maxPendingRequests: integer?,

    --- How many connections the pool may hold, 16 by default.
    maxConnections: integer?,

    --- How many connections the pool may hold to one host, 16 by default.
    maxConnectionsPerHost: integer?,

    --- How many response bytes a request may take, 0 for no bound and the
    --- default.
    maxBytes: integer?,

    --- Whether to offer and decode compressed responses, true by default.
    compressed: boolean?,

    --- Hosts whose certificates are not verified, each an exact host name or IP
    --- literal. Redirects are followed one hop at a time, so the exemption is
    --- applied per hop rather than to the whole chain.
    insecureHosts: {string}?,

    --- The proxy to reach every host through. Omitted takes the environment's,
    --- and an empty string deliberately uses none. Under CGI, where
    --- `REQUEST_METHOD` is set, the environment names no proxy: a request's own
    --- `Proxy` header arrives there as `HTTP_PROXY`.
    proxy: string?,

    --- Hosts to reach directly rather than through the proxy.
    noProxy: string?,

    --- Credentials for the proxy, as `user:password`.
    proxyCredentials: string?
}

What a client applies to every request it sends.

A plain table, like every other option bag in the io facades. Every field is optional, so http.newClient() with nothing is a client with the defaults below. A request may override the three limits it also names.

Readerinterface#

interface Reader is nupp.Closeable ...

A portable byte source for an HTTP upload, with consuming teardown.

Members

NameKindDescription
readmethod

readmethod#

read: function(exclusive self: Reader, count: integer): (string?, string?)
Arguments
NameTypeDescription
exclusive selfReader
countinteger
Returns
TypeDescription
string?
string?

ReaderBodyrecord#

record ReaderBody is nupp.Closeable ...

A request body streamed from a reader rather than held in memory.

Built by nupp.io.http.reader. A redirect that preserves the request body cannot replay one, so the request fails rather than following that redirect.

Members

NameKindDescription
readerfieldWhere the body's bytes come from.
lengthfieldHow many bytes the reader will produce, when that is known ahead of time.
contentTypefieldThe content-type to send, unless the request names one itself.
closemethod

readerfield#

Where the body's bytes come from.

lengthfield#

length: integer?

How many bytes the reader will produce, when that is known ahead of time. The native transport fails a request whose reader produces more or fewer.

contentTypefield#

contentType: string?

The content-type to send, unless the request names one itself.

closemethod#

close: function close(takes self): nil
Arguments
NameTypeDescription
takes selfany
Returns
TypeDescription
nil

Requestrecord#

record Request is nupp.Closeable ...

A request, as http.Client:send takes it.

Only url is required, and it must be http or https. The three limits here override the client's for this request alone. A request is an affine owner; closing it closes a streaming reader body, and otherwise has no external work.

local request = new http.Request(
    url = assert(uri.newURI("https://example.com/upload")),
    method = "POST",
    headers = {["content-type"] = "application/json"},
    body = document
)

Members

NameKindDescription
urlfieldWhere the request goes.
methodfieldThe method to use, GET when omitted.
headersfieldHeaders for this request, which replace the client's by name.
bodyfieldWhat to send as the body, or nothing.
timeoutMsfieldHow long this request may take end to end, or the client's limit.
stallTimeoutMsfieldHow long this request's response body may make no progress, or the client's limit.
maxBytesfieldHow many response bytes this request may take, or the client's limit.
closemethodCloses the request and any streaming reader it owns.

urlfield#

url: uri.URI

Where the request goes.

methodfield#

method: string?

The method to use, GET when omitted.

headersfield#

headers: {[string]: string}?

Headers for this request, which replace the client's by name. A content-length must match the body, where no body counts as zero, and transfer-encoding is the transport's to choose; the native transport refuses a request that breaks either rule.

bodyfield#

body: http.RequestBody?

What to send as the body, or nothing.

timeoutMsfield#

timeoutMs: integer?

How long this request may take end to end, or the client's limit.

stallTimeoutMsfield#

stallTimeoutMs: integer?

How long this request's response body may make no progress, or the client's limit.

maxBytesfield#

maxBytes: integer?

How many response bytes this request may take, or the client's limit.

closemethod#

close: function close(takes self): nil

Closes the request and any streaming reader it owns.

Arguments
NameTypeDescription
takes selfany

this request, spent by the call

Returns
TypeDescription
nil

RequestBodytype#

type RequestBody = string | http.ReaderBody | http.FileBody

Everything a typed request body may own: immutable bytes, a reader stream, or a file path. Convert a Buffer or ByteView with getString() so the request owns an immutable snapshot rather than borrowing mutable storage.

Responseinterface#

interface Response is nupp.Closeable ...

Members

NameKindDescription
statusfieldHTTP response status code.
versionfieldObserved protocol version, absent when the host cannot expose it.
urlfieldCanonical final response URI, including completed redirects.
bodyfieldOwned response reader whose lifetime is governed by the response.
okmethodReports whether the status is in the successful 2xx range.
headermethodReturns a header value by case-insensitive name, or nil if absent.
headerValuesmethodReturns all values for a case-insensitive header name.
headersmethodReturns a fresh response header mapping keyed by lowercase name.

statusfield#

status: integer
@readonly

HTTP response status code.

versionfield#

version: Version?
@readonly

Observed protocol version, absent when the host cannot expose it.

urlfield#

url: URI
@readonly

Canonical final response URI, including completed redirects.

bodyfield#

body: Body
@readonly

Owned response reader whose lifetime is governed by the response.

okmethod#

ok: function(self: Response): boolean
@readonly

Reports whether the status is in the successful 2xx range.

Arguments
NameTypeDescription
selfResponse
Returns
TypeDescription
boolean

headermethod#

header: function(self: Response, name: string): string?
@readonly

Returns a header value by case-insensitive name, or nil if absent.

Arguments
NameTypeDescription
selfResponse
namestring
Returns
TypeDescription
string?

headerValuesmethod#

headerValues: function(self: Response, name: string): {string}
@readonly

Returns all values for a case-insensitive header name.

Arguments
NameTypeDescription
selfResponse
namestring
Returns
TypeDescription
{string}

headersmethod#

headers: function(self: Response): {[string]: string}
@readonly

Returns a fresh response header mapping keyed by lowercase name. Repeated values are joined with , except set-cookie, whose first value is returned; use headerValues to preserve every value.

Arguments
NameTypeDescription
selfResponse
Returns
TypeDescription
{[string]: string}

Versiontype#

type Version = "1.0" | "1.1" | "2"

The protocol version a response arrived over.

Functions#

http.filefunction#

function http.file(path: Path, contentType: string?): http.FileBody

Builds a request body read from a file as it is sent.

The path must name a regular file when the request is sent. A device, FIFO or directory has no length to send, and the native transport refuses it.

Arguments

NameTypeDescription
pathPath

the file to send, as anything nupp.io.files accepts

contentTypestring?

the content-type to send, unless the request names one

Returns

TypeDescription
http.FileBody

the request body

Raises

TypeCondition
string

when path is neither a string nor a path, or contentType is not a string

http.readerfunction#

function http.reader(takes reader: messages.Reader, length: integer?, contentType: string?): http.ReaderBody

Builds a request body that streams from a reader.

The bytes are sent as the reader produces them, so a large upload never has to be held in memory. A redirect that preserves the request body cannot replay one, so the request fails rather than following that redirect.

A reader given a length must produce exactly that many bytes; the native transport fails the request when it produces more or fewer. When the response headers arrive before the reader is finished, the upload stops there and the peer never sees the shortened body as complete.

local request = new http.Request(
    url = endpoint,
    method = "POST",
    body = http.reader(source, size, "application/octet-stream")
)

Arguments

NameTypeDescription
takes readermessages.Reader

where the body's bytes come from

lengthinteger?

how many bytes the reader will produce, when that is known

contentTypestring?

the content-type to send, unless the request names one

Returns

TypeDescription
http.ReaderBody

the request body

Raises

TypeCondition
string

when reader does not implement the reader contract, length is negative, or contentType is not a string