Module: net.http

Source: ./net/http.reef


Overview

net.http - HTTP/1.1 client with HTTPS support

Provides HTTP client functionality for making GET and POST requests. Supports both HTTP and HTTPS (TLS/SSL) connections with configurable timeouts. Automatically handles chunked transfer encoding (RFC 7230).

RECOMMENDED API (handles both text and binary safely):

  • http_get_auto(url) - auto-detects content type, returns HttpAutoResponse
  • http_auto_body_string(resp) - get body as text (for text content)
  • http_auto_body_bytes(resp) - get body as bytes (for binary content)
  • http_auto_is_binary(resp) - check if content is binary

For streaming large files to disk:

  • http_download_file(url, path) - stream directly to disk (memory efficient)

LEGACY TEXT-ONLY API (do not use for binary data - will truncate at null bytes):

  • http_get(url) - returns HttpResponse with string body
  • http_response_body(resp) - returns string (truncates binary data!)

Types

HttpUrl

HTTP URL components

Fields:

Name Type
scheme string
host string
port int
path string
query string

HttpRequest

HTTP Request

Fields:

Name Type
method string
url HttpUrl
headers [string]
header_count int
body string

HttpResponse

HTTP Response NOTE: no error field -- a completed transaction (including 4xx/5xx) is Ok(HttpResponse); only a transport failure (connect/send/recv) is Err.

Fields:

Name Type
status_code int
status_text string
headers [string]
header_count int
body string

HttpBinaryResponse

HTTP Binary Response (for binary data that may contain null bytes) NOTE: no error field -- see HttpResponse.

Fields:

Name Type
status_code int
status_text string
headers [string]
header_count int
body_bytes [uint8]
body_length int

HttpAutoResponse

HTTP Auto Response (safe for both text and binary - recommended) Stores body as bytes internally, provides accessors for both formats NOTE: no error field -- see HttpResponse.

Fields:

Name Type
status_code int
status_text string
headers [string]
header_count int
body_bytes [uint8]
body_length int
content_type string
is_binary bool

HttpDownloadProgress

HTTP Download Progress (for progress-reporting downloads)

Fields:

Name Type
total_bytes int
downloaded_bytes int
elapsed_ms int64
speed_bps int
status string
error string

DownloadOutcome (enum)

The four outcomes a download attempt can have.

C16 / ROADMAP 8.2: this was TWO structs. DownloadResult carried success + is_redirect + location + cancelled + error -- three booleans for four outcomes, so success && is_redirect && cancelled was representable and meant nothing. It was a sum type written as a product, and every consumer already destructured it as one, with an if/elif chain.

DownloadResultVerbose was the same shape plus bytes_received and content_length. It is gone too: bytes_received was WRITE-ONLY (ten writes, zero reads) and content_length was producer-internal -- a loop guard, and a value copied into HttpDownloadProgress. Neither was outcome data.

Redirected always carries a USABLE location: a 3xx whose Location header is missing or empty is reported as Failed by the producer, which is the only place that can tell. That is why the variant's payload needs no guard at the call site.

Variants:

  • Succeeded
  • Redirected(string)
  • Cancelled
  • Failed(string)

ChunkedResult

Chunked decoding result type

Fields:

Name Type
data [uint8]
length int

Functions

fn reef_string_alloc(len: int): string

FFI declaration for string allocation

fn http_parse_url(url: string): option.Option[HttpUrl]

============================================================================ URL Parsing

Parse a URL into its components, or None if it is not a URL.

C16 / ROADMAP 8.2: this returned an HttpUrl carrying an is_valid: bool, which is a Maybe spelled as a struct field. Two of the six call sites in this module -- both redirect paths -- never read the flag, so a redirect to a malformed Location was parsed into a zero-host HttpUrl and used. An Option cannot be used without being opened.

fn parse_int(s: string): option.Option[int]

Simple int parser 0.9.2 C16: parse a decimal integer, or None.

This SKIPPED every non-digit and returned 0 for empty input, with no failure channel -- so "80x0" parsed as 800 (the 'x' silently dropped) and ":" and "abc" both gave 0, all while http_parse_url set is_valid = true unconditionally. Measured before the fix: :8080 -> port=8080 :80x0 -> port=800 : -> port=0 :abc -> port=0 A caller could not tell a real port 0 from a parse failure, and the one guard consumers have (is_valid) never covered the port at all.

fn request_for(method: string, parsed: HttpUrl): HttpRequest

Build a request around an already-parsed URL. http_request_new is this plus the parse; the redirect loops below use both halves separately so they parse each URL exactly once.

fn http_request_new(method: string, url: string): option.Option[HttpRequest]

Build a request, or None if the URL does not parse.

C16: because a HttpRequest can now only exist with a parsed URL, the "Invalid URL" guard that http_send_timeout and http_send_binary used to open with is gone -- there is no longer a request that can fail it.

fn http_get(url: string): res.Result[HttpResponse, error.Error]

Perform HTTP GET request Follows HTTP redirects automatically (up to 5 redirects) Ok(HttpResponse) on any completed transaction (status is DATA -- a 404/500 is still Ok); Err(error.Error) only on transport failure.

fn http_post(url: string, body: string): res.Result[HttpResponse, error.Error]

fn http_post_json(url: string, json: string): res.Result[HttpResponse, error.Error]

fn http_get_bytes(url: string): res.Result[HttpBinaryResponse, error.Error]

Download binary data into memory as a byte array Use this for files that may contain null bytes (images, executables, etc.) Follows HTTP redirects automatically (up to 5 redirects)

fn http_download_file(url: string, path: string): res.Result[bool, error.Error]

Download a file directly to disk (streaming, memory-efficient) Follows HTTP redirects automatically (up to 5 redirects) Ok(true) on success; Err(error.Error) on failure, carrying the reason.

fn http_download_progress(url: string, path: string): res.Result[HttpDownloadProgress, error.Error]

Download a file with verbose progress reporting Ok(final HttpDownloadProgress) on complete; Err(error.Error) on transport failure (the progress struct's own status/error fields still carry the same detail for callers who want to inspect it directly, but the Result is now the primary success/failure signal).

fn http_download_verbose_tcp(parsed_url: HttpUrl, request_str: string, path: string, progress: HttpDownloadProgress): DownloadOutcome

Internal: Verbose download over TCP

fn http_download_verbose_tls(parsed_url: HttpUrl, request_str: string, path: string, progress: HttpDownloadProgress): DownloadOutcome

Internal: Verbose download over TLS

fn create_progress(total: int, downloaded: int, start_ms: int64, status: string): HttpDownloadProgress

Helper: Create a progress struct with calculated speed

fn create_error_progress(error_msg: string): HttpDownloadProgress

Helper: Create an error progress struct

fn http_download_file_tls_callback(parsed_url: HttpUrl, request_str: string, path: string, callback: (HttpDownloadProgress) -> bool): DownloadOutcome

Internal: Download file over TLS with progress callback

fn http_download_file_tcp_callback(parsed_url: HttpUrl, request_str: string, path: string, callback: (HttpDownloadProgress) -> bool): DownloadOutcome

Internal: Download file over TCP with progress callback

fn http_download_file_callback(url: string, path: string, callback: (HttpDownloadProgress) -> bool): res.Result[bool, error.Error]

Download a file with progress callback The callback is invoked periodically with download progress updates Return false from callback to cancel the download Follows HTTP redirects automatically (up to 5 redirects) Ok(true) on success; Ok(false) if the callback cancelled the download (user-cancel is NOT an error); Err(error.Error) on transport failure.

fn get_content_length_from_bytes(data: [uint8], header_len: int): int

Helper: Get Content-Length from header bytes Returns -1 when the header is ABSENT -- an out-of-band marker, since a real Content-Length is never negative.

C16 note (reef-lang#53): the digit loop below has no consumed-check, so a PRESENT but malformed header (Content-Length: abc, or an empty value) returns 0 -- indistinguishable from a legitimate Content-Length: 0. That is the shape documented in docs/design/2026-09-04-parse-loops-must-prove-they-consumed-something.md.

It is BENIGN TODAY, and that was measured rather than assumed: every caller treats 0 as "length unknown" and falls into a read-to-EOF accumulator, so a malformed header degrades to the correct-but-O(n^2) path. A witness serving a real body under 'abc', '', '0' and '15' returns the full body in all four cases, on both http_get and http_download_file.

*** THE SAFETY IS CONDITIONAL. *** It holds only because this module speaks HTTP/1.0 with Connection: close, where end-of-stream is always available as framing. Under HTTP/1.1 KEEP-ALIVE the length is the ONLY framing, read-to-EOF stops being an option, and the collision becomes live: a malformed header would frame a zero-length body and leave the real body in the socket, desynchronising the connection for every later request on it.

If you add keep-alive, this function gains a consumed-check and returns Option[int] IN THE SAME CHANGE-SET.

fn http_send(req: HttpRequest): res.Result[HttpResponse, error.Error]

fn http_send_timeout(req: HttpRequest, timeout_ms: int): res.Result[HttpResponse, error.Error]

fn http_send_tcp(host: string, port: int, request_str: string, timeout_ms: int): res.Result[HttpResponse, error.Error]

Internal: Send HTTP request over plain TCP

fn http_send_tls(host: string, port: int, request_str: string): res.Result[HttpResponse, error.Error]

Internal: Send HTTP request over TLS (HTTPS)

fn http_send_binary(req: HttpRequest): res.Result[HttpBinaryResponse, error.Error]

Internal: Send request and get binary response

fn http_send_binary_tcp(host: string, port: int, request_str: string): res.Result[HttpBinaryResponse, error.Error]

Internal: Send binary request over plain TCP

fn http_send_binary_tls(host: string, port: int, request_str: string): res.Result[HttpBinaryResponse, error.Error]

Internal: Send binary request over TLS

fn http_download_file_tcp_internal(parsed_url: HttpUrl, request_str: string, path: string): DownloadOutcome

Internal: Download file over TCP (streaming to disk) Returns a DownloadOutcome; Redirected carries the Location to follow.

fn http_download_file_tls_internal(parsed_url: HttpUrl, request_str: string, path: string): DownloadOutcome

Internal: Download file over TLS (streaming to disk) Returns a DownloadOutcome; Redirected carries the Location to follow.

fn append_bytes(dest: [uint8], dest_len: int, src: [uint8], src_len: int): [uint8]

Append bytes to a byte array (returns new array)

fn find_header_end(data: [uint8], len: int): int

Find \r\n\r\n in byte array (returns position of first \r, or -1)

fn parse_status_from_bytes(data: [uint8], header_len: int): int

Parse HTTP status code from header bytes

fn is_chunked_from_bytes(data: [uint8], header_len: int): bool

Check if Transfer-Encoding: chunked from header bytes

fn matches_transfer_encoding(data: [uint8], pos: int, len: int): bool

Helper: check if bytes match "transfer-encoding:" (case insensitive)

fn is_redirect_status(status: int): bool

Check if status code is a redirect

fn get_location_from_bytes(data: [uint8], header_len: int): string

Extract Location header from response bytes (case-insensitive)

fn resolve_redirect_url(base: HttpUrl, location: string): string

Resolve redirect URL (handles relative and absolute URLs)

fn decode_chunked_bytes(data: [uint8], len: int): ChunkedResult

Decode chunked transfer encoding from byte array

fn parse_hex_from_bytes(data: [uint8], start: int, length: int): option.Option[int]

Parse hex number from byte array 0.9.2 C16: None for a malformed chunk-size field.

This used to return 0 both for "the terminating zero-length chunk" and for "this size line is garbage", because the loop SILENTLY SKIPPED every non-hex byte (digit stayed -1 and nothing rejected it). A corrupted size line therefore parsed as 0 and decode_chunked_bytes read it as end-of-body, truncating the response with no diagnostic. Non-hex bytes are now rejected.

Trailing spaces/tabs before the CRLF are tolerated: technically invalid per RFC 7230, but emitted by enough servers that rejecting them would be a regression against the old skip-everything behaviour.

fn parse_http_response_binary(data: [uint8], len: int): res.Result[HttpBinaryResponse, error.Error]

Parse HTTP binary response from raw bytes

fn bytes_to_string(data: [uint8], start: int, length: int): string

Convert bytes to string (using runtime allocation)

fn parse_hex_int(s: string): option.Option[int]

Parse a hex string to integer 0.9.2 C16: None for a malformed chunk-size string.

String twin of parse_hex_from_bytes, with the same defect: the loop silently skipped non-hex characters, so garbage parsed as 0 and decode_chunked read that as the terminating chunk. This one can be strict -- its only caller has already stripped the ";" chunk extension and trimmed whitespace -- so every character must be a hex digit and an empty string is None, not 0.

fn decode_chunked(data: string): string

Decode chunked transfer encoding Format: chunk-size CRLF chunk-data CRLF ... 0 CRLF CRLF

fn is_chunked_transfer(headers: [string], header_count: int): bool

Check if response uses chunked transfer encoding

fn parse_http_response(data: string): res.Result[HttpResponse, error.Error]

fn http_response_status(resp: HttpResponse): int

fn http_response_body(resp: HttpResponse): string

fn http_response_header(resp: HttpResponse, name: string): option.Option[string]

0.9.2 C16: None when the header is absent.

This returned "" for BOTH "no such header" and a header present with an empty value, and X-Foo: with nothing after the colon is a legal HTTP header. The old inner loop made that worse: on an empty/all-space value it fell out without returning and kept searching, so the header was reported absent. It now returns Some("") the moment the name matches.

fn http_response_header_or(resp: HttpResponse, name: string, default: string): string

fn http_response_is_ok(resp: HttpResponse): bool

fn http_binary_response_status(resp: HttpBinaryResponse): int

fn http_binary_response_body(resp: HttpBinaryResponse): [uint8]

fn http_binary_response_length(resp: HttpBinaryResponse): int

fn http_binary_response_header(resp: HttpBinaryResponse, name: string): option.Option[string]

0.9.2 C16: None when the header is absent.

This returned "" for BOTH "no such header" and a header present with an empty value, and X-Foo: with nothing after the colon is a legal HTTP header. The old inner loop made that worse: on an empty/all-space value it fell out without returning and kept searching, so the header was reported absent. It now returns Some("") the moment the name matches.

fn http_binary_response_header_or(resp: HttpBinaryResponse, name: string, default: string): string

fn http_binary_response_is_ok(resp: HttpBinaryResponse): bool

fn http_get_auto(url: string): res.Result[HttpAutoResponse, error.Error]

Perform HTTP GET with automatic content-type detection Stores body as bytes internally, safe for both text and binary Follows HTTP redirects automatically (up to 5 redirects)

fn http_get_auto_tcp(host: string, port: int, request_str: string): res.Result[HttpAutoResponse, error.Error]

Internal: Auto HTTP over TCP

fn http_get_auto_tls(host: string, port: int, request_str: string): res.Result[HttpAutoResponse, error.Error]

Internal: Auto HTTP over TLS

fn parse_http_auto_response(data: [uint8], len: int): res.Result[HttpAutoResponse, error.Error]

Parse HTTP auto response from raw bytes

fn is_binary_content_type(content_type: string): bool

Check if content-type indicates binary data

fn http_auto_status(resp: HttpAutoResponse): int

fn http_auto_body_string(resp: HttpAutoResponse): string

Get body as string (safe for text content) Warning: For binary content, this may truncate at null bytes

fn http_auto_body_bytes(resp: HttpAutoResponse): [uint8]

Get body as bytes (safe for all content)

fn http_auto_body_length(resp: HttpAutoResponse): int

fn http_auto_header(resp: HttpAutoResponse, name: string): option.Option[string]

0.9.2 C16: None when the header is absent.

This returned "" for BOTH "no such header" and a header present with an empty value, and X-Foo: with nothing after the colon is a legal HTTP header. The old inner loop made that worse: on an empty/all-space value it fell out without returning and kept searching, so the header was reported absent. It now returns Some("") the moment the name matches.

fn http_auto_header_or(resp: HttpAutoResponse, name: string, default: string): string

fn http_auto_content_type(resp: HttpAutoResponse): string

fn http_auto_is_binary(resp: HttpAutoResponse): bool

fn http_auto_is_ok(resp: HttpAutoResponse): bool


Procedures

proc http_request_set_header(req: HttpRequest, name: string, value: string)

proc http_request_set_body(req: HttpRequest, body: string)

proc copy_bytes(dest: [uint8], dest_offset: int, src: [uint8], src_offset: int, length: int)

Copy bytes from source to destination


Generated by reefc doc