Skip to content

HTTP & JSON

HTTP

HTTP request functions make synchronous requests and return a response map.

Sandbox capabilities

http/get, http/post, http/put, http/query, http/delete, and http/request work without extra configuration in Sema's default mode. In a sandboxed run they require the network capability (NETWORK) and return PermissionDenied when it is denied. The json/* functions do not require a capability. See the CLI sandbox documentation.

Response Map

All HTTP functions return a map with three keys:

KeyTypeDescription
:statusintHTTP status code (e.g., 200, 404, 500)
:headersmapResponse headers as keyword-keyed map
:bodystring | bytevectorResponse body — a string by default, or a bytevector with {:as :bytes}
sema
(define resp (http/get "https://httpbin.org/get"))

(:status resp)    ; => 200
(:headers resp)   ; => {:content-type "application/json" :server "..." ...}
(:body resp)      ; => "{\"args\": {}, ...}"

Headers are returned with keyword keys derived from the header name (e.g., Content-Type becomes :content-type). The body is a raw string by default — use json/decode to parse JSON responses, or {:as :bytes} to get a bytevector for binary downloads (see Binary bodies & downloads).

Options Map

The http/get, http/post, http/put, http/delete, and http/request functions accept an optional options map with the following keys:

KeyTypeDescription
:headersmapRequest headers (string or keyword keys both work)
:timeoutintRequest timeout in milliseconds
:askeywordResponse body decoding: :text (default) or :bytes (a bytevector)
:multipartlistSend a multipart/form-data body (file uploads) — see Multipart & file uploads
sema
;; Custom headers and timeout
(http/get "https://api.example.com/data"
  {:headers {"Authorization" "Bearer tok_abc123"
             "Accept" "application/json"}
   :timeout 5000})

Binary bodies & downloads

The request body may be a bytevector to send raw bytes (a binary upload); it's sent verbatim with no JSON encoding. Set your own Content-Type header if the server needs one.

Pass {:as :bytes} to receive the response :body as a bytevector instead of a string — required for binary payloads (audio, images, PDFs) that would be corrupted by UTF-8 text decoding. Pair with file/write-bytes to save a download.

sema
;; Download binary data and save it to disk
(let ((resp (http/get "https://api.example.com/audio.mp3" {:as :bytes})))
  (file/write-bytes "out.mp3" (:body resp)))

;; Upload raw bytes (e.g. an image read from disk)
(http/post "https://api.example.com/upload"
  (file/read-bytes "photo.jpg")
  {:headers {"Content-Type" "image/jpeg"}})

Multipart & file uploads

Set :multipart in the options map to a list of part maps to send a multipart/form-data body. Each part is {:name "..." :content ...} plus optional :filename and :content-type. A :filename (or bytevector content) marks the part as an uploaded file. When :multipart is present the positional body is ignored.

Part keyTypeDescription
:namestring (required)The form field name
:contentstring | bytevectorThe field value or file bytes (required)
:filenamestring (optional)Upload as a file with this name
:content-typestring (optional)MIME type for the part
sema
;; Upload a file alongside a text field
(http/post "https://api.example.com/documents"
  {}    ; positional body ignored when :multipart is set
  {:headers {"Authorization" "Bearer tok_abc123"}
   :multipart (list
     {:name "purpose"  :content "rag-ingest"}
     {:name "file"     :filename "report.pdf"
      :content (file/read-bytes "report.pdf")
      :content-type "application/pdf"})})

http/get

(http/get url)
(http/get url opts)

Make an HTTP GET request.

  • url — string, the request URL
  • opts — optional options map: :headers, :timeout, :as (:text/:bytes), :multipart
sema
;; Simple GET
(http/get "https://httpbin.org/get")

;; GET with custom headers
(http/get "https://api.example.com/users"
  {:headers {:authorization "Bearer my-token"}})

http/post

(http/post url body)
(http/post url body opts)

Make an HTTP POST request.

  • url — string, the request URL
  • body — request body: a map (auto-encoded as JSON with Content-Type: application/json), a string (sent as-is), or a bytevector (sent as raw bytes). Ignored when :multipart is set.
  • opts — optional options map: :headers, :timeout, :as (:text/:bytes), :multipart
sema
;; POST with a map body (auto-JSON-encoded)
(http/post "https://httpbin.org/post"
  {:name "Ada" :age 36})

;; POST with string body and custom headers
(http/post "https://api.example.com/webhook"
  "raw payload"
  {:headers {"Content-Type" "text/plain"}})

;; POST with JSON body and auth
(http/post "https://api.example.com/users"
  {:name "Ada" :role "admin"}
  {:headers {"Authorization" "Bearer tok_abc123"}
   :timeout 10000})

http/put

(http/put url body)
(http/put url body opts)

Make an HTTP PUT request. Behaves identically to http/post — map bodies are auto-JSON-encoded.

  • url — string, the request URL
  • body — request body (string or map)
  • opts — optional options map: :headers, :timeout, :as (:text/:bytes), :multipart
sema
(http/put "https://api.example.com/users/42"
  {:name "Ada Lovelace" :role "admin"})

http/delete

(http/delete url)
(http/delete url opts)

Make an HTTP DELETE request.

  • url — string, the request URL
  • opts — optional options map: :headers, :timeout, :as (:text/:bytes), :multipart
sema
(http/delete "https://api.example.com/users/42"
  {:headers {"Authorization" "Bearer tok_abc123"}})

http/request

(http/request method url)
(http/request method url opts)
(http/request method url opts body)

Make an HTTP request with any method. Use this for methods not covered by the convenience functions (e.g., PATCH, HEAD).

  • method — string, HTTP method (case-insensitive, converted to uppercase). Supported: GET, POST, PUT, DELETE, PATCH, HEAD
  • url — string, the request URL
  • opts — optional options map: :headers, :timeout, :as (:text/:bytes), :multipart
  • body — optional request body (string or map)
sema
;; PATCH request
(http/request "PATCH" "https://api.example.com/users/42"
  {:headers {"Content-Type" "application/json"}}
  {:name "Updated Name"})

;; HEAD request (body will be empty)
(define resp (http/request "HEAD" "https://example.com"))
(:status resp)    ; => 200
(:body resp)      ; => ""

Error Handling

Network errors (DNS failure, connection refused, timeout) throw a SemaError::Io error. Use try/catch to handle them:

sema
;; Handle network errors
(try
  (http/get "https://unreachable.invalid")
  (catch e
    (println "Request failed:" e)))

;; Check status codes
(define resp (http/get "https://api.example.com/data"))
(cond
  ((= (:status resp) 200) (json/decode (:body resp)))
  ((= (:status resp) 404) (error "Not found"))
  ((>= (:status resp) 500) (error "Server error"))
  (else (error (format "Unexpected status: ~a" (:status resp)))))

;; Timeout handling
(try
  (http/get "https://slow-api.example.com/data"
    {:timeout 3000})
  (catch e
    (println "Request timed out or failed:" e)))

Common Patterns

GET + JSON Decode Pipeline

sema
;; Fetch JSON data and extract fields
(define data
  (-> (http/get "https://api.example.com/users/1")
      (:body)
      (json/decode)))

(:name data)   ; => "Ada"
(:email data)  ; => "ada@example.com"

POST with JSON Body and Auth Headers

sema
(define resp
  (http/post "https://api.example.com/posts"
    {:title "Hello World" :body "Content here"}
    {:headers {"Authorization" "Bearer tok_abc123"
               "X-Request-Id" "req-001"}}))

(when (= (:status resp) 201)
  (println "Created:" (:body resp)))

Paginated API Requests

sema
(define (fetch-all-pages base-url)
  (let loop ((page 1) (results '()))
    (define resp (http/get (format "~a?page=~a" base-url page)))
    (define data (json/decode (:body resp)))
    (define items (:items data))
    (if (empty? items)
      results
      (loop (+ page 1) (append results items)))))

JSON

Functions for encoding Sema values to JSON strings and decoding JSON strings back into Sema values.

Type Mapping

Encoding (Sema → JSON)

Sema TypeJSON TypeNotes
intnumber4242
floatnumber3.143.14. NaN/Infinity cause errors
stringstring"hello""hello"
keywordstring:name"name"
symbolstring'foo"foo"
#t / #fboolean#ttrue, #ffalse
nilnullnilnull
listarray'(1 2 3)[1, 2, 3]
vectorarray[1 2 3][1, 2, 3]
mapobject{:a 1}{"a": 1}
hashmapobjectSame as map
functionerrorCannot encode functions as JSON
recorderrorCannot encode records as JSON

Decoding (JSON → Sema)

JSON TypeSema TypeNotes
numberint/floatIntegers decode as int, decimals as float
stringstring"hello""hello"
booleanbooltrue#t, false#f
nullnilnullnil
arraylist[1, 2](1 2)
objectmapKeys become keywords: {"a": 1}{:a 1}

json/encode

(json/encode value) → string

Encode a Sema value as a compact JSON string. Uses strict conversion — errors on values that cannot be represented in JSON (functions, records, NaN, Infinity).

  • value — any JSON-encodable Sema value
sema
(json/encode 42)                    ; => "42"
(json/encode "hello")               ; => "\"hello\""
(json/encode #t)                    ; => "true"
(json/encode nil)                   ; => "null"
(json/encode '(1 2 3))             ; => "[1,2,3]"
(json/encode [1 2 3])              ; => "[1,2,3]"
(json/encode {:name "Ada" :age 36}) ; => "{\"age\":36,\"name\":\"Ada\"}"

Encoding errors:

sema
;; NaN and Infinity cannot be represented in JSON
(json/encode (/ 0.0 0.0))   ; Error: cannot encode NaN/Infinity as JSON

;; Functions cannot be encoded
(json/encode println)        ; Error: cannot encode native-fn as JSON

json/encode-pretty

(json/encode-pretty value) → string

Encode a Sema value as a pretty-printed JSON string with 2-space indentation. Same strict conversion rules as json/encode.

  • value — any JSON-encodable Sema value
sema
(json/encode-pretty {:name "Ada" :scores [95 87 92]})
;; =>
;; {
;;   "name": "Ada",
;;   "scores": [
;;     95,
;;     87,
;;     92
;;   ]
;; }

json/decode

(json/decode json-string) → value

Decode a JSON string into a Sema value. JSON objects become maps with keyword keys, arrays become lists. See the type mapping table for full details.

  • json-string — a string containing valid JSON
sema
(json/decode "42")                          ; => 42
(json/decode "3.14")                        ; => 3.14
(json/decode "\"hello\"")                   ; => "hello"
(json/decode "true")                        ; => #t
(json/decode "null")                        ; => nil
(json/decode "[1, 2, 3]")                   ; => (1 2 3)
(json/decode "{\"name\": \"Ada\"}")         ; => {:name "Ada"}

Decoding errors:

sema
;; Invalid JSON throws an error
(json/decode "not json")    ; Error: json/decode: expected value at line 1 column 1

;; Argument must be a string
(json/decode 42)            ; Error: type error: expected string, got int

JSON Roundtrips

Values that survive an encode → decode roundtrip preserve their structure, though some types are normalized:

sema
;; Vectors become lists after roundtrip
(json/decode (json/encode [1 2 3]))   ; => (1 2 3)

;; Keywords in maps are preserved
(json/decode (json/encode {:a 1 :b 2}))   ; => {:a 1 :b 2}

;; Nested structures work
(define data {:users [{:name "Ada"} {:name "Bob"}]
              :count 2
              :active #t})
(define roundtripped (json/decode (json/encode data)))
(:count roundtripped)   ; => 2
(:active roundtripped)  ; => #t

Error Handling

JSON encoding and decoding errors can be caught with try/catch:

sema
;; Catch encoding errors
(try
  (json/encode (/ 0.0 0.0))
  (catch e
    (println "Encode failed:" e)))

;; Catch decoding errors
(try
  (json/decode "invalid json {{{")
  (catch e
    (println "Decode failed:" e)))