Outbound HTTP
HTTP.getandHTTP.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
| Call | Returns |
|---|---|
res.status | Integer, e.g. 200 |
res.ok? | true when status is 200–299 |
res.body | String, "" when there was no body |
res.header("content-type") | String or nil, case-insensitive |
res.headers | Array[[String, String]] |
res.url | String, the URL this response came from — the final one after any redirects |
res.json | the 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
| Class | Raised when |
|---|---|
HTTP::TimeoutError | connect or read exceeded timeout: |
HTTP::TooManyRedirects | more than 5 hops |
HTTP::Error | DNS 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.