RatMinimal web framework

HTTP

Five members on the http namespace for outbound requests: http.get, http.post, http.put, http.delete, and http.request for arbitrary methods. Each returns a uniform response object with ok, status, headers, body, and error. Branch on ok, and read status and body when you need them. All calls share one connection pool, and arrays or objects passed as the body are auto-JSON-encoded with a Content-Type: application/json header.

http.get: fetch a URL

Server-side only; pure data shape

http.get(url, opts?) issues a GET request and returns the response object. opts is optional and accepts timeout (ms), headers (object), follow_redirects (bool), and body (rare for GET). Default timeout is 30 seconds.

> server
resp: http.get('https://api.github.com/repos/anthropics/anthropic-sdk-python')
ok: resp.ok
status: resp.status

> page
<p> [status] / ok? [ok]

Response shape

Uniform regardless of method or status

Every response object has exactly five fields. ok is true when status is between 200 and 399. error is non-empty only when the transport itself failed (DNS, timeout, broken connection), and for those failures status is 0. The body is always a string; call json.parse(resp.body) when you expect JSON.

# resp shape - five fields:
#   ok:      true                   (status < 400)
#   status:  200                    (0 on transport failure)
#   headers: {content-type: '...'}  (lower-cased keys)
#   body:    'raw bytes as string'
#   error:   ''                     (non-empty on transport failure)

http.post: JSON body

Object or array body → automatic JSON encoding

Passing an object or array as the body is the common case. Rat marshals it with encoding/json and sets Content-Type: application/json for you, unless you override the header. Pass a string body verbatim when you need form-encoded or pre-built payloads.

> server
resp: http.post('https://httpbin.org/post', {hello: 'rat'})

> page
<p> echo'd back: [resp.body]

Custom headers and timeout

Third arg = opts object

The opts object on POST / PUT goes in the third slot (after URL and body); on GET / DELETE / request it's the second slot. headers merges with the defaults; explicit Content-Type wins over the auto-set one.

> server
resp: http.post( 'https://api.example.com/items', {name: 'rat'}, { timeout: 5000, headers: {Authorization: 'Bearer ' + api_token}, }, )

http.request: arbitrary method

For PATCH, HEAD, OPTIONS, etc.

http.request(method, url, opts?) takes the method as a string. The body goes inside opts.body rather than a positional slot, so the call shape stays uniform for verbs that don't conventionally carry a body.

> server
resp: http.request('PATCH', 'https://api.example.com/items/1', { body: {name: 'updated'}, headers: {Authorization: 'Bearer ' + api_token}, })

Disabling redirect-following

follow_redirects: false to stop at 3xx

By default http follows redirects up to the standard net/http limit. Setting follow_redirects: false returns the 3xx response untouched, which is useful when you want to inspect the Location header without chasing it.

> server
resp: http.get('https://example.com/old-path', {follow_redirects: false})
next: resp.headers['location']

Composing with json

JSON in, JSON out

The standard pattern: pass an object body (auto-encoded), parse the response body. The connection pool is reused for repeat calls to the same host, so successive requests are cheap.

> server
resp: http.post('https://api.example.com/echo', {hello: 'rat'})
data: json.parse(resp.body)

> page
<p> server saw: [data.hello]

SSRF guard in prod mode

Refuses loopback / link-local / RFC1918 targets

When RAT_MODE=prod, every outbound call goes through a Server-Side Request Forgery gate. The scheme must be http or https. The resolved IP must not be loopback (127/8, ::1), link-local (169.254/16, including the cloud metadata endpoint), or RFC1918 private (10/8, 172.16/12, 192.168/16). The check fires three times per request: at parse time, at connect time (closing the DNS-rebinding window), and on every redirect hop. Dev mode keeps loopback open so authors can hit their own services. Blocked requests come back as {ok: false, status: 0, error: 'ssrf: ...'}, the same shape as any transport failure, so handler code needs no special branch.

> server
# In prod, this returns an error response without dialing:
resp: http.get('http://169.254.169.254/latest/meta-data/')
ok: resp.ok
err: resp.error

See also JSON · Public endpoints · Encoding