ruby / http clients

I prefer code like this to 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

An SDK has costs. I must upgrade it for security patches and dependency conflicts. Each one is another interface to learn. When I replace an SDK, I rewrite only the endpoints the app calls.

The example uses the HTTP gem because net/http has a worse interface. The Go version uses only the standard library (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))
}

I add to a client as I need, for example retries with exponential backoff.

Case studies

Cloudflare Images: retries and untrusted input

The upload client retried 429/502/503/504 responses with a short backoff (DELAYS = [1, 2, nil], under Rack's 15s request timeout). Uploads are idempotent, so a retry is safe.

Uploads that fetch a user-supplied URL first passed an SSRF guard. The guard resolved the host and rejected private, loopback, link-local, and multicast addresses. That blocked http://169.254.169.254/latest/meta-data/.

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 one POST /email. Postmark posts inbound emails to a webhook, which needed two layers of authentication.

Basic auth covered transport. I compared credentials with Rack::Utils.secure_compare. I coerced them with .to_s, so a malformed credential returned the same 401 as a wrong password. DKIM authenticated the message:

ok = auth_results.any? do |v|
  v.match?(/\bdkim=pass\b/i) &&
    v.match?(/\b(?:header\.)?d=example\.com\b/i)
end

Postmark retries a failed webhook up to 10 times, so the handler recorded each message ID and rejected duplicates.

WorkOS: OAuth without the SDK

Four endpoints covered SSO, audit logs, and AuthKit. The jwt gem verified JWTs.

The login handler stored a CSRF state token in the session. The callback rejected a mismatch. The AuthKit bridge added a consent POST so an attacker could not bind their identity to a logged-in victim. The handler checked the redirect URL from WorkOS 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 includes tracing, profiling, breadcrumbs, and many integrations. I used only error capture, which fits 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 DSN, so tests need no stubs. The client parses the DSN (https://{key}@{host}/{project_id}) once at boot to build the envelope endpoint and auth header.

Events go onto a bounded SizedQueue. A background thread POSTs them with retries. A Process.pid check makes the worker fork-safe under Puma. A full queue drops events so a Sentry failure never blocks the request.

Each event carries request params, so a POST /login would send the password. The caller filters params:

SENSITIVE_KEY_FRAGMENTS = %w(email password secret token).freeze
SENSITIVE_EXACT_KEYS = Set.new(%w(code)).freeze

Substring matching catches current_password and *_token. Exact matching redacts the OAuth code and keeps country_code. The gem's breadcrumbs (request params, SQL, headers) leak PII, and this client never emits them.

← All articles