http.bend checks
raw source on the hub · import 0x0a372da4a053652f70ded7d6e0d19330/http.bend as Http
ezhttp/http: HTTP/1.1 message text for client and server — request and response formatting/parsing — with no sockets. Semantics follow RFC 9110; message syntax and framing follow RFC 9112. TLS is outside this module.
2 imports
import Base import ./body.bend as Body
Types
type Cut source · line 8 · raw
Data
a string divided at the first occurrence of a pattern, or not divided at all
NoCutCut
Cut@before:String -> @after:String -> Cut
type Header source · line 13 · raw
Data
one header field name and value (RFC 9110 §5 / RFC 9112 §5)
H@name:String -> @value:String -> Header
type Reply source · line 17 · raw
Data
what a response turned out to be: status, headers, framed body — or why not
Torn@why:String -> Reply
Reply@status:U32 -> @headers:List<&2, Header> -> @body:String -> Reply
type Next source · line 218 · raw
Data
the next scan step, or why this chunk header is not usable
StopNext
Go@cut:Cut -> Next
Reject@why:String -> Next
type Request source · line 655 · raw
Data
a request that came apart, or the reason it would not (RFC 9112 §3)
Bad@why:String -> Request
Request@method:String -> @target:String -> @headers:List<&2, Header> -> @body:String -> Request
Definitions
def first source · line 22 · raw
@ps:List<&2, String> -> String
the first of a list of pieces, "" when there are none
def second source · line 30 · raw
@ps:List<&2, String> -> String
the second of a list of pieces, "" when there is no second
def divide.push source · line 38 · raw
@h:Char -> @r:Cut -> Cut
one char of the divide, put back on the front of what the rest found
def divide.step source · line 46 · raw
@here:Bool -> @h:Char -> @t:String -> @n:Nat -> @rest:(@_:Unit -> Cut) -> Cut
one step of the walk; rest is a thunk so Bool.pick does not force recursion
def divide.go source · line 55 · raw
@s:String -> @+pat:String -> @+n:Nat -> Cut
the walk: stop at the first hit
def divide source · line 64 · raw
@s:String -> @+pat:String -> Cut
a string in two halves around the first occurrence of a pattern
def hex.of source · line 68 · raw
@+u:U32 -> U32
the value of one hex digit's code point, or 16 when it is not one
def hex.val source · line 75 · raw
@c:Char -> U32
the value of one hex digit, or 16 when the char is not one
def hex.step source · line 79 · raw
@done:Bool -> @acc:Nat -> @rest:(@_:Unit -> Nat) -> Nat
one step of the hex read
def hex.go source · line 87 · raw
@s:String -> @+acc:Nat -> Nat
the hex digits at the front of a string, read until one is not a digit
def hexlen source · line 97 · raw
@s:String -> Nat
chunk-size from a chunk header (RFC 9112 §7.1); extensions after ; ignored
def hex.digit source · line 101 · raw
@c:Char -> Bool
whether a character is a hex digit
def hex.rest source · line 105 · raw
@token:String -> @good:Bool -> Bool
the rest of a hex token once the first digit is known
def hex.well source · line 115 · raw
@token:String -> Bool
a chunk-size token is one or more hex digits (RFC 9112 §7.1)
def chunks.token source · line 123 · raw
@before:String -> String
the chunk-size token, extensions after ; removed
def utf8.width source · line 128 · raw
@+u:U32 -> Nat
bytes one code point takes in UTF-8. Content-Length counts octets (RFC 9110 §8.6); a Bend String counts code points.
def utf8.len source · line 134 · raw
@s:String -> Nat
how many bytes of UTF-8 a string is
def utf8.take.step source · line 142 · raw
@short:Bool -> @h:Char -> @rest:(@_:Unit -> String) -> String
one step of the take
def utf8.take source · line 150 · raw
@s:String -> @+n:Nat -> String
the front of a string that is this many bytes of UTF-8
def utf8.drop.step source · line 159 · raw
@short:Bool -> @h:Char -> @t:String -> @rest:(@_:Unit -> String) -> String
one step of the drop
def utf8.drop source · line 168 · raw
@s:String -> @+n:Nat -> String
the rest of a string after this many bytes of UTF-8
def digits.val.of source · line 177 · raw
@+u:U32 -> U32
a decimal digit's value, or 10 when the character is not a digit
def digits.val source · line 181 · raw
@c:Char -> U32
a decimal digit's value, or 10 when the character is not a digit
def digits.step source · line 185 · raw
@ok:Bool -> @next:(@_:Unit -> Maybe<&2, Nat>) -> Maybe<&2, Nat>
accept one digit and keep reading, or stop when it is not a digit
def digits.go source · line 193 · raw
@s:String -> @+acc:Nat -> @left:Nat -> Maybe<&2, Nat>
read a decimal token without building Base's Nat.read ceiling
def digits.read source · line 205 · raw
@s:String -> Maybe<&2, Nat>
a short decimal (Content-Length, max-age). Empty is none. At most 9 digits.
def chunks.ready source · line 213 · raw
@+after:String -> @+n:Nat -> Bool
a non-zero chunk has its data and the following CRLF
def chunks.next.of source · line 224 · raw
@zero:Bool -> @ready:Bool -> @+after:String -> @+n:Nat -> Next
one chunk header: reject non-hex sizes, stop on the 0 chunk, else continue
def chunks.next.hex source · line 234 · raw
@+before:String -> @+after:String -> @ok:Bool -> Next
hex check, then the length
def chunks.next source · line 243 · raw
@+before:String -> @+after:String -> Next
classify one chunk header (RFC 9112 §7.1)
def chunks.scan.step source · line 247 · raw
@step:Next -> @rest:(@_:Cut -> Maybe<&2, String>) -> Maybe<&2, String>
apply one scan step
def chunks.scan source · line 258 · raw
@fuel:Nat -> @c:Cut -> Maybe<&2, String>
None when every chunk size is hex and the 0 chunk is reached
def chunks.more source · line 268 · raw
@+n:Nat -> @+after:String -> @rest:(@_:Unit -> String) -> @zero:Bool -> String
a zero chunk ends the body; anything after it is a trailer (RFC 9112 §7.1)
def chunks source · line 278 · raw
@fuel:Nat -> @c:Cut -> String
a chunked body put back together (RFC 9112 §7.1)
def dechunk source · line 291 · raw
@+s:String -> String
a Transfer-Encoding: chunked body as the octets it stands for
def head.hit source · line 296 · raw
@h:String -> @+name:String -> Bool
whether a head line names this header (field names are case-insensitive: RFC 9110 §5.1)
def head.pick source · line 300 · raw
@hit:Bool -> @h:String -> @name:String -> @rest:String -> String
this line's value when it is the header asked for
def head.go source · line 308 · raw
@ls:List<&2, String> -> @+name:String -> String
the value a head gave a header, "" when it gave none
def head.value source · line 316 · raw
@h:String -> @name:String -> String
this line's trimmed field value
def head.values.cons source · line 320 · raw
@hit:Bool -> @value:String -> @rest:(@_:Unit -> List<&2, String>) -> List<&2, String>
cons a value when the line is the header asked for
def head.values source · line 329 · raw
@ls:List<&2, String> -> @+name:String -> List<&2, String>
every value a head gave a header, in order (RFC 9112 §6.3 lists Content-Length)
def field.folded source · line 338 · raw
@+value:String -> Bool
a field value still holding a line break is obs-fold, which we refuse
def field.keep source · line 342 · raw
@name:String -> @+value:String -> @folded:Bool -> Maybe<&2, Header>
a trimmed field value, or none when it still contains obs-fold
def head.from_kept source · line 350 · raw
@before:String -> @+after:String -> Maybe<&2, Header>
name and trimmed value from a cut at the first colon
def head.from_cut source · line 355 · raw
@c:Cut -> Maybe<&2, Header>
one header line as a Header, or none when it has no colon
def head.line source · line 363 · raw
@h:String -> Maybe<&2, Header>
one header line as a Header, or none when it has no colon
def head.cons source · line 367 · raw
@m:Maybe<&2, Header> -> @rest:List<&2, Header> -> List<&2, Header>
prepend a parsed header when present
def head.list source · line 375 · raw
@ls:List<&2, String> -> List<&2, Header>
header lines after the status line as a list of fields
def header.line source · line 383 · raw
@h:Header -> String
one request header line
def headers.find.pick source · line 389 · raw
@value:String -> @hit:Bool -> @rest:(@_:Unit -> String) -> String
the value when this field is the one asked for
def headers.find.go source · line 397 · raw
@hs:List<&2, Header> -> @+name:String -> String
case-insensitive field lookup (RFC 9110 §5.1)
def headers.find source · line 406 · raw
@hs:List<&2, Header> -> @+name:String -> String
the value of a field, "" when it is absent
def headers.lines source · line 410 · raw
@hs:List<&2, Header> -> String
extra headers rendered in order
def clen.line source · line 418 · raw
@+n:Nat -> @empty:Bool -> String
Content-Length line when the body is not empty (RFC 9110 §8.6)
def request source · line 428 · raw
@method:String -> @host:String -> @path:String -> @headers:List<&2, Header> -> @+body:String -> String
a request-line and headers (RFC 9112 §3, §5). Absolute-path request-target (RFC 9112 §3.2.1). Connection: close so the connection end frames the body when no Content-Length / chunked framing applies (RFC 9112 §9.6 / §6.3).
def get_req source · line 442 · raw
@host:String -> @path:String -> @headers:List<&2, Header> -> String
GET with optional headers
def head_req source · line 446 · raw
@host:String -> @path:String -> @headers:List<&2, Header> -> String
HEAD with optional headers (RFC 9110 §9.3.2): same as GET, no body
def post_req source · line 450 · raw
@host:String -> @path:String -> @headers:List<&2, Header> -> @body:0x0a372da4a053652f70ded7d6e0d19330/body.Body -> String
POST with body and optional headers
def put_req source · line 455 · raw
@host:String -> @path:String -> @headers:List<&2, Header> -> @body:0x0a372da4a053652f70ded7d6e0d19330/body.Body -> String
PUT with body and optional headers (RFC 9110 §9.3.4)
def delete_req source · line 460 · raw
@host:String -> @path:String -> @headers:List<&2, Header> -> @body:0x0a372da4a053652f70ded7d6e0d19330/body.Body -> String
DELETE with body and optional headers (RFC 9110 §9.3.5)
def options_req source · line 465 · raw
@host:String -> @path:String -> @headers:List<&2, Header> -> String
OPTIONS with optional headers (RFC 9110 §9.3.7). No request body.
def method.safe source · line 469 · raw
@+method:String -> Bool
GET, HEAD, and OPTIONS are safe (RFC 9110 §9.2.1). Tokens are case-sensitive.
def method.idempotent source · line 474 · raw
@+method:String -> Bool
safe methods, plus PUT and DELETE, are idempotent (RFC 9110 §9.2.2)
def token.ok.step source · line 479 · raw
@space:Bool -> @rest:(@_:Unit -> Bool) -> Bool
the rest of a token once this character is known
def token.ok source · line 487 · raw
@s:String -> Bool
a bearer token has no whitespace (RFC 6750 §2.1)
def target.origin source · line 495 · raw
@path:String -> Bool
origin-form request-target: absolute-path, optional query (RFC 9112 §3.2.1)
def target.asterisk source · line 499 · raw
@method:String -> @star:Bool -> Bool
asterisk-form is only * and only for OPTIONS (RFC 9112 §3.2.4)
def target.form source · line 507 · raw
@method:String -> @path:String -> @absolute:Bool -> Bool
origin-form, or OPTIONS asterisk-form
def parse.short source · line 515 · raw
@status:U32 -> @headers:List<&2, Header> -> @n:Nat -> @body:String -> @short:Bool -> Reply
a body against the length its head promised
def parse.clen source · line 524 · raw
@status:U32 -> @headers:List<&2, Header> -> @+n:Nat -> @+body:String -> Reply
a body framed by Content-Length (RFC 9110 §8.6 / RFC 9112 §6.3)
def parse.len source · line 529 · raw
@status:U32 -> @headers:List<&2, Header> -> @m:Maybe<&2, Nat> -> @body:String -> Reply
Content-Length when present; else the rest of the connection is the body
def clen.agree.step source · line 538 · raw
@same:Bool -> @rest:(@_:Unit -> Bool) -> Bool
every Content-Length text equals the first
def clen.agree.go source · line 546 · raw
@rest:List<&2, String> -> @+h:String -> Bool
the tail of a Content-Length list against the first value
def clen.agree source · line 554 · raw
@vs:List<&2, String> -> Bool
duplicate Content-Length fields must be the same digits (RFC 9112 §6.3)
def clen.any source · line 562 · raw
@vs:List<&2, String> -> Bool
whether any Content-Length field was present
def parse.chunked source · line 570 · raw
@status:U32 -> @headers:List<&2, Header> -> @body:String -> @bad:Maybe<&2, String> -> Reply
a chunked body, or why the chunk framing was rejected
def parse.chunks source · line 579 · raw
@status:U32 -> @headers:List<&2, Header> -> @+body:String -> Reply
scan chunk sizes, then decode when they are hex and closed by 0
def parse.te.clash source · line 584 · raw
@status:U32 -> @headers:List<&2, Header> -> @body:String -> @present:Bool -> Reply
Transfer-Encoding together with Content-Length is a bad message
def parse.te source · line 593 · raw
@status:U32 -> @headers:List<&2, Header> -> @+lens:List<&2, String> -> @+body:String -> @chunked:Bool -> Reply
chunked framing, or Content-Length, or the rest of the connection
def parse.agree source · line 602 · raw
@status:U32 -> @headers:List<&2, Header> -> @te:String -> @+lens:List<&2, String> -> @body:String -> @ok:Bool -> Reply
disagreeing Content-Length values are torn before framing
def parse.frame source · line 612 · raw
@status:U32 -> @headers:List<&2, Header> -> @te:String -> @+lens:List<&2, String> -> @body:String -> Reply
the headers that decide the framing (RFC 9112 §6.3)
def parse.status source · line 617 · raw
@m:Maybe<&2, U32> -> @+ls:List<&2, String> -> @body:String -> Reply
a head whose status line has been read (RFC 9112 §4)
def parse.head source · line 628 · raw
@+ls:List<&2, String> -> @body:String -> Reply
the head's lines, with the status line read off the first
def parse.cut source · line 633 · raw
@c:Cut -> Reply
a response divided into its head and everything after the blank line (RFC 9112 §2.1: header section ends at empty line)
def parse source · line 641 · raw
@raw:String -> Reply
the status, headers, and body of a raw response
def show source · line 645 · raw
@r:Reply -> String
a parsed response on one line, for a test to read
def reason source · line 661 · raw
@+code:U32 -> String
reason-phrase for a few common status codes (RFC 9112 §4); others get ""
def respond source · line 673 · raw
@+status:U32 -> @headers:List<&2, Header> -> @+body:String -> String
status-line + headers + body (RFC 9112 §4, §5). Connection: close ends the exchange after one response.
def status.nocontent source · line 683 · raw
@+code:U32 -> Bool
204 and 304 are terminated by the header section (RFC 9110 §15.3.5, §15.4.5)
def respond.of.body source · line 687 · raw
@+status:U32 -> @headers:List<&2, Header> -> @body:String -> @drop:Bool -> String
drop a 204/304 body; any other status keeps it
def respond.of source · line 696 · raw
@r:Reply -> String
format a structured Reply as a response message
def reply.for.pick source · line 704 · raw
@reply:Reply -> @head:Bool -> Reply
HEAD carries no message body; any other method keeps the body (RFC 9110 §9.3.2)
def reply.for source · line 714 · raw
@method:String -> @reply:Reply -> Reply
drop a HEAD response body before it is written
def method.of source · line 718 · raw
@req:Request -> String
the method token, or "" when the request did not parse
def ask.short source · line 726 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @n:Nat -> @body:String -> @short:Bool -> Request
request body against Content-Length
def ask.clen source · line 735 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @+n:Nat -> @+body:String -> Request
a request body framed by Content-Length (RFC 9110 §8.6 / RFC 9112 §6.3)
def ask.len source · line 740 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @m:Maybe<&2, Nat> -> @body:String -> Request
Content-Length when present; else the rest of the connection is the body
def ask.chunked source · line 749 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @body:String -> @bad:Maybe<&2, String> -> Request
a chunked request body, or why the chunk framing was rejected
def ask.chunks source · line 758 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @+body:String -> Request
scan chunk sizes, then decode when they are hex and closed by 0
def ask.te.clash source · line 764 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @body:String -> @present:Bool -> Request
Transfer-Encoding together with Content-Length is a bad request
def ask.te source · line 773 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @+lens:List<&2, String> -> @+body:String -> @chunked:Bool -> Request
chunked framing, or Content-Length, or the rest of the connection
def ask.agree source · line 782 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @te:String -> @+lens:List<&2, String> -> @body:String -> @ok:Bool -> Request
disagreeing Content-Length values are bad before framing
def ask.frame source · line 792 · raw
@method:String -> @target:String -> @headers:List<&2, Header> -> @te:String -> @+lens:List<&2, String> -> @body:String -> Request
the headers that decide request framing (RFC 9112 §6.3)
def ask.origin source · line 797 · raw
@method:String -> @target:String -> @+ls:List<&2, String> -> @body:String -> @ok:Bool -> Request
origin-form, or a bad request when the target is not an absolute-path
def ask.line source · line 808 · raw
@+method:String -> @+target:String -> @+ls:List<&2, String> -> @body:String -> Request
request-line: method SP request-target SP HTTP-version (RFC 9112 §3)
def ask.parts source · line 814 · raw
@ps:List<&2, String> -> @+ls:List<&2, String> -> @body:String -> Request
three fields of the request-line, or torn
def ask.head source · line 827 · raw
@+ls:List<&2, String> -> @body:String -> Request
the head's lines, with the request-line read off the first
def ask.cut source · line 835 · raw
@c:Cut -> Request
a request divided into its head and everything after the blank line
def ask source · line 843 · raw
@raw:String -> Request
the method, target, headers, and body of a raw request
def ask.show source · line 847 · raw
@r:Request -> String
a parsed request on one line, for a test to read