RFC 066: std.http — Incan-first HTTP client and request/response surface¶
- Status: Draft
- Created: 2026-04-16
- Author(s): Danny Meijer (@dannymeijer)
- Related:
- RFC 022 (namespaced stdlib modules and compiler handoff)
- RFC 023 (compilable stdlib and Rust module binding)
- RFC 037 (native web stdlib redesign)
- RFC 051 (
JsonValueforstd.json) - RFC 055 (
std.fspath-centric filesystem APIs) - RFC 063 (
std.processprocess spawning and command execution)
- Issue: https://github.com/dannys-code-corner/incan/issues/84
- RFC PR: —
- Written against: v0.2
- Shipped in: —
Summary¶
This RFC proposes std.http as Incan's standard library module for explicit HTTP client work. The module standardizes a request or response model, one-shot and client-based request APIs, timeout and retry policy, structured errors, and JSON convenience surfaces so ordinary programs, tools, and automation workflows do not need to fall through to rust::reqwest-shaped APIs or ad hoc wrappers.
Core model¶
Read this RFC as one foundation plus three mechanisms:
- Foundation: HTTP is a general-purpose stdlib capability, not a CI-only or framework-only helper surface.
- Mechanism A:
std.httpprovides explicitRequest,Response,Body,Method, andHttpErrortypes with predictable behavior and no panic-driven network contract. - Mechanism B: the module supports both one-shot convenience helpers and a reusable
Clientsurface so simple scripts and heavier integrations share one coherent model. - Mechanism C: JSON, timeout, redirect, and retry behavior remain explicit policy surfaces rather than ambient magic.
Motivation¶
Networking is a recurring boundary for ordinary Incan programs: API clients, release tooling, CI automation, ingestion pipelines, service-to-service calls, health checks, and migration scripts all need HTTP. Today, the practical escape hatch is Rust interop. That works, but it leaks Rust-shaped APIs, Rust-shaped errors, and inconsistent conventions into user code precisely where the standard library should provide one stable model.
This matters for more than ergonomics. HTTP boundaries are policy-heavy: timeouts, retries, redirect handling, header redaction, JSON decoding, and error reporting all need explicit behavior. If every project rebuilds these choices differently, the language ends up with a fragmented story for one of the most common integration surfaces.
std.http should therefore do for network requests what std.fs, std.process, and the newer stdlib RFCs are doing in their domains: define an Incan-first contract while still allowing the runtime to map onto Rust-native implementations underneath.
Goals¶
- Provide a first-class
std.httpmodule for client-side HTTP work. - Standardize explicit request and response types rather than centering shell calls or Rust interop.
- Keep timeout behavior first-class and non-ambient.
- Define a structured
HttpErrormodel so network failures, status failures, timeout failures, decoding failures, and policy failures are distinguishable. - Provide JSON convenience helpers that compose cleanly with RFC 051
JsonValue. - Support both one-shot request helpers and a reusable
Clientsurface. - Make retry behavior explicit and policy-shaped rather than automatic and invisible.
- Require safe default treatment of sensitive headers in diagnostics and debug-facing representations.
Non-Goals¶
- Defining server-side HTTP routing or handler APIs here; that belongs with RFC 037 and related web-platform work.
- Shipping a full browser fetch surface in this RFC; browser-oriented HTTP behavior may reuse the same contract later but is not defined here.
- Making HTTP a language intrinsic or keyword surface.
- Introducing a GitHub- or cloud-specific SDK into the standard library.
- Standardizing cookies, OAuth flows, multipart forms, WebSockets, or HTTP/3-specific behavior in the first version.
Guide-level explanation¶
One-shot requests¶
For simple scripts, users should be able to write:
from std.http import get
response = get("https://api.example.com/health", timeout=5s)?
text = response.text()?
The important point is not the exact helper spelling. The important point is that ordinary request code stays inside std.http, uses Result[..., HttpError], and does not require dropping into rust::.
Explicit requests¶
For more control, users should be able to build a request directly:
from std.http import Body, Method, Request, send
request = Request(
method=Method.POST,
url="https://api.example.com/events",
headers={"Authorization": token, "Content-Type": "application/json"},
body=Body.json(payload),
timeout=10s,
)
response = send(request)?
This makes policy visible:
- the method is explicit
- the body is explicit
- the timeout is explicit
- the caller chooses whether to inspect status, body bytes, text, or JSON
Reusable clients¶
For workflows that share headers, retries, or transport settings, users should be able to use a Client:
from std.http import Client, RetryPolicy
client = Client(
default_headers={"Authorization": token},
timeout=15s,
retry=RetryPolicy.transient(max_attempts=3),
)
response = client.get("https://api.example.com/items")?
items = response.json()?
This does not change the basic model. It only moves repeated policy into one reusable value.
Status handling should stay explicit¶
The response model should not hide status behavior behind panics. Users should opt into strict status expectations:
response = client.get(url)?
response = response.require_success()?
data = response.json()?
or branch explicitly:
response = client.get(url)?
if response.status.is_success:
return response.json()?
else:
return Err(HttpError.unexpected_status(response.status))
Sensitive data should not print carelessly¶
Headers such as Authorization should be redactable in debug-facing output by default:
println(request)
should not casually dump bearer tokens or secrets into logs.
Reference-level explanation¶
Module surface¶
std.http must provide, at minimum:
MethodBodyRequestResponseStatusCodeHttpErrorClient- one-shot request helpers or a functionally equivalent request entry surface
- explicit retry-policy types if retry behavior is part of the request contract
The exact spelling of all helpers is part of the module API, but the contract is that the user-facing model is request- and response-centric rather than shell-centric or backend-centric.
Request model¶
A Request must carry:
- method
- URL
- headers
- query parameters if modeled separately from the URL
- body
- timeout policy
- redirect policy if separately configurable
- retry policy when the caller opts into retries
A request must be constructible without requiring a Client.
Response model¶
A Response must expose:
- status code
- response headers
- body bytes
- helpers for decoding text and JSON
A response must not silently panic on unsuccessful status codes. Status-based failure should remain explicit through helpers such as require_success() or equivalent APIs.
Error model¶
std.http operations must return Result[..., HttpError].
HttpError must distinguish at least:
- connection failures
- timeout failures
- redirect-policy failures
- TLS or transport failures
- decode failures
- explicit status-policy failures
The module may include richer variants, but it must not collapse all failures into one undifferentiated string.
Timeouts¶
Timeouts must be first-class and explicit. The contract must define:
- how request timeouts are attached
- whether a client-level timeout can be overridden per request
- what error variant a timeout produces
This RFC intentionally does not hardcode one exact default timeout yet; see unresolved questions.
Retries¶
Retries must be opt-in and policy-shaped. A retry policy may cover:
- maximum attempts
- backoff strategy
- which status codes are retryable
- which transport failures are retryable
The module must not silently retry every request by default.
JSON integration¶
Body.json(value) or an equivalent API may accept JsonValue and, where later RFCs standardize model-oriented JSON encoding, other serializable values.
Response.json() must decode into JsonValue at minimum. Typed decode into models may be added through compatible follow-up RFCs, but this RFC's floor is a coherent JsonValue path.
Redaction and debug-facing behavior¶
Implementations should redact sensitive header values such as Authorization, Proxy-Authorization, and similarly sensitive token-bearing headers in debug-facing request or response displays.
The public contract does not need to prescribe every redacted header name exhaustively in v1, but it must require that sensitive-header treatment is conservative and documented.
Design details¶
Syntax¶
This RFC does not require new language syntax. It is a namespaced stdlib surface.
Semantics¶
The semantic center is explicit network behavior:
- request creation is explicit
- timeout policy is explicit
- retry policy is explicit
- status handling is explicit
- failures are structured
The module should not rely on hidden ambient globals for client state, retry behavior, or timeout behavior.
Interaction with existing features¶
- RFC 051 (
JsonValue): JSON request and response helpers should compose withJsonValueas the baseline dynamic JSON type. - RFC 055 (
std.fs): file uploads or downloads may later compose with path or file surfaces, but this RFC does not require multipart or streaming file-transfer APIs. - RFC 063 (
std.process): HTTP should remain a direct network API, not a wrapper over shelling out tocurl. - RFC 037 (native web stdlib redesign): this RFC covers client-side HTTP. Server-side web contracts remain separate even if they eventually share types such as methods or status codes.
Compatibility / migration¶
This feature is additive. Existing Rust-interop HTTP wrappers remain valid, but the design claim is that new code, docs, and examples should prefer std.http once it exists.
Alternatives considered¶
- Rust interop only
- Rejected because it leaves a common boundary with Rust-shaped APIs, Rust-shaped errors, and inconsistent conventions.
- Shell out to
curl - Rejected because it weakens safety, portability, and structured error handling.
- Only one-shot helpers, no
Client - Rejected because real tooling and API clients need reusable policy and shared headers.
- Only
Client, no one-shot helpers - Rejected because it makes simple scripts too ceremonious.
Drawbacks¶
- HTTP is a deceptively broad domain, and the API can sprawl if the module tries to cover every advanced transport concern immediately.
- Timeout, retry, redirect, and status behavior need very careful wording or users will make conflicting assumptions.
- Redaction rules and debug output need discipline or the module will create accidental secret leakage.
Implementation architecture¶
(Non-normative.) A practical implementation likely uses a Rust-native HTTP stack underneath, but the public contract should remain request- and response-shaped. A sensible rollout would start with one-shot requests, explicit request objects, reusable clients, structured errors, timeouts, and JsonValue helpers before expanding into richer transport features such as multipart, streaming bodies, or cookie persistence.
Layers affected¶
- Stdlib / runtime: must provide the request, response, method, body, client, and error surfaces promised by this RFC.
- Language surface: the module and its helper types must be available as specified.
- Execution handoff: implementations must preserve timeout, retry, status, and decoding semantics without leaking backend-specific APIs as the public contract.
- Docs / tooling: examples and documentation must standardize safe defaults, explicit status handling, and redaction expectations.
Unresolved questions¶
- Should
std.httpexpose a default timeout at the module or client level, or should callers be required to choose one explicitly? - Should
Response.json()standardize onlyJsonValuedecoding in this RFC, or should typed model decoding be part of the base contract too? - Which redirect policy should be the default: follow a bounded number of redirects, or require explicit opt-in?
- Should retry policies live on
Request,Client, or both? - How much of cookie handling belongs in the initial contract versus a follow-up RFC?