In this page
http
HTTP/HTTPS client. Runs on libcurl, and verifies TLS against the same trust
chain the runtime resolved at startup, so --ca-path and --unsecure apply
here exactly as they do to the rest of GreyCat.
@library("http", "0.0.0");
One-shot requests
Http<T> is generic over what the response body deserializes into. Without a
generic parameter, or with String, the body is handed back as raw text; with
anything else an application/json body is parsed into T.
var body = Http<String> {}.get("https://example.com/", null);
var repo = Http<Repo> {}.post(
"https://api.example.com/repos",
Repo { name: "greycat" },
Map<String, String> { "authorization": "Bearer ${token}" },
);
Http<any> {}.getFile("https://example.com/data.csv", "data/local.csv", null);
send gives access to the status line and the response headers, and takes the
options a bare verb cannot express:
var res = Http<String> {}.send(HttpRequest {
method: HttpMethod::POST,
url: "https://api.example.com/things",
headers: Map<String, String> { "content-type": "application/json" },
body: "{\"name\":\"thing\"}",
timeout: 30s,
max_response_size: 1_000_000,
});
println("${res.status_code} ${res.headers.get("content-type")}");
max_response_size bounds the body in bytes and aborts the transfer past it,
which is what keeps a chunked or Content-Length-less response from growing
without bound. null or 0 means no limit.
The status decides where the body lands
T names the shape of a successful body. A failing request answers with
something else, so a non-2xx body is never decoded into T: it comes back as
text in error_msg, and content stays null.
var res = Http<Order> {}.send(request);
if (res.status_code >= 200 && res.status_code < 300) {
println(res.content?.total);
} else {
println("HTTP ${res.status_code} ${res.error_msg}");
}
Success is strictly 2xx. The two fields are never both set, and a failure
carrying no body at all leaves error_msg null too.
A JSON error envelope arrives as its JSON text, so reading a field out of it is the caller’s step and only happens when the detail is wanted:
var detail = Json {}.parse(res.error_msg ?? "{}");
The untyped forms are the exception: Http {}, Http<any> and Http<String>
name no schema for a failed body to contradict, so they carry it in content
as well, decoded exactly as a 2xx would be.
Streaming responses
chunked returns a reader that yields the body as it arrives instead of
buffering it whole, for text/event-stream, newline-delimited JSON and other
long-lived streams.
var reader = Http<String> {}.chunked(HttpRequest {
method: HttpMethod::GET,
url: "https://api.example.com/v1/stream",
timeout: 60s,
});
while (reader.can_read()) {
println(reader.read());
}
reader.close();
Framing follows the response Content-Type. An event stream is read as Server-
Sent Events, so one read returns the data of one event with the prefix
removed and comment lines skipped; anything else is read line by line. Either
way the transfer encoding of the wire is not observable. An event stream ends on
data: [DONE], which is reported as the end of the stream rather than handed
out.
On a reader, max_response_size caps the bytes held between two reads rather
than the whole body, so an endless stream is read at any length as long as the
caller keeps up.
HttpReader holds a live connection, so it can neither be persisted nor
resumed: read it to completion, or close it, from the task that opened it.
Unix sockets
unix_socket connects through a Unix domain socket instead of resolving the
URL’s authority, which is how a local daemon is reached: Docker, podman,
systemd, or an application behind nginx.
var res = Http<any> {}.send(HttpRequest {
method: HttpMethod::GET,
url: "http://localhost/v1.43/containers/json?all=true",
unix_socket: "/var/run/docker.sock",
});
The socket decides only where the connection goes. The URL still supplies the
request line, its scheme still decides whether TLS is negotiated over the
socket, and its host still becomes the Host header, which is why localhost
is the convention for an authority nothing routes on. A URL that is a bare path
gets http://localhost put in front of it, so one nothing reads can be left
out:
url: "/v1.43/containers/json",
chunked takes the field too, so a daemon’s event stream is read exactly like
any other stream.
The socket belongs to the connection rather than to the URL, so a redirect is followed over the same socket whatever host it names.
Bodies on disk
A body too large to hold in memory travels as a file. body_file streams the
request body straight off the disk, and response_file writes the response body
to one as it arrives, so neither passes through the heap. A 64 MB upload whose
echo comes back to a file costs the same resident memory as an empty request.
var res = Http<any> {}.send(HttpRequest {
method: HttpMethod::PUT,
url: "https://api.example.com/objects/report.parquet",
headers: Map<String, String> { "content-type": "application/vnd.apache.parquet" },
body_file: http::FileBody { path: "/data/report.parquet" },
timeout: 10min,
});
println("${res.status_code} ${res.headers.get("etag")}");
offset and size send a byte range rather than the whole file, which is what
a multipart part is. Both default to the whole file, and a range reaching past
the end is refused before the request opens rather than stalling the peer
mid-transfer.
body_file: http::FileBody { path: "/data/report.parquet", offset: 8_388_608, size: 8_388_608 },
response_file leaves content null, since a body is either returned or
written and never both. The status and headers arrive as they do for any other
response, which is what tells a downloaded file apart from a downloaded error
page:
var res = Http<any> {}.send(HttpRequest {
method: HttpMethod::GET,
url: "https://api.example.com/objects/report.parquet",
response_file: http::FileSink { path: "/data/downloaded.parquet" },
timeout: 10min,
});
if (res.status_code != 200) {
File::delete("/data/downloaded.parquet");
throw "download failed: HTTP ${res.status_code}";
}
Parent directories are created. append adds to the file instead of truncating
it. Setting body and body_file on the same request throws rather than
picking one.
FileBody and FileSink are always qualified, as http::FileBody and
http::FileSink.
For the two common cases there are shortcuts, both returning the response:
var up = Http<any> {}.put_file(url, "/data/report.parquet", headers);
var down = Http<any> {}.get_file(url, "/data/downloaded.parquet", null);
getFile predates all of this and stays as it is. It reports no status, so a
404 writes the error body to the target file and looks like a success; prefer
get_file, which returns one.