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.client()
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. See NEP 5: Suspension for why waiting is a suspension rather than a block.

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 client = http.client()

local function storeStatus(codes: {integer}, index: integer, url: string): nil
    local response = assert(client:send(new http.Request(
        url = assert(uri.newURI(url))
    )))
    codes[index] = response.status
    response:close()
end

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

print(codes[1], codes[2])
client:close()

A body calls a function holding the client rather than capturing the client itself, because a closure that captures an owner directly cannot be stored.

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 and response body are all owners, so each closes at its lexical boundary unless it is transferred. Closing a request also closes a reader body transferred into it.

Module contents

Types

TypeKindDescription
Bodytype
CapabilitiesrecordTransport capability flags describing observable HTTP guarantees.
Clientinterface
FileBodyrecordA request body read from a file as it is sent.
OptionsrecordWhat 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.
RequestrecordOne 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#

Bodytype#

type Body = ResponseBody

Capabilitiesrecord#

record Capabilities
    streamingResponse: boolean
    streamingRequest: boolean
    transportPolicy: boolean
    connectionPolicy: boolean
    protocolVersion: boolean
end

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.

Fields

streamingResponse#
streamingResponse: boolean

Response bytes become available before the whole response arrives.

streamingRequest#
streamingRequest: boolean

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

transportPolicy#
transportPolicy: boolean

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

connectionPolicy#
connectionPolicy: boolean

Redirect and connection limits can be enforced by the caller.

protocolVersion#
protocolVersion: boolean

The negotiated HTTP version is observable.

Clientinterface#

affine interface Client is nupp.Closeable
    readonly flush: function(exclusive self: Client): nil
    readonly pending: function(self: Client): integer
    readonly send: function(self: Client, borrows request: Request): (Response?, string?)
end

Methods

flush#
flush: function(exclusive self: Client): nil

Services outstanding client work while holding exclusive client access.

Arguments
NameTypeDescription
exclusive selfClient
Returns
TypeDescription
nil
pending#
pending: function(self: Client): integer

Returns the number of outstanding requests or transfers.

Arguments
NameTypeDescription
selfClient
Returns
TypeDescription
integer
send#
send: function(self: Client, borrows request: Request): (Response?, string?)

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

Arguments
NameTypeDescription
selfClient
borrows requestRequest
Returns
TypeDescription
Response?
string?

FileBodyrecord#

record FileBody
    path: string | path.Path
    contentType: string?
end

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

Built by nupp.io.http.file.

Fields

path#
path: string | path.Path

The file to send.

contentType#
contentType: string?

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

Optionsrecord#

record Options
    userAgent: string?
    headers: {string: string}?
    timeoutMs: integer?
    connectTimeoutMs: integer?
    stallTimeoutMs: integer?
    maxRedirects: integer?
    maxPendingRequests: integer?
    maxConnections: integer?
    maxConnectionsPerHost: integer?
    maxBytes: integer?
    compressed: boolean?
    insecureHosts: {string}?
    proxy: string?
    noProxy: string?
    proxyCredentials: string?
end

What a client applies to every request it sends.

Every field is optional, so http.client() with nothing is a client with the defaults below. A request may override the three limits it also names.

Fields

userAgent#
userAgent: string?

The user-agent header sent with every request.

headers#
headers: {string: string}?

Headers sent with every request, which a request's own headers replace by name.

timeoutMs#
timeoutMs: integer?

How long one request may take end to end, 30000 by default.

connectTimeoutMs#
connectTimeoutMs: integer?

How long establishing a connection may take, 10000 by default.

stallTimeoutMs#
stallTimeoutMs: integer?

How long a transfer may make no progress before it fails, 0 to allow any pause and the default.

maxRedirects#
maxRedirects: integer?

How many redirects one request may follow, 5 by default.

maxPendingRequests#
maxPendingRequests: integer?

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

maxConnections#
maxConnections: integer?

How many connections the pool may hold, 16 by default.

maxConnectionsPerHost#
maxConnectionsPerHost: integer?

How many connections the pool may hold to one host, 16 by default.

maxBytes#
maxBytes: integer?

How many response bytes one request may take, 0 for no bound and the default.

compressed#
compressed: boolean?

Whether to offer and decode compressed responses, true by default.

insecureHosts#
insecureHosts: {string}?

Hosts whose certificates are not verified, each an exact host name or IP literal. Naming any of them makes this client follow redirects itself, so that the exemption is applied per hop rather than to the whole chain.

proxy#
proxy: string?

The proxy to reach every host through. Omitted takes the environment's, and an empty string deliberately uses none.

noProxy#
noProxy: string?

Hosts to reach directly rather than through the proxy.

proxyCredentials#
proxyCredentials: string?

Credentials for the proxy, as user:password.

Readerinterface#

interface Reader is nupp.Closeable
    read: function(self: Reader, count: integer): (string?, string?)
end

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

Methods

read#
read: function(self: http.Reader, count: integer): (string?, string?)
Arguments
NameTypeDescription
selfhttp.Reader
countinteger
Returns
TypeDescription
string?
string?

ReaderBodyrecord#

record ReaderBody is nupp.Closeable
    reader: http.Reader
    length: integer?
    contentType: string?

    function close(takes self): nil end
end

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

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

Methods

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

Fields

reader#

Where the body's bytes come from.

length#
length: integer?

How many bytes the reader will produce, when that is known ahead of time.

contentType#
contentType: string?

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

Requestrecord#

record Request is nupp.Closeable
    url: uri.URI
    method: string?
    headers: {string: string}?
    body: http.RequestBody?
    timeoutMs: integer?
    stallTimeoutMs: integer?
    maxBytes: integer?
    function close(takes self): nil end
end

One 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
)

Methods

close#
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

Fields

url#
url: uri.URI

Where the request goes.

method#
method: string?

The method to use, GET when omitted.

headers#
headers: {string: string}?

Headers for this request, which replace the client's by name.

body#
body: http.RequestBody?

What to send as the body, or nothing.

timeoutMs#
timeoutMs: integer?

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

stallTimeoutMs#
stallTimeoutMs: integer?

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

maxBytes#
maxBytes: integer?

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

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#

affine interface Response is nupp.Closeable
    readonly status: integer
    readonly version: Version?
    readonly url: URI
    readonly body: Body
    readonly ok: function(self: Response): boolean
    readonly header: function(self: Response, name: string): string?
    readonly getAll: function(self: Response, name: string): {string}
    readonly headers: function(self: Response): {[string]: string}
end

Methods

ok#
ok: function(self: Response): boolean

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

Arguments
NameTypeDescription
selfResponse
Returns
TypeDescription
boolean
header#
header: function(self: Response, name: string): string?

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

Arguments
NameTypeDescription
selfResponse
namestring
Returns
TypeDescription
string?
getAll#
getAll: function(self: Response, name: string): {string}

Returns all values for a case-insensitive header name.

Arguments
NameTypeDescription
selfResponse
namestring
Returns
TypeDescription
{string}
headers#
headers: function(self: Response): {[string]: string}

Returns the response header mapping exposed by the host.

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

Fields

status#
status: integer

HTTP response status code.

version#
version: Version?

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

url#
url: URI

Canonical final response URI, including completed redirects.

body#
body: Body

Owned response reader whose lifetime is governed by the response.

Versiontype#

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

The protocol version a response arrived over.

Functions#

http.filefunction#

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

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

Arguments

NameTypeDescription
pathstring | Path

the file to send

contentTypestring?

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

Returns

TypeDescription
http.FileBody

the request body

Raises

  • 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 cannot replay one, so a request carrying this fails rather than following.

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

  • when length is negative, or contentType is not a string