HTTP clients
Besides providing support for creating HTTP servers, the standard
library also provides a module for sending HTTP 1.1 requests:
std.net.http.client
. This module provides the type
Client
, which is an HTTP 1.1 client that supports HTTP,
HTTPS and Unix domain socket requests.
Getting started
To send a request, we need two things:
- An instance of
Client
to send the request - An instance of
Uri
that specifies where to send the request to
For example, here's how to send a GET request to http://httpbun.org/get and read its response:
import std.net.http.client (Client)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('http://httpbun.org/get').or_panic
let res = client.get(uri).send.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
Client.new
returns a new HTTP client.
Uri.parse
parses a URI from a String
and
returns a Result[Uri, Error]
. The method
Client.get
returns a
Request
for building a GET request, and
Request.send
sends the request, without
including a body. The return value is an instance of Response
,
and the field Response.body
contains the response body,
which implements the Read
trait.
The output of this example is as follows:
{
"method": "GET",
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Host": "httpbun.org",
"User-Agent": "inko/0.18.1 (https://inko-lang.org)",
"Via": "1.1 Caddy"
},
"origin": "86.93.96.67",
"url": "http://httpbun.org/get",
"form": {},
"data": "",
"json": null,
"files": {}
}
Methods
The Client
type defines the following methods for
generating HTTP requests along with the HTTP request method used:
Request.get
: GETRequest.post
: POSTRequest.put
: PUTRequest.delete
: DELETERequest.head
: HEADRequest.request
: for all other request methods (e.g. TRACE)
Headers
Extra request headers are added using the
Request.header
method. This method takes
ownership of its receiver:
import std.net.http (Header)
import std.net.http.client (Client)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('http://httpbun.org/get').or_panic
let res = client
.get(uri)
.header(Header.user_agent, 'custom agent')
.header(Header.new('custom-header'), 'custom-value')
.send
.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
The output is as follows:
{
"method": "GET",
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Custom-Header": "custom-value",
"Host": "httpbun.org",
"User-Agent": "custom agent",
"Via": "1.1 Caddy"
},
"origin": "86.93.96.67",
"url": "http://httpbun.org/get",
"form": {},
"data": "",
"json": null,
"files": {}
}
Query strings
The method Request.query
is used to add
query string parameters to the request:
import std.net.http.client (Client)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('http://httpbun.org/get').or_panic
let res = client
.get(uri)
.query('name', 'Alice')
.query('age', '42')
.send
.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
The output is as follows:
{
"method": "GET",
"args": {
"age": "42",
"name": "Alice"
},
"headers": {
"Accept-Encoding": "gzip",
"Host": "httpbun.org",
"User-Agent": "inko/0.18.1 (https://inko-lang.org)",
"Via": "1.1 Caddy"
},
"origin": "86.93.96.67",
"url": "http://httpbun.org/get?name=Alice&age=42",
"form": {},
"data": "",
"json": null,
"files": {}
}
Bodies
To include a body in the request, use
Request.body
:
import std.net.http.client (Client)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('http://httpbun.org/post').or_panic
let res = client.post(uri).body('request body').or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
The output is as follows:
{
"method": "POST",
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "12",
"Host": "httpbun.org",
"User-Agent": "inko/0.18.1 (https://inko-lang.org)",
"Via": "1.1 Caddy"
},
"origin": "86.93.96.67",
"url": "http://httpbun.org/post",
"form": {},
"data": "request body",
"json": null,
"files": {}
}
HTML forms
Generating and sending HTML form data is done using
Request.url_encoded_form
or
Request.multipart_form
, depending on the
encoding type that's necessary. For example, a URL encoded form is built and
sent as follows:
import std.net.http.client (Client)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('http://httpbun.org/post').or_panic
let form = client.post(uri).url_encoded_form
form.add('name', 'Alice')
form.add('age', '42')
let res = form.send.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
The output is as follows:
{
"method": "POST",
"args": {},
"headers": {
"Accept-Encoding": "gzip",
"Content-Length": "17",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "httpbun.org",
"User-Agent": "inko/0.18.1 (https://inko-lang.org)",
"Via": "1.1 Caddy"
},
"origin": "86.93.96.67",
"url": "http://httpbun.org/post",
"form": {
"age": "42",
"name": "Alice"
},
"data": "",
"json": null,
"files": {}
}
Keep-alive connections
After establishing a connection, the connection is kept alive until the server
disconnects the connection (e.g. due to it being idle for too long). Connections
are scoped per URI scheme, host and port. This means that sending a request to
http://foo
and https://foo
results in two connections.
HTTPS requests
A Client
supports both HTTP and HTTPS requests. The TLS configuration used for
performing HTTPS requests is initialized as needed and stored in the field
Client.tls
, unless the field already contains a
TLS configuration object.
To specify a custom TLS configuration, create an instance of
ClientConfig
and store it in the
Client.tls
field as an Option.Some
:
import std.net.http.client (Client)
import std.net.tls (ClientConfig)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
client.tls = Option.Some(ClientConfig.new.get)
let uri = Uri.parse('https://httpbun.org/get').or_panic
let res = client.get(uri).send.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
In this example we use ClientConfig.new
to create a configuration object that
uses the system's certificates. While this is the same as a Client
does
automatically (if needed), it illustrates how one may specify a custom TLS
configuration.
In other words: if you just want to use the system's certificates you don't
need to assign the tls
field yourself.
Following redirects
If the request method is GET, HEAD, OPTIONS or TRACE, redirects are followed
automatically. Unsafe redirects (e.g. a redirect from an HTTPS to HTTP URL)
result in a Error.InsecureRedirect
error.
The maximum number of redirects followed is defined by the
Client.max_redirects
field, and defaults to a
maximum of 5 redirects. Upon encountering too many redirects, a
Error.TooManyRedirects
error is returned.
When sending a multipart/form-data
request generated using
Request.multipart_form
, redirects are not
followed regardless of the request method, as the streaming nature of multipart
forms makes it impossible to do so reliably in a generic way. For example, if
such a form field's value is populated from a file, the file's cursor would need
to rewind back to the start but a Client
has no way of doing so.
Cookies
To send cookies along with a request, create a Cookie
instance and use it to populate the Cookie
header accordingly:
import std.net.http (Header)
import std.net.http.client (Client)
import std.net.http.cookie (Cookie)
import std.stdio (Stdout)
import std.uri (Uri)
type async Main {
fn async main {
let client = Client.new
let uri = Uri.parse('https://httpbun.org/cookies').or_panic
let name = Cookie.new('name', 'Alice')
let age = Cookie.new('age', '42')
let res = client
.get(uri)
.header(Header.cookie, '${name.to_request}; ${age.to_request}')
.send
.or_panic
let buf = ByteArray.new
let _ = res.body.read_all(buf).or_panic
Stdout.new.print(buf)
}
}
The output is as follows:
{
"cookies": {
"age": "42",
"name": "Alice"
}
}
Support for client cookie jars is not yet provided. Refer to this issue for more details.
More information
For more information, refer to the documentation of the following:
std.net.http
: contains various HTTP building blocks, such as theHeader
typestd.net.http.cookie
: handling of cookies for both clients and servers- The various fields of the
Client
type, used to configure the client such as the connection timeout std.net.multipart
: parsing and generating ofmultipart/form-data
streams (used byRequest.multipart_form
)