The web:: Library
Host libraries, not language. The jaiscript runner and jai_spider register both libraries on this page. An engine embedded in another host has neither until that host calls jai::stdlib::register_web(engine) (which register_all leaves out) and, for the client, jai::stdlib::register_spider(engine) with an HTTP stack and the network permission (The HTTP transport).
jai_spider is the web server host: it runs a script as a site, calling the script's handler once per HTTP request and sending back what the handler returns. Its routes, requests, sessions and SQLite are on its own page; this page covers the two libraries a script calls: web:: text functions and the spider:: HTTP client. Pages made from files are HTML Templates.
Part 1: web:: text functions
Every web:: function is pure, so all of them run in parallel bodies.
HTML text
web::html_escape(s) writes & < > " ' as & < > " ' (PHP's htmlspecialchars with ENT_QUOTES) and nothing else. The result is safe in element text and in a quoted attribute, and nowhere else (where escaping is not enough).
web::html_unescape(s) reads character references as a browser does in text: the 2231 HTML5 names, the legacy names without their ; (longest match wins, so ¬it; is ¬it;), and numeric references with the ; optional. A reference to 0, a surrogate or past U+10FFFF becomes U+FFFD; 0x80 to 0x9F read as windows-1252. Anything else stays as written, so unescaping an escaped string gives the string back. Both functions return the argument's kind: a utf8_string in, a utf8_string out.
string name = "Tom & Jerry's <b>show</b>";
print(`<p title="${web::html_escape(name)}">`);
print(web::html_unescape("café © 2026 🙂 – � &bogus; AT&T"));
print(web::html_unescape(web::html_escape(name)) == name);
utf8_string word = "café";
print(type_of(web::html_escape(word)));<p title="Tom & Jerry's <b>show</b>">
café © 2026 🙂 – � &bogus; AT&T
true
utf8_stringPercent-encoding
web::url_encode(s) encodes a path segment or any other URL part (RFC 3986, a space is %20); web::form_encode(s) encodes a form body or query string (a space is +). web::url_decode(s) and web::form_decode(s) read them back. The exact byte sets: encoding and query strings.
Decoding never fails: a % without two hex digits after it is kept as written. The decoded bytes need not be valid UTF-8.
string title = "Snap Jaw/Social? 50% off & more";
print(web::url_encode(title));
print(web::form_encode(title));
print(web::url_decode("100%25 sure, 100% sure"));
print(web::url_decode("a+b"), " ", web::form_decode("a+b"));Snap%20Jaw%2FSocial%3F%2050%25%20off%20%26%20more
Snap+Jaw%2FSocial%3F+50%25+off+%26+more
100% sure, 100% sure
a+b a bQuery strings
web::parse_query(s) reads a query into a map<string, string> where a repeated key keeps its last value, web::parse_query_all(s) keeps every value as an array, and web::build_query(m) form-encodes a map in its key order. Every rule: encoding and query strings.
print(to_json(web::parse_query("?page=2&sort=new+first&tag=a&tag=b&draft")));
print(to_json(web::parse_query_all("tag=a&tag=b&tag=c+d")));
print(web::build_query({"q": "jai script", "page": 2, "tags": ["a", "b c"], "draft": false, "skip": null}));{"draft":"","page":"2","sort":"new first","tag":"b"}
{"tag":["a","b","c d"]}
draft=false&page=2&q=jai+script&tags=a&tags=b+cURLs
web::parse_url(s) splits an RFC 3986 URI reference into a web::url value with the fields scheme, user, password, host, port (an int, 0 when absent), path, query and fragment.
- Nothing is decoded going in or encoded coming out: encode a path segment with
web::url_encodeand a query withweb::build_querybefore storing them. - A reference without a scheme or host (
/search?q=jai,//cdn.example/x,page.html#top) parses too. An IPv6 host keeps its brackets. - A port outside 0 to 65535, or a bracket that does not close, raises the error below.
to_string()joins the fields back, writing//and the authority only when there is a host, a user or a port, and?and#only before a non-empty query and fragment.
web::url u = web::parse_url("https://ada:pw@[::1]:8443/posts/7?sort=new#comments");
print(to_json([u.scheme, u.user, u.password, u.host, u.port, u.path, u.query, u.fragment]));
u.path = "/posts/8";
u.query = web::build_query({"sort": "old", "page": 2});
u.user = "";
u.password = "";
print(u.to_string());
try { web::parse_url("http://example.org:99999/"); } catch (e) { print(e); }["https","ada","pw","[::1]",8443,"/posts/7","sort=new","comments"]
https://[::1]:8443/posts/8?page=2&sort=old#comments
web::parse_url: 'http://example.org:99999/' has a malformed host or portPart 2: HTTP with spider::
Each call runs one request to its whole answer and returns a spider::Response. It blocks the script until then, inside a coroutine too, and a parallel body cannot call it. The two sending calls take their arguments in opposite orders:
| Call | Argument order | Default method |
|---|---|---|
spider::catch(url[, options]) | URL | GET |
spider::get(url[, options]) | the same call, plain name | GET |
spider::sling(content, url[, options]) | content, then URL | POST |
spider::post(url, content[, options]) | URL, then content | POST |
This sample reaches the network, so it is shown without output:
spider::Response page = spider::catch("https://api.example.org/scores?game=snapjaw");
if (page.ok()) { print("top score: ", page.json()[0]["points"]); }
spider::Response saved = spider::sling({"player": "ada", "points": 420}, "https://api.example.org/scores");
print(saved.status, " ", saved.header("Location"));
spider::Response gone = spider::catch("https://api.example.org/scores/7",
{"method": "DELETE", "headers": {"Authorization": "Bearer " + token}, "timeout": 5});Text content is sent as it is (text/plain; charset=utf-8); any other value (a map, an array, a number, a bool, null) is sent as to_json writes it (application/json). The options map takes:
method, headers, timeout (seconds, default 30), type and redirects (default 5). Their values: request options.
spider::Response
An HTTP answer: what a client call returns, and what a jai_spider handler builds to answer a request. An error status is an answer, not an error: ok() tells them apart. A response is a value: assigning one copies its headers and cookies.
| Member | What it is |
|---|---|
status, ok() | the status code (int); true for 2xx |
body, json() | the body's bytes; from_json of them |
header(name) | a header by any spelling of its name, "" when absent |
set_cookie(name, value[, options]) | adds a Set-Cookie line, the value percent-encoded |
Every member and cookie option: spider::Response members.
Cookies and sessions: what is and is not protected
spider::Response r = spider::Response(200, "hi");
r.set_cookie("session", "abc 1", {"max_age": 3600});
r.set_cookie("theme", "dark", {"secure": true, "http_only": false, "same_site": "Strict"});
for (auto line : r.cookies) { print(line); }session=abc%201; Path=/; Max-Age=3600; HttpOnly; SameSite=Lax
theme=dark; Path=/; Secure; SameSite=Strict- On by default: every cookie, the
jai_spidersession cookie included, isHttpOnly(page script cannot read it) andSameSite=Lax(a browser leaves it off cross-site form posts and subrequests). - Off by default:
Secure. Pass"secure": true(on a session store,secure_cookie = true) once the site is served over HTTPS. - No CSRF tokens. Neither
spider::norjai_spiderissues or checks one.SameSite=Laxstill sends the cookie on a cross-site link or other top-level GET, so a GET handler must not change state, and a form that needs more thanLaxcarries a token the application makes and checks. - A handler's plain string is served as HTML, unescaped.
jai_spidersends a returned string astext/html, and a backtick string's${...}does not escape. Return atemplate(...), whose splices are escaped, or pass each value throughweb::html_escape.
Redirects and errors
A 301, 302, 303, 307 or 308 answer with a Location is followed up to redirects times, a relative Location resolving against the URL that answered. As in a browser:
- a 303 turns any method but HEAD into a GET without a body, a 301 or 302 turns a POST into a GET, and a 307 or 308 sends the same request again;
- Authorization and Cookie headers are dropped when a redirect leaves the origin they were given for;
- a redirect from https to http is refused.
Every failure is a catchable error that starts with the call's name (the reference has each full text):
- Before anything is sent: an unknown or wrong-kind option (
spider::catch: unknown option 'retries' (method, headers, timeout, type, redirects)); a header name that is not a token, or a value holding a line break or NUL; a header the transport writes itself (Host,Content-Length,Transfer-Encoding,Connection); a URL that is not absolute http or https; a host that installed no transport;Insufficient privilege: network.without the permission. - During the call:
spider::catch: http://example.org/slow: timed out after 5 s,more than 5 redirects,a redirect from https to http (http://example.org/) is refused, or the transport's own reason (cannot connect to 127.0.0.1:1: no connection, an unresolved host, a certificate that did not verify, an answer cut short).