lib/http.bx

HTTP/1.1 over the sockets net.bx already opens.

use "lib/http.bx";

Why this is Burxt’s and not a framework’s. A request arriving on a socket is untrusted bytes becoming typed values, and that boundary is the one thing this language exists to make checkable: an HttpRequest whose fields carry contracts is a fact burxt review can diff between versions. Put the parser in a framework and the interesting property leaves the type system. It is the same call lib/html.bx got when star-burxt moved to its own repository — one escaping implementation, in the place where being right is checkable — and lib/cgi.bx is the precedent: HTTP protocol has lived in the standard library since it existed. Routing a request to a page is MEANING, and that belongs to whoever is building the framework.

Nothing here is new capability. net_listen, net_accept, net_connect_ipv4, net_read and net_write have all worked since v1.1.0, and cgi.bx already percent-decodes. What was missing was the framing in between: a method, a path, headers, a status line. A server written without it takes an opaque String and answers a document, which is a renderer.

—- what this slice does NOT do, named rather than discovered ———————————

  • No chunked transfer encoding. A body is read by Content-Length or not at all. A request
 with `Transfer-Encoding: chunked` is REFUSED by name rather than half-read, because a body
 silently truncated is the wrong answer this language exists to avoid.
  • No DNS, so a client takes four octets. getaddrinfo already returns 0 from a Burxt
 program; what is missing is one builtin to read a pointer back out of C memory. See
 `docs/limitations.md`.
  • No TLS here. lib/tls.bx wraps that, and an https client goes through it. * No keep-alive. One request, one response, one connection. Connection: close is sent.

What is in it

| Name | Kind | What it answers | |—|—|—| | HttpHeader | class | Prefixed by hand, like every other name in this library. cgi.bx already has a Request and use concatenates sources | | HttpRequest | class | A request, parsed. target is what arrived on the wire; path and query are it split, with the path percent-decoded | | HttpResponse | class | — | | Handler | interface | What a server does with a request. The interface IS the handler, because Burxt has no function values — and dynamic Han | | [http_parse_request](#http-parse-request) | function | The wire format, or a refusal that says which part was wrong. | | [http_header](#http-header) | function | A header by name, case-insensitively, because the wire does not agree on capitalisation and a lookup that did would find | | [http_content_length](#http-content-length) | function | How many bytes of body the headers say to expect. None when the header is absent or is not a number — the caller decid | | [http_query_params](#http-query-params) | function | The query string, decoded into name/value pairs. Uses cgi.bx's decoder rather than a second one: two percent-decoders | | [http_form_params](#http-form-params) | function | A form-encoded body, decoded the same way. | | [http_response](#http-response) | function | — | | [http_html](#http-html) | function | — | | [http_text](#http-text) | function | — | | [http_json](#http-json) | function | — | | [http_not_found](#http-not-found) | function | — | | [http_with_header](#http-with-header) | function | One more header on a response. Answers a NEW response rather than mutating, so a handler can build one from another with | | [http_render_response](#http-render-response) | function | The bytes to put on the socket. | | [http_parse_response](#http-parse-response) | function | A response read back from a server, for the client half. | | [http_response_header](#http-response-header) | function | — | | [http_read_request](#http-read-request) | function | Read one request off an accepted connection. | | [http_send_response](#http-send-response) | function | — | | [http_serve_one](#http-serve-one) | function | Serve one connection: read, hand to the handler, answer, close. Answers whether a response was written, so a caller's lo | | [http_serve](#http-serve) | function | Listen, then serve until something stops answering. None when the port could not be taken — which is the one failure a | | [http_send_request](#http-send-request) | function | One request to a server at a known address, and the response. | | [http_get](#http-get) | function | The common case. | | [http_post`](#http-post) | function | A POST with a body and a content type. |

Types

HttpHeader

class HttpHeader { name: String, value: String }

Prefixed by hand, like every other name in this library. cgi.bx already has a Request and use concatenates sources into one buffer, so there is nothing to be namespaced apart — see docs/limitations.md on the absence of a registry and of namespacing. Param is deliberately NOT redefined here: it comes from cgi.bx and one decoded name/value pair should be one type.

Source

HttpRequest

class HttpRequest

A request, parsed. target is what arrived on the wire; path and query are it split, with the path percent-decoded and the query left encoded because http_query is what decodes it — decoding before splitting is how a %3D in a value becomes an extra =.

Source

HttpResponse

class HttpResponse

Source

Handler

interface Handler

What a server does with a request. The interface IS the handler, because Burxt has no function values — and dynamic Handler is one in all but name, which is the recorded decision rather than a workaround (docs/limitations.md, “No closures”). lib/fn.bx’s Mapper and Predicate are the precedent.

A caller binds its handler to a variable before passing it: an interface object borrows the storage it refers to, so a literal has nowhere to be borrowed from, and the compiler says so.

Source

Functions

http_parse_request

pure function http_parse_request(text: String) -> Result<HttpRequest, String> allocates

The wire format, or a refusal that says which part was wrong.

Every failure names the part rather than the whole: a reader who gets “no request line” and a reader who gets “no blank line after the headers” have different problems, and one message for both sends one of them to the wrong place.

Source

http_header

pure function http_header(request: HttpRequest, name: String) -> Option<String>

A header by name, case-insensitively, because the wire does not agree on capitalisation and a lookup that did would find Content-Length and miss content-length.

Source

http_content_length

pure function http_content_length(request: HttpRequest) -> Option<Int>

How many bytes of body the headers say to expect. None when the header is absent or is not a number — the caller decides whether that means zero or means refuse.

Source

http_query_params

pure function http_query_params(request: HttpRequest) -> Option<[Param]> allocates

The query string, decoded into name/value pairs. Uses cgi.bx’s decoder rather than a second one: two percent-decoders is two chances to disagree about +.

Source

http_form_params

pure function http_form_params(request: HttpRequest) -> Option<[Param]> allocates

A form-encoded body, decoded the same way.

Source

http_response

pure function http_response(status: Int, content_type: String, body: String) -> HttpResponse allocates

Source

http_html

pure function http_html(body: String) -> HttpResponse allocates

Source

http_text

pure function http_text(body: String) -> HttpResponse allocates

Source

http_json

pure function http_json(body: String) -> HttpResponse allocates

Source

http_not_found

pure function http_not_found() -> HttpResponse allocates

Source

http_with_header

pure function http_with_header(response: HttpResponse, name: String, value: String) -> HttpResponse allocates

One more header on a response. Answers a NEW response rather than mutating, so a handler can build one from another without the caller wondering which of the two changed.

Source

http_render_response

pure function http_render_response(response: HttpResponse) -> String allocates

The bytes to put on the socket.

cgi_status_text supplies the code AND the reason — it answers "200 OK", not "OK" — which is right for CGI’s Status: header and is the whole line here. Writing the code again in front of it produced HTTP/1.1 200 200 OK, and it was invisible until the bytes were printed: both halves were correct and the composition was not. Reusing it keeps the two transports from disagreeing about what 404 is called, which is worth the sharp edge as long as it is named.

Source

http_parse_response

pure function http_parse_response(text: String) -> Result<HttpResponse, String> allocates

A response read back from a server, for the client half.

Source

http_response_header

pure function http_response_header(response: HttpResponse, name: String) -> Option<String>

Source

http_read_request

function http_read_request(fd: Int, limit: Int) -> Result<HttpRequest, String>

Read one request off an accepted connection.

Two reads, and the second is what makes a body arrive. net_read answers what one recv gave, which for a request with a body is usually the head and some of it. So: read until the blank line is in hand, then keep reading until Content-Length bytes of body have arrived. A server that read once and parsed would work on every hand-typed request and truncate every real POST — the class of bug that passes a demo and fails a form.

Source

http_send_response

function http_send_response(fd: Int, response: HttpResponse) -> Bool allocates touches network

Source

http_serve_one

function http_serve_one(server: Int, handler: dynamic Handler, limit: Int) -> Bool

Serve one connection: read, hand to the handler, answer, close. Answers whether a response was written, so a caller’s loop can count failures rather than guess.

A malformed request is answered with 400 rather than dropped. A client that gets nothing back cannot tell a rejection from a hang, and “the server closed on me” is the least actionable thing a server can say.

Source

http_serve

function http_serve(port: Int, handler: dynamic Handler, limit: Int) -> Option<Int>

Listen, then serve until something stops answering. None when the port could not be taken — which is the one failure a caller can act on, by choosing another.

No fork here on purpose. os_fork exists and a pre-forked server is written with it, but how many workers and how they are supervised is an application’s decision, not a protocol’s. This serves one connection at a time and says so; http_serve_one is what a forking loop calls.

Source

http_send_request

function http_send_request(a: Int, b: Int, c: Int, d: Int, port: Int, host: String,

One request to a server at a known address, and the response.

Four octets rather than a hostname, and that is a language gap rather than a design choice. getaddrinfo already returns 0 from a Burxt program; what is missing is one builtin to read a pointer back out of C memory, so the addrinfo chain cannot be walked. docs/limitations.md carries the measurement. When that builtin lands this grows a http_get_host beside it and this function does not change.

host is still needed because HTTP/1.1 requires a Host: header and a virtual host answers by it — so the name travels even when the address does not come from it.

Source

http_get

function http_get(a: Int, b: Int, c: Int, d: Int, port: Int, host: String, path: String)

The common case.

Source

http_post

function http_post(a: Int, b: Int, c: Int, d: Int, port: Int, host: String, path: String,

A POST with a body and a content type.

Source