Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Outbound HTTP

HTTP.get and HTTP.post: the only way an app talks to another server.

HTTP is the only way an app talks to another server. It is a thin client over net/http with timeouts, redirect following and errors you can rescue. HTTP and Melee::HTTP are the same module; either name works.

res = HTTP.get("https://example.com/feed.ics", timeout: 10)
halt 502, "feed is down" unless res.ok?
body = res.body

The two calls

HTTP.get(url, query: {}, headers: {}, timeout: 10, raise_on_error: false)          -> HTTP::Response
HTTP.post(url, body:, query: {}, headers: {}, timeout: 10, raise_on_error: false)   -> HTTP::Response

url is a String with an http or https scheme; anything else raises HTTP::Error. headers is a Hash{String => String} (both are stringified). body is a required String on post — set your own Content-Type header if the server needs one; there is no form or JSON encoding step, so build the body yourself with JSON.generate(...) or by hand.

query: is a Hash appended to the URL, percent-encoded for you. Use it rather than building a query string by hand:

res = HTTP.get("https://example.com/search", query: { "q" => params.fetch(:q), "limit" => 20 })

Only GET and POST exist. There is no PUT, PATCH, DELETE, HEAD, no request streaming, no connection reuse and no cookie jar.

The response

CallReturns
res.statusInteger, e.g. 200
res.ok?true when status is 200–299
res.bodyString, "" when there was no body
res.header("content-type")String or nil, case-insensitive
res.headersArray[[String, String]]
res.urlString, the URL this response came from — the final one after any redirects
res.jsonthe body parsed as JSON; raises HTTP::Error when the body is not JSON
res = HTTP.get("https://example.com/api/notes")
notes = res.ok? ? res.json : []

Index a parsed body by String keys and narrow with is_a? before arithmetic, as with request.json.

A 4xx or 5xx is a normal return, not an exception — check res.ok? or res.status. Pass raise_on_error: true to get a HTTP::Error for any status ≥ 400 instead.

Timeouts

timeout: is whole seconds and applies to both connect and read; the default is 10. A timeout raises HTTP::TimeoutError. Choose it deliberately: the request that is calling out has its own deadline at the server, and a slow upstream holds a request slot for the whole time.

A Float is rounded up — timeout: 0.5 waits a second — because a sub-second timeout is not something the compiled app can express. Write the number you mean. timeout: 0 is the one value that reads backwards: net/http takes it as no timeout, so the call waits until melee-server kills the request.

Redirects

Any 3xx with a Location is followed automatically, up to 5 hops. 307 and 308 repeat the method and body; every other 3xx becomes a GET with no body. A sixth hop raises HTTP::TooManyRedirects.

Errors

def refresh_feed(feed_id, name, url)
  body = HTTP.get(url, timeout: 10).body
  store_events(feed_id, ICal.parse(body))
  log.info "feed refreshed", feed: name
rescue Melee::HTTP::Error => e
  db.run "UPDATE feeds SET error = ? WHERE id = ?", "#{e.class}: #{e.message}", feed_id
  log.warn "feed failed", feed: name, error: e.message
end
ClassRaised when
HTTP::TimeoutErrorconnect or read exceeded timeout:
HTTP::TooManyRedirectsmore than 5 hops
HTTP::ErrorDNS failure, connection refused, TLS failure, a non-http(s) URL, or raise_on_error: with a ≥ 400 status

The first two are subclasses of HTTP::Error, so one rescue Melee::HTTP::Error catches everything this module raises. Always rescue it somewhere: a network call inside a request that raises becomes a 500 for the visitor.

Write the full path in a rescue. HTTP is an alias for Melee::HTTP, and a rescue clause that reaches the class through an alias does not match under Spinel — it matches under CRuby, so melee dev looks right and the deployed app does not rescue at all. Use rescue Melee::HTTP::Error. melee check refuses the other spelling, naming the line, so this is a build error rather than something to remember. Calls are fine either way: HTTP.get is the same as Melee::HTTP.get. (dialect.md, docs/research/spinel.md.)

TLS

https URLs verify the certificate chain against the system store. There is no flag to turn that off, and none is planned. Certificate problems arrive as HTTP::Error.

The User-Agent is melee/0.1 and Accept is */* unless you set them in headers:.

Where to call it from

Anywhere a route can reach — a route block, a helper, an error handler. There is no background job runner yet, so an outbound call happens while a visitor waits. For anything slow, fetch it on a request that can afford it (an admin “refresh now” button, as the kitchen app does) and store the result in the database.