Packages / eta-http

eta-http v0.1.0 Native sidecar eta-native-v1 MIT

eta-http

HTTP client support for Eta, backed by a native libcurl sidecar.

eta-http provides the (import net.http) module for making HTTP requests, working with sessions, handling JSON, setting headers/options, streaming downloads, and using URL helpers.

(import net.http)

(define resp (get "https://example.com"))
(status resp)       ; => 200
(body-string resp)  ; response body as text

Module

(import net.http)

The public Eta API lives in src/net/http.eta. The low-level native primitives are registered by the sidecar as http/* primitives, but normal user code should prefer the net.http wrappers documented here.

Quick start

(import net.http)

(let ((resp (get "https://example.com" '((timeout-ms . 10000)))))
  (raise-for-status! resp)
  (display (status resp))
  (newline)
  (display (body-string resp))
  (newline))

One-shot helpers create and close a temporary session automatically unless you pass an existing session.

One-shot requests

FunctionDescription
(get url [opts-or-session] [session])Execute a GET request and return a response handle.
(post url body [opts-or-session] [session])Execute a POST request with a UTF-8 string body.
(post-json url value [opts-or-session] [session])JSON-encode value, POST it, and set Content-Type: application/json.
(get-json url [opts-or-session] [session])GET a URL and decode the response body as JSON.
(download url path [opts])Stream the response body directly to path; returns a response handle.

Examples:

(import net.http)

(define resp (get "https://example.com/api/status"
                  '((timeout-ms . 5000)
                    (user-agent . "eta-client/1.0"))))

(status resp)
(headers resp)
(body-string resp)
(import net.http)

(define created
  (post "https://example.com/api/items"
        "{\"name\":\"eta\"}"
        '((timeout-ms . 10000))))

(raise-for-status! created)

JSON

net.http imports std.json internally and exposes JSON convenience helpers:

FunctionDescription
(post-json url value ...)Encodes value with json:write-string and sends it as UTF-8 JSON.
(get-json url ...)Performs GET and returns (body-json response).
(body-json response)Decodes response text with json:read-string.

Example POST/GET JSON flow:

(import net.http)

(define payload
  (hash-map
    "name" "eta"
    "count" 2
    "enabled" #t))

(define resp
  (post-json "https://example.com/api/items"
             payload
             '((timeout-ms . 10000))))

(raise-for-status! resp)

(define decoded (body-json resp))

Eta JSON values follow std.json conventions:

If you need exact integer preservation for manual decoding, use std.json directly:

(import std.json)

(json:read-string "{\"id\":1}" 'keep-integers-exact? #t)

Sessions

Sessions hold default options and shared libcurl state such as DNS/cache/cookie sharing. Use a session when making multiple requests to the same service.

FunctionDescription
(make-session [opts])Create a session and optionally apply default options.
(session? value)Return whether value is a live HTTP session.
(close-session! session)Close a session and release native resources.
(session-set-option! session name value)Set a session default option.
(session-get-option session name)Read a session option.

Example:

(import net.http)

(define session
  (make-session
    '((timeout-ms . 30000)
      (connect-timeout-ms . 5000)
      (user-agent . "my-service/1.0")
      (bearer-token . "TOKEN"))))

(define user-json
  (get-json "https://example.com/api/me" session))

(define post-resp
  (post-json "https://example.com/api/events"
             (hash-map "event" "started")
             session))

(close-session! session)

The one-shot helpers accept either:

(get url)
(get url opts)
(get url session)
(get url opts session)

The same pattern applies to post, post-json, and get-json.

Request builder API

Use the builder API when you need full control over method, headers, bodies, or per-request overrides.

FunctionDescription
(make-request session method)Create a request bound to session; method is normally a symbol such as 'get, 'post, 'put, 'delete, 'head, etc.
(request? value)Return whether value is a request handle.
(request-set-url! request url)Set the request URL.
(request-set-header! request name value-or-false)Add a header; pass #f to remove existing headers with that name.
(request-set-option! request name value)Override an option for this request only.
(request-set-body! request string)Set a UTF-8 string body.
(request-set-body-string! request string charset)Set a string body and a text content type using charset.
(request-set-body-bytes! request bytevector)Set a raw bytevector body.
(request-set-body-file! request path)Load the request body from a file path.
(request-set-form! request fields)Send application/x-www-form-urlencoded form fields.
(request-set-multipart! request parts)Send multipart form data.
(perform request)Execute and buffer the response body in memory.
(perform-stream request)Execute through the streaming transfer loop; currently returns a response handle with captured body.

Example with custom headers:

(import net.http)

(define session (make-session '((timeout-ms . 10000))))
(define req (make-request session 'put))

(request-set-url! req "https://example.com/api/items/123")
(request-set-header! req "Accept" "application/json")
(request-set-header! req "Content-Type" "application/json")
(request-set-body! req "{\"name\":\"updated\"}")

(define resp (perform req))
(raise-for-status! resp)
(close-session! session)

Forms and multipart

URL-encoded form fields are alists of (key . value) pairs. Keys and values may be symbols or strings.

(import net.http)

(define session (make-session))
(define req (make-request session 'post))

(request-set-url! req "https://example.com/login")
(request-set-form! req
  '((username . "demo")
    (password . "secret")))

(define resp (perform req))
(close-session! session)

Multipart parts are lists with 2 to 4 fields:

(name data)
(name filename data)
(name filename content-type data)

data may be a string or bytevector.

(import net.http)

(define session (make-session))
(define req (make-request session 'post))

(request-set-url! req "https://example.com/upload")
(request-set-multipart! req
  '(("description" "Eta upload")
    ("file" "hello.txt" "text/plain" "hello from Eta\n")))

(define resp (perform req))
(close-session! session)

Responses

FunctionDescription
(response? value)Return whether value is a response handle.
(status response)Return the HTTP status code.
(ok? response)Return true for 2xx status codes.
(raise-for-status! response)Return response for 2xx; raise an error otherwise.
(body-bytes response)Return the response body as a bytevector.
(body-string response)Decode response body bytes as a bytewise string.
(body-json response)Decode response body as JSON.
(headers response)Return response headers as (name . value) pairs.
(header response name)Return the first matching header value, or #f.
(effective-url response)Return the final URL after redirects.

Example:

(import net.http)

(define resp (get "https://example.com"))

(if (ok? resp)
    (begin
      (display (header resp "Content-Type"))
      (newline)
      (display (body-string resp)))
    (raise-for-status! resp))

Options

Options are supplied as an alist:

'((timeout-ms . 10000)
  (follow-redirects . #t)
  (user-agent . "eta-client/1.0"))

Option names may be symbols or strings. They are case-normalized internally. Values must be booleans, non-negative integers, symbols, or strings depending on the option.

Boolean options

OptionDefaultDescription
follow-redirects#tFollow redirects when max-redirects is greater than zero.
verify-tls#tVerify TLS peer and host.
verbose#fEnable libcurl verbose output.

Integer options

OptionDefaultDescription
max-redirects10Maximum redirects to follow.
connect-timeout-ms30000Connection timeout in milliseconds.
timeout-ms0Total transfer timeout in milliseconds. 0 means no total timeout.
low-speed-limit-bps0Low-speed threshold in bytes per second.
low-speed-time-s0Seconds below the low-speed threshold before aborting.

String/path options

OptionDescription
user-agentUser-Agent header value.
accept-encodingAccepted response encodings; empty string lets libcurl request supported encodings.
ca-bundlePath to a CA bundle file.
ca-pathPath to a CA certificate directory.
client-certPath to client certificate.
client-keyPath to client private key.
usernameUsername for authentication.
passwordPassword for authentication.
bearer-tokenOAuth2 bearer token used by libcurl.
proxyProxy URL.
cookie-jarPath where cookies are written.
cookie-fileCookie file to read; empty uses in-memory cookies.
unix-socket-pathUnix domain socket path for HTTP-over-UDS.

Symbol/string option

OptionValuesDescription
http-version'any, 'http/1.1, 'http/2, 'http/2-tlsPreferred HTTP version. The native sidecar falls back when libcurl lacks HTTP/2 support.

Example with session defaults and a request override:

(import net.http)

(define session
  (make-session
    '((timeout-ms . 30000)
      (user-agent . "eta-client/1.0")
      (http-version . http/2-tls))))

(define req (make-request session 'get))
(request-set-url! req "https://example.com/slow")
(request-set-option! req 'timeout-ms 5000)

(define resp (perform req))
(close-session! session)

Downloads

download streams the response body directly to a file instead of keeping the payload in memory.

(import net.http)

(define resp
  (download "https://example.com/archive.zip"
            "archive.zip"
            '((timeout-ms . 0)
              (low-speed-limit-bps . 10000)
              (low-speed-time-s . 30))))

(raise-for-status! resp)
(status resp)

The returned response contains status, headers, and effective URL. The body bytes are written to the requested path.

URL helpers

FunctionDescription
(url-encode text)Percent-encode URL text.
(url-decode text)Decode percent-encoded URL text.
(url-parse text)Parse URL text into a component alist.
(url-build components)Build a URL from a component alist.
(import net.http)

(define encoded (url-encode "alpha beta/%eta?"))
(define decoded (url-decode encoded))

Version information

(import net.http)

(http-version)
; => (libcurl-version protocols features)

The result is a three-item list containing the libcurl version string, supported protocol strings, and feature symbols such as ssl, http2, brotli, zstd, or ipv6 when available.

Error handling

Network failures, invalid options, malformed URLs, and non-2xx status checks raise runtime errors through the Eta wrappers.

raise-for-status! is explicit: get, post, and post-json return responses for all HTTP status codes unless libcurl itself fails.

(import net.http)

(define resp (get "https://example.com/missing"))

(if (ok? resp)
    (body-string resp)
    (raise-for-status! resp))

Native sidecar

eta-http is loaded automatically when (import net.http) is evaluated, provided ETA_MODULE_PATH includes the package directory.

Host sidecar artifacts are staged under libs/<arch>/:

PlatformArtifact
Windows x64libs/amd64/eta_http.dll
Linux x64libs/amd64/libeta_http.so
Linux arm64libs/arm64/libeta_http.so
macOS x64libs/amd64/libeta_http.dylib
macOS arm64libs/arm64/libeta_http.dylib

Build and test

From the repository root, one local Windows/MSVC build shape is:

cmake -S packages/net/native/http -B out/http-msvc `
  -DETA_CORE_LIBRARY="C:/Users/lewis/develop/eta/out/msvc-release/eta/core/eta_core.lib" `
  -DETA_ETAI_EXECUTABLE="C:/Users/lewis/develop/eta/out/msvc-release/eta/tools/interpreter/etai.exe" `
  -DETA_STDLIB_DIR="C:/Users/lewis/develop/eta/stdlib"
cmake --build out/http-msvc --config Release
ctest --test-dir out/http-msvc --output-on-failure

Package-local smoke tests run against the loopback fixture in tests/fixtures/loopback_server.py and exercise sessions, GET/POST, JSON, downloads, status handling, URL helpers, and the cookbook examples.

← All packages View source on GitHub →