ruby / http clients
I would rather have written and maintained code like this than depended on a library specific to the API:
require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "http"
end
url = ENV["API_URL"]
if url.to_s.strip == ""
puts "err: API_URL environment variable is not set"
exit 1
end
begin
resp = HTTP.post(url, json: { text: "hi" })
rescue => e
puts "err: #{e.message}"
exit 1
end
puts resp.body.to_s
SDK-style libraries have costs. They may need to be upgraded to patch security issues or resolve competing requirements in the dependency graph. They can be slow to be updated or become unmaintained. They are an additional interface for the team to learn.
When a security review motivates replacing an SDK, the rewrite is bounded by the endpoints the app actually calls. Most of an SDK's weight is code the app never used.
In the above example,
I used the HTTP gem.
I preferred it to the Ruby standard library net/http's interfaces
but ideally, I'd have used the standard library,
such as this Go version of the same program (see go / http clients):
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
url := os.Getenv("API_URL")
if url == "" {
log.Fatalln("err: API_URL environment variable is not set")
}
reqBody, err := json.Marshal(map[string]string{
"text": "Hello, world!",
})
if err != nil {
log.Fatalf("err: %v\n", err)
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(reqBody))
if err != nil {
log.Fatalf("err: %v\n", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("err: %v\n", err)
}
fmt.Println(string(respBody))
}
As I used an API, I built up my custom client as needed. For example, I might add retries with exponential backoff.
Case studies
Four clients I built this way, each with a lesson worth keeping.
Cloudflare Images: retries and untrusted input
The upload client retried transient 429/502/503/504 responses
with a short backoff (DELAYS = [1, 2, nil], kept under Rack's
15s request timeout). Image uploads are idempotent, so retrying
is safe.
Uploads that fetch an attacker-supplied URL first passed through
a shared SSRF guard, which resolved the host and rejected any
private, loopback, link-local, or multicast address before the
HTTP client connected. That stopped a URL like
http://169.254.169.254/latest/meta-data/ from reaching cloud
metadata.
Content type came from the bytes, not the Content-Type header:
private def detect_content_type(body)
bytes = body.bytes.take(4)
if bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF
"image/jpeg"
elsif bytes == [0x89, 0x50, 0x4E, 0x47]
"image/png"
elsif bytes[0] == 0x47 && bytes[1] == 0x49 && bytes[2] == 0x46
"image/gif"
end
end
Postmark: authenticating an inbound webhook
Outbound email was a single POST /email. The interesting half
was inbound: Postmark posts parsed emails to a webhook, so it
needed two layers of authentication.
Basic auth covered transport, compared with
Rack::Utils.secure_compare to avoid timing attacks and coerced
with .to_s so a malformed credential returned the same 401 as
a wrong password. DKIM authenticated the message itself:
ok = auth_results.any? do |v|
v.match?(/\bdkim=pass\b/i) &&
v.match?(/\b(?:header\.)?d=example\.com\b/i)
end
Postmark retries failed webhooks up to 10 times, so the handler recorded every message ID on first arrival and rejected duplicates.
WorkOS: OAuth without the SDK
Four endpoints covered SSO, audit logs, and AuthKit. JWT verification stayed in my own code via the jwt gem.
The login handler stashed a CSRF state token in the session
and the callback rejected any mismatch. The AuthKit bridge added
a consent POST so an attacker could not silently bind their
identity to a logged-in victim. The completion call returned a
redirect URL from WorkOS, checked before use:
uri = URI.parse(redirect_uri.to_s)
if uri.scheme != "https" || !uri.host&.end_with?(".workos.com", ".authkit.app")
return redirect_to("/login")
end
Sentry: error capture without the SDK
The sentry-ruby gem is large: APM tracing, profiling,
breadcrumbs, session tracking, and dozens of integrations. I
used only error capture, which a thin client covers in a few
hundred lines:
Sentry.init(dsn: ENV["SENTRY_DSN"])
Sentry.capture_exception(err, extra: {...}, fingerprint: [...])
Sentry.capture_message("did something weird", extra: {...})
capture_* is a no-op until init runs with a non-empty DSN,
so tests stay quiet without stubbing. A DSN
(https://{key}@{host}/{project_id}) is parsed once at boot to
build the envelope
endpoint and signed auth header.
Capture runs on the error path, so events go onto a bounded
SizedQueue that a background thread POSTs with retries. A
Process.pid check makes the worker fork-safe under Puma, and a
full queue drops events rather than blocking the caller. A
failed Sentry must never break the request.
Request params ride along with each event, so a POST /login
would otherwise ship the password. Filter at the call site,
since the client only sends what callers hand it:
SENSITIVE_KEY_FRAGMENTS = %w(email password secret token).freeze
SENSITIVE_EXACT_KEYS = Set.new(%w(code)).freeze
Substring matching catches current_password and any *_token;
exact matching redacts the OAuth code while keeping
country_code readable. Keeping passwords, OAuth codes, and PII
off a third-party host is the point. The gem's auto-instrumented
breadcrumbs (request params, SQL, headers) are a steady PII leak
that this client simply never emits.