Skip to content

Clausal Prolog — HTTP & URL Modules (http, url)

Overview

Two modules for HTTP requests and URL manipulation, wrapping Python's urllib stdlib. Zero third-party dependencies.

-import_from(py.http, [get, json_get, post, request])
-import_from(py.url, [encode, decode, parse, join])

fetch_page(URL, BODY) <- get(URL, BODY)

fetch_api(URL, DATA) <- json_get(URL, DATA)

Or via module import:

-import_module(py.http)
-import_module(py.url)

from clausal import to_python

page(BODY) <- py.http.get("http://example.com", BODY)

def main():
    for BODY in --page(BODY):
        print(to_python(BODY))      # BODY is a string; to_python gives the str

URLs, headers and bodies may be written as strings or atoms. Every body the module hands back is a string (the ('$chars', text) carrier in Python), and a parsed JSON object is a DictTerm whose keys are atoms.


py.http — HTTP Requests

-import_from(py.http, [get, post, request, json_get, json_post])

Simple requests

Predicate Mode Description
get(Url, Body) +Url, -Body GET request, body as string. An HTTP error status (4xx/5xx) raises; see Errors.
get(Url, Headers, Body) +Url, +Headers, -Body GET with custom headers (DictTerm).
post(Url, Data, Body) +Url, +Data, -Body POST string data, response as string.
post(Url, Data, Headers, Body) +Url, +Data, +Headers, -Body POST with custom headers.
get("http://example.com", BODY)

get("http://api.example.com/data",
    {"Authorization": "Bearer TOKEN"},
    BODY)

post("http://example.com/submit", "key=value", RESPONSE)

General request

Predicate Mode Description
request(Options, Status, Body) +Options, -Status, -Body General HTTP request with full control.

Options is a DictTerm with keys:

Key Type Default Description
url string or atom required request URL
method string or atom "GET" HTTP method
headers DictTerm {} request headers
data string None request body
timeout number 30 Timeout in seconds

The keys are names: write them bare (declared) or single-quoted ('url'); a double-quoted "url" is matched by its text too.

Unlike get and post, request does not raise on 4xx/5xx — it returns the status code as a value so you can handle it explicitly (as Scryer's http_open/3 does). A network failure raises in request too, and so does a body that is not UTF-8 text, whatever the status.

Errors

A network failure or an HTTP error status raises (ruled 2026-10-02; it used to fail). ISO has no network error; where Scryer has a term it is used, otherwise the engine's system_error(Code) / resource_error(R):

Failure Error
the host does not resolve existence_error(source_sink, Url)
a DNS failure that is not "no such host" system_error(host_lookup_failed)
connection refused / reset system_error(connection_refused) / system_error(connection_reset)
host or network unreachable system_error(host_unreachable) / system_error(network_unreachable)
the timeout passed resource_error(timeout)
a TLS failure; a malformed response system_error(tls_failure); system_error(http_protocol_error)
status 404, 410 (get, post, json_get, json_post) existence_error(source_sink, Url)
status 401, 403, 407 permission_error(open, source_sink, Url)
any other status >= 400 system_error(http_status(Status))
a response body that is not UTF-8 text syntax_error(invalid_data)
a body json_get / json_post cannot parse as JSON syntax_error(invalid_json)

An error status is answered first: a 404 whose body is not text raises the 404. The body errors (ruled 2026-10-02; they used to fail, and request read an undecodable error body as "") are ISO's term for input text that cannot be read (syntax_error); invalid_data is Scryer's kind for bytes that are not UTF-8 on a text stream.

Wrap a call in catch/3 for the old failure: catch(get(U, B), error(existence_error(source_sink, _), _), fail).

request({"url": "http://example.com", "method": "HEAD"}, STATUS, BODY)

JSON requests

Predicate Mode Description
json_get(Url, Term) +Url, -Term GET + parse JSON → DictTerm/list
json_post(Url, Term, Response) +Url, +Term, -Response POST JSON + parse response

JSON objects are converted to DictTerm (atom keys), arrays to lists and JSON strings to strings, using the same conversion as the py.json module. Read a field as USER.name or py.json.get(USER, name, NAME) (with -import_module(py.json) and name declared); a string key "name" does not match an atom key and the goal fails.

json_get("http://api.example.com/users/1", USER),
NAME is USER.name              # JSON object keys are atoms: USER.name, or
                               # py.json.get(USER, name, NAME) -- not "name"

json_post("http://api.example.com/items",
         {"name": "Widget", "price": 9.99},
         RESPONSE)

py.url — URL Utilities

-import_from(py.url, [encode, decode, parse, join])
Predicate Mode Description
encode(String, Encoded) +String, -Encoded URL-encode (%20 for spaces, etc.)
decode(Encoded, String) +Encoded, -String URL-decode
parse(Url, Parts) +Url, -Parts parse URL into DictTerm
join(Parts, Url) +Parts, -Url Assemble URL from DictTerm

encode / decode

encode("hello world", E)   # E = "hello%20world"
decode("hello%20world", S) # S = "hello world"

encode uses safe="" — all special characters are encoded.

parse / join

parse returns a DictTerm keyed by the atoms scheme, host, port, path, query, fragment — so PARTS.scheme reads a part, and join consumes the same dict unchanged. Port is an integer (0 if not specified).

parse("https://example.com:8080/path?q=1#frag", PARTS)
# PARTS = {scheme: "https", host: "example.com", port: 8080,
#          path: "/path", query: "q=1", fragment: "frag"}
# (atom keys, string values)

join({scheme: "https", host: "example.com", path: "/api"}, URL)
# URL = "https://example.com/api"

Examples

Build a query URL

-import_from(py.url, [encode, join])
-import_from(py.http, [json_get])

search_api(QUERY, RESULTS) <- (
    encode(QUERY, SAFE_QUERY),
    append("https://api.example.com/search?q=", SAFE_QUERY, URL),   # strings are lists
    json_get(URL, RESULTS)
)

Check API health

-import_from(py.http, [request])

health_check(URL) <- (
    request({"url": URL, "timeout": 5}, STATUS, _),
    STATUS == 200
)

See also: JSON — JSON parsing and DictTerm integration · Dicts & Sets — DictTerm construction and access · Python Interop — ++() for advanced HTTP with requests.