ward.net
ward.net.http- HTTP request primitivesward.net.fetch- fetch into a file/dirward.module- external module manager
local net = require("ward.net")
local http = require("ward.net.http")
local fetch = require("ward.net.fetch")
ward.net groups network-related helpers. Today it exposes two submodules:
ward.net.http- in-process HTTP requests viareqwestward.net.fetch- higher-level “fetch into a file/dir” helpers
ward.net.http - HTTP request primitives
Functions
All functions below are async (implemented with create_async_function).
Call them normally and receive a HttpResponse userdata.
http.get(url, opts?) -> HttpResponsehttp.delete(url, opts?) -> HttpResponsehttp.options(url, opts?) -> HttpResponsehttp.post(url, opts?) -> HttpResponsehttp.put(url, opts?) -> HttpResponse
Options (opts table)
opts is optional. When omitted or not a table, defaults are applied.
query(table) - query parameters.- Keys are strings.
- Values must be
string,number,integer, orboolean(they are converted to strings).
headers(table) - header map:string -> string.timeout(number) - request timeout in seconds (float accepted). Must be positive and finite.follow_redirects(boolean, defaulttrue) - when enabled, redirects are followed (limited to 10).allow_error(boolean, defaultfalse) -false: non-2xx responses raise a runtime error.true: non-2xx responses are returned asHttpResponse.
Body options (used by post and put):
json(any) - serializable Lua value encoded as JSON.form(table) - form fieldsstring -> string.
If both json and form are present, JSON takes precedence.
HttpResponse userdata
Returned by http.* functions.
Fields (also available via methods):
resp.status(integer) - HTTP status code.resp.headers(table) - header mapstring -> string.- Note: duplicate header names will be overwritten in the table (last one wins).
resp.body(string|nil) - response body decoded as text.
Methods:
resp:is_ok() -> boolean- true for 2xx.resp:get_status() -> integerresp:get_headers() -> tableresp:get_body() -> string|nil
Examples:
local http = require("ward.net.http")
-- Basic GET
local r = http.get("https://example.com", { follow_redirects = true })
print(r.status)
print(r:is_ok())
-- Query + headers
local r2 = http.get("https://httpbin.org/get", {
query = { q = "ward", page = 1, debug = true },
headers = { ["User-Agent"] = "ward" },
timeout = 10,
allow_error = true,
})
print(r2.status)
print(r2:body())
-- POST JSON
local r3 = http.post("https://httpbin.org/post", {
json = { hello = "world", n = 1 },
headers = { ["Content-Type"] = "application/json" },
})
assert(r3:is_ok())
ward.net.fetch - fetch into a file/dir
fetch is for “download/checkout into a path” workflows that compose well with ward.fs.
fetch.url(url, opts?) -> FetchResponse
Downloads the response body as bytes into a file (streaming), then returns metadata.
Options (opts table):
method(string, default"GET") - one of:GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS.headers(table) - header mapstring -> string.timeout(number) - request timeout in seconds.follow_redirects(boolean, defaulttrue) - redirects are followedinsecure(boolean, defaultfalse) - allow invalid TLS certificates. (limited to 10).into(string|nil) - destination file path.- If omitted, Ward creates a unique file path under the OS temp directory.
max_bytes(integer|number|nil) - maximum allowed response size.- Values
<= 0disable the limit. - If the limit is exceeded, Ward removes the partial file and returns
ok=falsewithstatus=413andpath=nil.
- Values
FetchResponse userdata fields/methods:
resp.ok/resp:is_ok() -> booleanresp.status/resp:get_status() -> integer- HTTP status code (or413ifmax_bytesexceeded).resp.path/resp:get_path() -> string|nil- destination path.resp.size/resp:get_size() -> integer- bytes written.
Example:
local fetch = require("ward.net.fetch")
local fs = require("ward.fs")
local r = fetch.url("https://example.com/file.tar.gz", {
into = "./downloads/file.tar.gz",
max_bytes = 50 * 1024 * 1024,
})
if not r.ok then
error("fetch failed: status=" .. tostring(r.status))
end
print("saved to", r.path, "bytes", r.size)
assert(fs.is_file(r.path))
fetch.git(url, opts?) -> FetchResponse
Clones a Git repository into a directory (using the external git command),
then optionally checks out a revision.
Notes:
- Requires
gitto be installed and discoverable inPATH(it is relies on a systemgitbinary). gitstdout/stderr are suppressed; useok/statusto handle errors.
Options (opts table):
into(string|nil) - destination directory.- If omitted, Ward creates a unique directory under the OS temp directory.
depth(integer|nil) - shallow clone depth (must be > 0). Defaults to1.filter_blobs(boolean, defaulttrue) - when true, uses--filter=blob:none.branch(string|nil) - clone a specific branch.tag(string|nil) - clone a specific tag (used as--branch <tag>).- If both
branchandtagare set,branchtakes precedence.
- If both
recursive(boolean, defaultfalse) - when true, uses--recurse-submodules.rev(string|nil) - if set, runsgit checkout <rev>after cloning.timeout(number|nil) - command timeout in seconds.max_bytes(integer|number|nil) - maximum allowed on-disk size for the cloned directory.- If exceeded, Ward removes the directory and returns
ok=falsewithstatus=413andpath=nil.
- If exceeded, Ward removes the directory and returns
On success, FetchResponse.status is 0 and ok=true. On failure, status is
the git exit code.
Example:
local fetch = require("ward.net.fetch")
local r = fetch.git("https://github.com/user/repo.git", {
into = "./vendor/repo",
depth = 1,
rev = "v1.2.3",
recursive = false,
timeout = 120,
})
if not r.ok then
error("git fetch failed: exit=" .. tostring(r.status))
end
print("checked out into", r.path, "bytes", r.size)
ward.module - external module manager
ward.module downloads third-party Lua modules into Ward’s data directory and
exposes them via require("<name>") for the lifetime of the
process. Downloads are content-addressed so multiple revisions can coexist and
be reused across runs.
local module = require("ward.module")
Where modules live
- Base directory:
${XDG_DATA_HOME}/ward/externals(or~/.local/share/ward/externalsifXDG_DATA_HOMEis unset). - Content-addressed store:
${base}/.store/<id>, where<id>issha256(normalized_url + "\n" + selector). - A per-process binding map ensures
require("<name>")resolves to the revision selected earlier in the same run. Rebinding to a different id is rejected unlessforce=true, in which case the nextrequirereloads the module.
Naming rules
- Default name comes from the URL basename (extensions,
.git,.lua,.zip,.tar.gz,.tgz, and query/fragment removed). - Names are canonicalized to lowercase with non-alphanumeric separators folded
into
_. Leading digits are prefixed with_. require("<name>.<submodule>")is supported; segments must be alphanumeric or_.
module.dir() -> string
Returns the absolute path to the externals base directory.
print(module.dir())
module.git(url, opts?) -> { ok, name, require, path, store_path, id }
Clone a Git repository into the externals store and bind it for require.
Options (all optional):
name(string) - override derived name and<name>binding.- Exactly one of
rev,branch, ortagselects a revision; default ishead. Different selectors produce different store ids. force(bool, defaultfalse) - allow rebinding an already-bound name to a new id (also clears cachedrequired module).depth(integer) - shallow clone depth (passed toward.net.fetch.git).recursive(bool) - fetch submodules (git --recurse-submodules).timeout(number, seconds) - overall fetch timeout.max_bytes(integer) - abort if transfer exceeds this many bytes.filter_blobs(bool) - request partial clone with blob filtering when supported by the remote.
Return table:
ok(bool) - whether the checkout succeeded.name(string) - canonicalized name.require(string) - require target (<name>).path/store_path(string) - final checkout directory.id(string) - content-addressed id.
Example (pin to a tag and require a submodule):
local module = require("ward.module")
local json = require("ward.convert.json")
local result = module.git("https://github.com/org/repo.git", {
name = "my_lib",
tag = "v1.2.3",
depth = 1,
recursive = true,
})
assert(result.ok, "git fetch failed")
local api = require(result.require .. ".api")
print(json.encode({ id = result.id, path = result.path, ok = result.ok }))
module.url(url, opts?) -> { ok, name, require, path, store_path, id }
Download a single Lua file and bind it as <name>. The URL content is
stored as init.lua inside its store directory.
Options (all optional):
name(string) - override derived name.insecure(bool, defaultfalse) - allow invalid TLS certificates.follow_redirects(bool, defaulttrue).retries(integer, default5, minimum1).retry_delay(milliseconds, default2000) - delay between retries.force(bool, defaultfalse) - rebind an existing name to the downloaded id.timeout(number, seconds) - overall request timeout.max_bytes(integer) - abort if download exceeds this size.method(string) - HTTP method (defaultGET).headers(table<string, string>) - additional request headers.
Return table fields match module.git.
Example (with custom headers and POST body):
local module = require("ward.module")
local result = module.url("https://example.com/plugin.lua", {
headers = { Authorization = "Bearer TOKEN" },
method = "POST",
retry_delay = 500,
})
if not result.ok then error("download failed") end
local plugin = require(result.require)
print(plugin.version())