# `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.
```nupp
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`](../../../../learn/runtime/concurrency/suspension/index.html): a CLI
blocks on the provider's condvar, while a scheduler or frame host only drives the
nonblocking source. See [NEP 5: Suspension](../../../../reference/neps/0005-suspension/index.html) 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](../../../../learn/runtime/concurrency/task-scopes/index.html) runs each body in a
coroutine of its own, and the one waiting on the network is what lets the next
one send:
```nupp
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.
::: seealso
- [ownership.md](../../../../learn/runtime/ownership/borrowing/index.html) for the complete contract
reference
- [Suspension](../../../../learn/runtime/concurrency/suspension/index.html) for what a wait does under a
handler
- `nupp.io.uri` for the URI values a request is addressed with
:::
## Types
### `Body` _type_
```nupp
type Body = ResponseBody
```
### `Capabilities` _record_
```nupp
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`
```nupp
streamingResponse: boolean
```
Response bytes become available before the whole response arrives.
##### `streamingRequest`
```nupp
streamingRequest: boolean
```
Request bodies may be read incrementally from a reader or file.
##### `transportPolicy`
```nupp
transportPolicy: boolean
```
Proxy and per-host certificate policy can be configured by the caller.
##### `connectionPolicy`
```nupp
connectionPolicy: boolean
```
Redirect and connection limits can be enforced by the caller.
##### `protocolVersion`
```nupp
protocolVersion: boolean
```
The negotiated HTTP version is observable.
### `Client` _interface_
```nupp
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`
```nupp
flush: function(exclusive self: Client): nil
```
Services outstanding client work while holding exclusive client access.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Client` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `pending`
```nupp
pending: function(self: Client): integer
```
Returns the number of outstanding requests or transfers.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Client` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `send`
```nupp
send: function(self: Client, borrows request: Request): (Response?, string?)
```
Borrows the Request while sending and returns an owned Response or an error.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Client` | |
| `borrows request` | `Request` | |
###### Returns
| Type | Description |
| --- | --- |
| `Response?` | |
| `string?` | |
### `FileBody` _record_
```nupp
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`](#nupp.io.http.file).
#### Fields
##### `path`
```nupp
path: string | path.Path
```
The file to send.
##### `contentType`
```nupp
contentType: string?
```
The `content-type` to send, unless the request names one itself.
### `Options` _record_
```nupp
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`
```nupp
userAgent: string?
```
The `user-agent` header sent with every request.
##### `headers`
```nupp
headers: {string: string}?
```
Headers sent with every request, which a request's own headers replace by
name.
##### `timeoutMs`
```nupp
timeoutMs: integer?
```
How long one request may take end to end, 30000 by default.
##### `connectTimeoutMs`
```nupp
connectTimeoutMs: integer?
```
How long establishing a connection may take, 10000 by default.
##### `stallTimeoutMs`
```nupp
stallTimeoutMs: integer?
```
How long a transfer may make no progress before it fails, 0 to allow any
pause and the default.
##### `maxRedirects`
```nupp
maxRedirects: integer?
```
How many redirects one request may follow, 5 by default.
##### `maxPendingRequests`
```nupp
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`
```nupp
maxConnections: integer?
```
How many connections the pool may hold, 16 by default.
##### `maxConnectionsPerHost`
```nupp
maxConnectionsPerHost: integer?
```
How many connections the pool may hold to one host, 16 by default.
##### `maxBytes`
```nupp
maxBytes: integer?
```
How many response bytes one request may take, 0 for no bound and the
default.
##### `compressed`
```nupp
compressed: boolean?
```
Whether to offer and decode compressed responses, true by default.
##### `insecureHosts`
```nupp
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`
```nupp
proxy: string?
```
The proxy to reach every host through. Omitted takes the environment's,
and an empty string deliberately uses none.
##### `noProxy`
```nupp
noProxy: string?
```
Hosts to reach directly rather than through the proxy.
##### `proxyCredentials`
```nupp
proxyCredentials: string?
```
Credentials for the proxy, as `user:password`.
### `Reader` _interface_
```nupp
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`
```nupp
read: function(self: http.Reader, count: integer): (string?, string?)
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `http.Reader` | |
| `count` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `string?` | |
| `string?` | |
### `ReaderBody` _record_
```nupp
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`](#nupp.io.http.reader). A redirect cannot replay one, so a request
carrying a reader body fails rather than following.
#### Methods
##### `close`
```nupp
close: function close(takes self): nil
```
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `any` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
##### `reader`
```nupp
reader: http.Reader
```
Where the body's bytes come from.
##### `length`
```nupp
length: integer?
```
How many bytes the reader will produce, when that is known ahead of time.
##### `contentType`
```nupp
contentType: string?
```
The `content-type` to send, unless the request names one itself.
### `Request` _record_
```nupp
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.
```nupp
local request = new http.Request(
url = assert(uri.newURI("https://example.com/upload")),
method = "POST",
headers = {["content-type"] = "application/json"},
body = document
)
```
#### Methods
##### `close`
```nupp
close: function close(takes self): nil
```
Closes the request and any streaming reader it owns.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes self` | `any` | this request, spent by the call |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
##### `url`
```nupp
url: uri.URI
```
Where the request goes.
##### `method`
```nupp
method: string?
```
The method to use, `GET` when omitted.
##### `headers`
```nupp
headers: {string: string}?
```
Headers for this request, which replace the client's by name.
##### `body`
```nupp
body: http.RequestBody?
```
What to send as the body, or nothing.
##### `timeoutMs`
```nupp
timeoutMs: integer?
```
How long this request may take end to end, or the client's limit.
##### `stallTimeoutMs`
```nupp
stallTimeoutMs: integer?
```
How long this request may make no progress, or the client's limit.
##### `maxBytes`
```nupp
maxBytes: integer?
```
How many response bytes this request may take, or the client's limit.
### `RequestBody` _type_
```nupp
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.
### `Response` _interface_
```nupp
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`
```nupp
ok: function(self: Response): boolean
```
Reports whether the status is in the successful 2xx range.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Response` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `header`
```nupp
header: function(self: Response, name: string): string?
```
Returns one header value by case-insensitive name, or nil if absent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Response` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `string?` | |
##### `getAll`
```nupp
getAll: function(self: Response, name: string): {string}
```
Returns all values for a case-insensitive header name.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Response` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `{string}` | |
##### `headers`
```nupp
headers: function(self: Response): {[string]: string}
```
Returns the response header mapping exposed by the host.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `self` | `Response` | |
###### Returns
| Type | Description |
| --- | --- |
| `{\[string\]: string}` | |
#### Fields
##### `status`
```nupp
status: integer
```
HTTP response status code.
##### `version`
```nupp
version: Version?
```
Observed protocol version, absent when the host cannot expose it.
##### `url`
```nupp
url: URI
```
Canonical final response URI, including completed redirects.
##### `body`
```nupp
body: Body
```
Owned response reader whose lifetime is governed by the response.
### `Version` _type_
```nupp
type Version = "1.0" | "1.1" | "2"
```
The protocol version a response arrived over.
## Functions
### `http.file` _function_
```nupp
function http.file(path: string | Path, contentType: string?): http.FileBody
```
Builds a request body read from a file as it is sent.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string | Path` | the file to send |
| `contentType` | `string?` | the `content-type` to send, unless the request names one |
#### Returns
| Type | Description |
| --- | --- |
| `http.FileBody` | the request body |
#### Raises
- when path is neither a string nor a path, or contentType is not a string
### `http.reader` _function_
```nupp
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.
```nupp
local request = new http.Request(
url = endpoint,
method = "POST",
body = http.reader(source, size, "application/octet-stream")
)
```
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `takes reader` | `messages.Reader` | where the body's bytes come from |
| `length` | `integer?` | how many bytes the reader will produce, when that is known |
| `contentType` | `string?` | the `content-type` to send, unless the request names one |
#### Returns
| Type | Description |
| --- | --- |
| `http.ReaderBody` | the request body |
#### Raises
- when length is negative, or contentType is not a string