You're looking at the documentation for the "main" branch, click here to view the documentation for the latest stable release.

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:

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: