Skip to content
Ori Pekelman edited this page May 11, 2026 · 1 revision

Tep::Http

Outbound HTTP/1.0 client, Faraday-shaped. Built on the same socket plumbing the inbound server uses (sphttp_connect / sphttp_recv_all); the missing piece this module adds is the HTTP/1.0 framing on top.

Scope

Feature v1
Plain HTTP yes
HTTPS / TLS no
HTTP/1.0 + close yes (only)
Keep-alive no
Chunked-transfer no (response)
Auto-redirects no
Streaming bodies no
Response cap ~64 KB

These limits cover the dashboard's needs (local inference backends, internal APIs over plain HTTP) and the bulk of "hit an internal API" workloads. HTTPS + keep-alive + chunked land as a v2 surface.

Class shortcuts

Faraday-style one-shots; build a default empty headers hash and dispatch through the workhorse send_req:

res = Tep::Http.get("http://api.local/users/42")
res = Tep::Http.post("http://api.local/users", '{"name":"a"}')
res = Tep::Http.put("http://api.local/users/42", body)
res = Tep::Http.patch("http://api.local/users/42", body)
res = Tep::Http.delete("http://api.local/users/42")
res = Tep::Http.head("http://api.local/users/42")

res.status         # Integer; 0 on connect/send failure
res.headers["x"]   # downcased keys
res.body           # String

Reusable client

When you want a base URL + default headers shared across many calls:

c = Tep::Http.new("http://api.local")
c.set_header("Authorization", "Bearer " + token)
c.set_header("Accept", "application/json")

res = c.do_get("/users/42")
res = c.do_post("/users", body)
res = c.do_put("/users/42", body)
res = c.do_patch("/users/42", body)
res = c.do_delete("/users/42")
res = c.do_head("/users/42")

The instance verbs are do_get / do_post / etc. rather than the bare Faraday names because Tep::Session#get(k) used to collide with them in spinel's same-named-imeth-across-classes unifier (fixed upstream in spinel #429, but the do_ spelling is preserved for stability and to keep the call sites visually distinct from the inbound-route DSL).

path is appended to base_url when it starts with /; pass a full URL to override.

Custom-headers one-shot

The class shortcuts always pass an empty headers hash. To inject per-request headers without standing up an instance:

h = Tep.str_hash
h["Accept"] = "application/json"
h["X-Trace-Id"] = trace_id
res = Tep::Http.send_req("GET", "http://api.local/users/42", "", h)

send_req(verb, url, body, headers) is the lower-level workhorse; both the instance and class verbs ultimately call into it.

Error handling

There are no exceptions in Tep::Http. The Response struct always comes back. On connect failure, send failure, malformed response, or unsupported scheme (HTTPS, etc.):

res = Tep::Http.get("https://example.com/")  # unsupported scheme
res.status   # 0
res.body     # ""

Inspect res.status == 0 for transport-level failures; non-zero statuses are real HTTP responses. The framework will not retry, redirect, or raise on its own.

Cookbook

POST JSON + read JSON back

require 'tep/json'

req_body = Tep::Json.from_str_hash({"name" => "alice"})
h = Tep.str_hash
h["Content-Type"] = "application/json"
res = Tep::Http.send_req("POST", "http://api.local/users", req_body, h)

if res.status == 201
  new_id = Tep::Json.get_str(res.body, "id")
  return new_id
end

Probe a local inference endpoint (Ollama / vLLM)

res = Tep::Http.get("http://localhost:11434/")
if res.status == 200
  "inference up: " + res.body
else
  "inference down (status=" + res.status.to_s + ")"
end

Hit an internal API with auth + default headers

c = Tep::Http.new("http://internal.svc.local")
c.set_header("Authorization", "Bearer " + ENV.fetch("API_TOKEN"))

before do
  res = c.do_get("/healthz")
  if res.status != 200
    halt 503, "upstream down"
  end
end

Pitfalls

  • Static-buffer return values. Tep::Http itself dups response strings before returning, but if you chain through other libs that call into sphttp_* FFI helpers directly, remember that bare :str returns alias static buffers — copy with + "" if you need to keep two values alive at once.
  • Content-Length and request body. The client sets Content-Length automatically from body.length. Don't pre-set it in the headers hash; that produces two Content-Length headers in the request.
  • Connection: close is hard-coded. Each call opens a fresh socket. For tight loops, batch on the server side or move to a background Tep::Job.

Clone this wiki locally