#http
HTTP/1.1 message framing: request parsing, response building, and response parsing, over bytes the caller supplies.
Nothing here touches a socket or a file. net is the socket layer; this
module turns the bytes it read into a Request, and a Response into
the bytes to write. There is no client: the module opens no connection
and speaks no TLS.
A request must be a complete HTTP/1.1 message with an origin-form
target. Header fields keep their received order and duplicates, bodies
stay raw bytes with any chunked coding removed, and whether the
connection stays open is reported as a Bool.
Every size and count the framer bounds is an exported max* value with
a matching check* predicate, so a caller can test a limit without
building a request that reaches it.
#Resource limits
#maxHttpRequestBytes
maxHttpRequestBytes : IntThe ceiling on one raw request, request line and framing bytes included.
> maxHttpRequestBytes
6291456#maxHttpHeaderBytes
maxHttpHeaderBytes : IntThe ceiling on the combined header and trailer sections of one request or response.
#maxHttpBodyBytes
maxHttpBodyBytes : IntThe ceiling on one decoded body, after chunked transfer coding is removed.
#maxHttpRequestLineBytes
maxHttpRequestLineBytes : IntThe ceiling on one request line.
#maxHttpResponseStatusLineBytes
maxHttpResponseStatusLineBytes : IntThe ceiling on one response status line.
#maxHttpResponseChunkBytes
maxHttpResponseChunkBytes : IntThe ceiling on one response chunk's declared size.
#maxHttpHeaderFields
maxHttpHeaderFields : IntThe ceiling on the number of header fields in one request or response.
#maxHttpTrailerFields
maxHttpTrailerFields : IntThe ceiling on the number of trailer fields in one request.
#maxHttpChunks
maxHttpChunks : IntThe ceiling on the number of chunks in one chunked body.
#maxJsonBodyBytes
maxJsonBodyBytes : IntThe ceiling on a body decoded as JSON by decodeRequestBody.
#maxTextBodyBytes
maxTextBodyBytes : IntThe ceiling on a body decoded as text by decodeRequestBody.
#maxRawBodyBytes
maxRawBodyBytes : IntThe ceiling on a body kept as raw bytes by decodeRequestBody.
#checkHttpRequestBytes
checkHttpRequestBytes : Int -> Result String Unit
checkHttpRequestBytes sizeOk when size is within maxHttpRequestBytes, Err with the
diagnostic the framer reports otherwise.
> isOk (checkHttpRequestBytes maxHttpRequestBytes)
True#checkHttpHeaderBytes
checkHttpHeaderBytes : Int -> Result String Unit
checkHttpHeaderBytes sizeOk when size is within maxHttpHeaderBytes, Err with the
diagnostic the framer reports otherwise.
#checkHttpBodyBytes
checkHttpBodyBytes : Int -> Result String Unit
checkHttpBodyBytes sizeOk when size is within maxHttpBodyBytes, Err with the diagnostic
the framer reports otherwise.
#checkHttpRequestLineBytes
checkHttpRequestLineBytes : Int -> Result String Unit
checkHttpRequestLineBytes sizeOk when size is within maxHttpRequestLineBytes, Err with the
diagnostic the framer reports otherwise.
#checkHttpResponseStatusLineBytes
checkHttpResponseStatusLineBytes : Int -> Result String Unit
checkHttpResponseStatusLineBytes sizeOk when size is within maxHttpResponseStatusLineBytes, Err with
the diagnostic the framer reports otherwise.
#checkHttpResponseChunkBytes
checkHttpResponseChunkBytes : Int -> Result String Unit
checkHttpResponseChunkBytes sizeOk when size is within maxHttpResponseChunkBytes, Err with the
diagnostic the framer reports otherwise.
#checkHttpHeaderFields
checkHttpHeaderFields : Int -> Result String Unit
checkHttpHeaderFields countOk when count is within maxHttpHeaderFields, Err with the
diagnostic the framer reports otherwise.
#checkHttpTrailerFields
checkHttpTrailerFields : Int -> Result String Unit
checkHttpTrailerFields countOk when count is within maxHttpTrailerFields, Err with the
diagnostic the framer reports otherwise.
#checkHttpChunks
checkHttpChunks : Int -> Result String Unit
checkHttpChunks countOk when count is within maxHttpChunks, Err with the diagnostic
the framer reports otherwise.
#checkJsonBodyBytes
checkJsonBodyBytes : Int -> Result String Unit
checkJsonBodyBytes sizeOk when size is within maxJsonBodyBytes, Err with the diagnostic
decodeRequestBody reports otherwise.
> isErr (checkJsonBodyBytes (maxJsonBodyBytes + 1))
True#checkTextBodyBytes
checkTextBodyBytes : Int -> Result String Unit
checkTextBodyBytes sizeOk when size is within maxTextBodyBytes, Err with the diagnostic
decodeRequestBody reports otherwise.
#checkRawBodyBytes
checkRawBodyBytes : Int -> Result String Unit
checkRawBodyBytes sizeOk when size is within maxRawBodyBytes, Err with the diagnostic
decodeRequestBody reports otherwise.
#Requests
#Header
data Header -- abstract: the constructors are not exportedOne header or trailer field: a name and a value. The name is lowercase ASCII; the value is raw bytes with surrounding whitespace removed.
#Request
data Request -- abstract: the constructors are not exportedA framed HTTP/1.1 request. Values come only from the parsers in this
module; the request* accessors read its parts.
#HttpParseFailure
data HttpParseFailure
= HttpMalformed String
| HttpResourceExcess StringWhy framing failed. HttpMalformed is a message the grammar rejects;
HttpResourceExcess is one that exceeds a max* ceiling. A server answers
the first with 400 and the second with 413. Both carry the diagnostic.
#httpParseFailureMessage
httpParseFailureMessage : HttpParseFailure -> StringThe diagnostic a framing failure carries, whichever class it is.
#headerName
headerName : Header -> StringThe field's name, in lowercase ASCII.
#headerValue
headerValue : Header -> BytesThe field's value bytes, with surrounding whitespace removed.
#requestMethod
requestMethod : Request -> StringThe request method token, exactly as it was received.
#requestTarget
requestTarget : Request -> StringThe request target, still percent-encoded. parseTargetQuery splits and
decodes it.
#requestHeaders
requestHeaders : Request -> List HeaderThe header fields in received order, duplicates retained.
#requestTrailers
requestTrailers : Request -> List HeaderThe trailer fields in received order, empty for a request whose body was not chunked.
#requestBody
requestBody : Request -> BytesThe body, with any chunked transfer coding removed. Empty for a request without a body.
#requestBodyLength
requestBodyLength : Request -> IntThe byte length of the decoded body.
#requestKeepAlive
requestKeepAlive : Request -> BoolWhether the connection stays open after this request. False when a
Connection field lists "close", otherwise True.
#isTokenByte
isTokenByte : Int -> Bool
isTokenByte byteWhether byte is one of the ASCII bytes HTTP allows in a token, such as
a method or a field name.
#findByte
findByte : Bytes -> Int -> Int -> Int -> Option Int
findByte value pos end wantedThe index of the first wanted in value[pos, end), or None. No
byte at or past end is read.
#trimLeftOws
trimLeftOws : Bytes -> Int -> Int -> Int
trimLeftOws value pos endThe index of the first byte in value[pos, end) that is not a space or
a tab, or end when they all are.
#trimRightOws
trimRightOws : Bytes -> Int -> Int -> Int
trimRightOws value start endThe index one past the last byte in value[start, end) that is not a
space or a tab, or start when they all are.
#parseFields
parseFields : Bytes -> Int -> Bool -> Result HttpParseFailure (List Header, Int)
parseFields input pos trailerThe fields of the header or trailer section starting at pos, and the
offset just past the blank line that ends it.
Names are lowercased, values lose their surrounding whitespace, and order
and duplicates are kept. When trailer is True, the fields that may
not appear in a trailer (Content-Length, Transfer-Encoding,
Trailer, Host, Connection) are rejected.
#hexDigit
hexDigit : Int -> Option Int
hexDigit byteThe value of one ASCII hexadecimal digit, or None when byte is not
one. Both letter cases are accepted.
#skipOws
skipOws : Bytes -> Int -> Int -> Int
skipOws value pos endThe index of the first byte in value[pos, end) that is not a space or
a tab, or end when they all are. The same as trimLeftOws.
#parseChunked
parseChunked : Bytes -> Int -> Result HttpParseFailure (Bytes, List Header, Int)
parseChunked input posThe decoded bytes of the chunked body starting at pos, its trailer
fields, and the offset just past the trailer section.
Chunk size, chunk count, decoded body size, trailer count, and trailer
section size are each checked against their max* ceiling.
#parseRequestClassified
parseRequestClassified : Bytes -> Result HttpParseFailure Request
parseRequestClassified inputThe request framed by a buffer that holds exactly one complete request, or the failure with its class. Bytes after the request are an error.
#Incremental framing
#HttpFrame
data HttpFrame
= HttpNeedMore
| HttpFramedAt Int
| HttpFrameFailed HttpParseFailureThe verdict of a scan. HttpNeedMore means more bytes could still
complete the request. HttpFramedAt n means a complete request ends at
that offset, where the next one begins. HttpFrameFailed means no further byte
can help.
#HttpScan
data HttpScan -- abstract: the constructors are not exportedThe progress of a scan over a buffer that is still growing.
Begin with httpScanStart, pass the state to scanRequestBoundaryFrom
or scanRequestBoundaryWithin with the same start and a buffer that
has only grown at its end, and keep the returned state for the next read.
A state describes one buffer prefix: for a different buffer, a different
start, or the next request, begin again from httpScanStart. Starting
over reaches the same verdict as resuming; it only rescans.
#httpScanStart
httpScanStart : HttpScanA scan that has read nothing.
#httpScanInHeaders
httpScanInHeaders : HttpScan -> BoolWhether the scan is still inside the request line or the header fields.
False once the blank line ending the header section has been read,
including for a scan that has framed a whole request.
#httpScanBodyRemaining
httpScanBodyRemaining : HttpScan -> Int -> Option Int
httpScanBodyRemaining _ availHow many bytes beyond the first avail the request still needs, or
None when that is not yet known.
It is known once the header section has fixed where the body ends: a
Content-Length body or no body at all. While the scan is still in the
header section, and for a chunked body, which declares its length one
chunk at a time, the answer is None.
#scanRequestBoundaryWithin
scanRequestBoundaryWithin : MutBytes -> Int -> Int -> HttpScan -> (HttpFrame, HttpScan)
scanRequestBoundaryWithin input avail start scanThe end of the first complete request at or after start within the
first avail bytes of input, resumed from a prior scan state, together
with the state to resume from next time.
Bytes at or past avail are never read, so a caller can scan a buffer
that is only partly filled. A pending request already larger than
maxHttpRequestBytes is reported as HttpFrameFailed rather than
HttpNeedMore. An avail outside the buffer, or a start outside
[0, avail], fails as HttpMalformed and leaves the state unchanged.
A resumed scan costs time proportional to the bytes that arrived since the
state was produced.
input is a buffer still being written, such as the block
bytebuilder.builderParts hands out, and it is read in place: nothing the
scan keeps refers to it.
#scanRequestBoundaryFrom
scanRequestBoundaryFrom : Bytes -> Int -> HttpScan -> (HttpFrame, HttpScan)
scanRequestBoundaryFrom input start scanscanRequestBoundaryWithin over every byte of input.
#scanRequestBoundary
scanRequestBoundary : Bytes -> Int -> HttpFrame
scanRequestBoundary input startThe verdict of scanRequestBoundaryFrom started from httpScanStart,
without the state.
#parseRequestAt
parseRequestAt : MutBytes -> Int -> Int -> Result HttpParseFailure Request
parseRequestAt input start endThe request framed by input[start, end), parsed as
parseRequestClassified parses a copy of that slice on its own. Bounds
outside the buffer fail as HttpMalformed.
#parseRequest
parseRequest : Bytes -> Result String Request
parseRequest inputparseRequestClassified with the failure reduced to its diagnostic.
Use the classified form to tell a 400 from a 413.
#Response parsing
#ParsedResponse
data ParsedResponse -- abstract: the constructors are not exportedA parsed HTTP/1.0 or HTTP/1.1 response.
Fields and trailers keep their received order and duplicates; the body
has any chunked transfer coding removed. Values come only from
parseResponseClassified and parseResponse.
#parsedResponseStatus
parsedResponseStatus : ParsedResponse -> IntThe status code.
#parsedResponseReason
parsedResponseReason : ParsedResponse -> StringThe reason phrase as received, read as UTF-8: a byte sequence that is not UTF-8 reads back as U+FFFD.
#parsedResponseHeaders
parsedResponseHeaders : ParsedResponse -> List HeaderThe header fields in received order, duplicates retained.
#parsedResponseTrailers
parsedResponseTrailers : ParsedResponse -> List HeaderThe trailer fields in received order, empty for a body that was not chunked.
#parsedResponseBody
parsedResponseBody : ParsedResponse -> BytesThe body, with any chunked transfer coding removed.
#parsedResponseBodyLength
parsedResponseBodyLength : ParsedResponse -> IntThe byte length of the decoded body.
#parseResponseClassified
parseResponseClassified : Bytes -> Result HttpParseFailure ParsedResponse
parseResponseClassified inputThe response framed by a buffer that holds exactly one complete response, or the failure with its class.
The status line, fields, chunks, decoded body, and trailers are each
bounded by their max* ceiling. A response with neither a
Content-Length nor a chunked Transfer-Encoding runs to the end of the
buffer, unless its status code (1xx, 204, or 304) forbids a body.
#parseResponse
parseResponse : Bytes -> Result String ParsedResponse
parseResponse inputparseResponseClassified with the failure reduced to its diagnostic.
> isErr (parseResponse (encodeUtf8 ""))
True#responseBoundaryWithin
responseBoundaryWithin : MutBytes -> Int -> Option Int
responseBoundaryWithin input availThe offset just past the first complete response in input[0, avail),
or None when that prefix is incomplete, malformed, or a response that
ends only when the connection closes.
Bytes at or past avail are never read. The offset is less than avail
when another message follows the response. An avail outside the buffer
answers None.
input is a buffer still being written, such as the block
bytebuilder.builderParts hands out, and it is read in place, so a
response whose header declares its length costs the header, not the body.
> responseBoundaryWithin (MB.thaw (encodeUtf8 "HTTP/1.1 204 No Content\r\n\r\nX")) 27
Some 27#responseBoundary
responseBoundary : Bytes -> Option Int
responseBoundary inputresponseBoundaryWithin over all bytes in input.
#Response building
#Response
data Response -- abstract: the constructors are not exportedA response ready to serialize. Values come only from makeResponse,
which checks the status, the reason phrase, and the fields.
#makeHeader
makeHeader : String -> Bytes -> Result String Header
makeHeader name valueA response field with name lowercased, or Err when name is not a
token or value is not printable ASCII. Control bytes, CR and LF among
them, are rejected, so a field cannot split the response.
#makeResponse
makeResponse : Int -> String -> List Header -> Bytes -> Result String Response
makeResponse status reason headers bodyA response with the given status, reason phrase, fields, and body, or
Err when one of them is invalid.
status must be from 100 to 599 and reason printable ASCII.
headers may not include
Content-Length or Transfer-Encoding; serializeResponse writes the
framing itself.
#responseStatus
responseStatus : Response -> IntThe status code.
#responseHeaders
responseHeaders : Response -> List HeaderThe fields given to makeResponse, in that order. The Content-Length
that serializeResponse adds is not among them.
#responseBody
responseBody : Response -> BytesThe body bytes.
#responseReason
responseReason : Response -> StringThe reason phrase, as given to makeResponse.
#serializeResponse
serializeResponse : Response -> BytesThe bytes of the response as an HTTP/1.1 message: the status line, the
fields in their given order, one content-length field, a blank line,
and the body.
#Targets, media types, and bodies
#MediaType
data MediaType -- abstract: the constructors are not exportedA media type reduced to its type and subtype, both lowercased.
Parameters are checked by parseMediaType and then dropped.
#DecodedBody
data DecodedBody
= JsonBody MediaType Json
| TextBody MediaType String
| RawBody MediaType BytesA request body decoded by its media type. An application/json body is
parsed as JSON, a text/* body is decoded as UTF-8 text, and every other
type keeps its bytes.
#QueryParam
data QueryParam
= QueryParam String StringOne query parameter: its name and its decoded value, "" when the
query gave it none.
#parseTargetQuery
parseTargetQuery : Request -> Result String (String, List QueryParam)The path and the query parameters of the request target, with percent escapes decoded.
Parameters keep their order and duplicates, and a parameter without =
has the value "". A + stays a literal +. Err on a malformed
percent escape, an empty parameter name, or a component that is not
UTF-8 text.
#mediaTypeType
mediaTypeType : MediaType -> StringThe lowercased type, such as "text" for text/plain.
#mediaTypeSubtype
mediaTypeSubtype : MediaType -> StringThe lowercased subtype, such as "plain" for text/plain.
#parseMediaType
parseMediaType : Bytes -> Result String MediaType
parseMediaType valueThe media type in value, with type and subtype lowercased, or Err
when it is not one. Parameters are checked for form and then dropped.
value must be ASCII and at most 4096 bytes.
#decodeRequestBody
decodeRequestBody : Request -> Result String DecodedBodyThe request body decoded by its Content-Type.
Err when the field is missing or repeated, when the media type is
invalid, when the body exceeds the ceiling for its kind
(maxJsonBodyBytes, maxTextBodyBytes, or maxRawBodyBytes), or when a
JSON or text body is not valid UTF-8.