///|
test "read a chunked request" {
let wire = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n"
let heads = @h1.HeadDecoder::new(skip_leading_crlf=true)
let result = heads.feed(wire)
guard result.head is Some(head) else { fail("head incomplete") }
let request = @h1.parse_request_head(head)
assert_eq(request.meth, Post)
assert_eq(request.target, "/upload")
guard @h1.request_framing(request.headers) is Chunked else {
fail("expected a chunked body")
}
let body = @h1.ChunkedDecoder::new(@h1.Limits::default())
let decoded = body.feed(wire[result.consumed:])
assert_eq(decoded.chunks, [b"hello"])
assert_true(decoded.done)
body.finish()
}///|
test "resolve response framing" {
let response = @h1.parse_response_head(
b"HTTP/1.1 200\r\nContent-Length: 12\r\n\r\n",
)
assert_eq(
@h1.response_framing(response.headers, status=response.status),
Length(12),
)
assert_eq(
@h1.response_framing(response.headers, status=response.status, meth=Head),
NoBody,
)
}///|
test "write a chunked response" {
let head = @h1.write_response_head(
200,
@mttp_core.Headers::of([("Content-Type", "text/plain")]),
Chunked,
Http11,
)
let parsed = @h1.parse_response_head(head)
assert_eq(@h1.response_framing(parsed.headers, status=parsed.status), Chunked)
assert_eq(@h1.encode_chunk(b"hi"), b"2\r\nhi\r\n")
}| Kind | Inputs | A server answers |
|---|---|---|
| HeadTooLarge | a head, chunk-size line or trailer section past its Limits | 431 for a head; 400 inside a body |
| Malformed | bare CR or LF, an empty line before a status line, obs-fold, whitespace before a colon, controls in a field, a missing or invalid Host on HTTP/1.1, an invalid target form, any duplicate Content-Length (even identical), an invalid or overflowing length or chunk size | 400 |
| Smuggling | Content-Length with Transfer-Encoding, Transfer-Encoding on HTTP/1.0, a framing or routing field in trailers | 400 |
| Unsupported | a transfer coding other than a single chunked, an HTTP version other than 1.x | 501 or 505 |
| Bound | Default |
|---|---|
| Head, start line, final CRLF CRLF and skipped empty lines included | 16 KiB |
| Fields per head, and per trailer section | 100 |
| Chunk-size line, extensions and CRLF included | 4 KiB |
| Trailer section, final CRLF included | 16 KiB |
pub struct ChunkedDecoder {
// private fields
}test {
let decoder = @h1.ChunkedDecoder::new(@h1.Limits::default())
let result = decoder.feed(b"4\r\nWiki\r\n0\r\nX-Sum: 1\r\n\r\nnext")
assert_eq(result.chunks, [b"Wiki"])
assert_true(result.done)
assert_eq(result.trailers.get("x-sum"), Some("1"))
assert_eq(result.consumed, 24)
decoder.finish()
}pub struct HeadDecoder {
// private fields
}test {
let decoder = @h1.HeadDecoder::new()
assert_eq(decoder.feed(b"HTTP/1.1 200 OK\r\n").consumed, 17)
let result = decoder.feed(b"\r\nbody")
assert_eq(result.consumed, 2)
assert_eq(result.head, Some(b"HTTP/1.1 200 OK\r\n\r\n"))
let requests = @h1.HeadDecoder::new(skip_leading_crlf=true)
let result = requests.feed(b"\r\nGET / HTTP/1.1\r\nHost: h\r\n\r\n")
assert_eq(result.consumed, 29)
assert_eq(result.head, Some(b"GET / HTTP/1.1\r\nHost: h\r\n\r\n"))
}test {
let limits = @h1.Limits::default()
assert_eq(limits.max_head_bytes, 16384)
let strict = { ..limits, max_headers: 20, }
assert_eq(strict.max_headers, 20)
}fn encode_chunk(bytes : BytesView) -> Bytestest {
assert_eq(@h1.encode_chunk(b"Wiki"), b"4\r\nWiki\r\n")
assert_eq(@h1.encode_chunk(b""), b"0\r\n\r\n")
}test {
let request = @h1.parse_request_head(
b"GET /items?page=2 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n",
)
assert_eq(request.meth, Get)
assert_eq(request.target, "/items?page=2")
assert_eq(request.version, Http11)
assert_eq(request.headers.get("host"), Some("example.com"))
}test {
let response = @h1.parse_response_head(
b"HTTP/1.1 200\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\n\r\n",
)
assert_eq(response.status, 200)
assert_eq(response.headers.get_all("set-cookie"), ["a=1", "b=2"])
}test {
let chunked = @mttp_core.Headers::of([("Transfer-Encoding", "chunked")])
assert_eq(@h1.request_framing(chunked), Chunked)
let both = chunked.add("Content-Length", "5")
try @h1.request_framing(both) catch {
@h1.ParseError(kind~, ..) => assert_eq(kind, Smuggling)
} noraise {
_ => fail("CL+TE must be refused")
}
}test {
let wire = b"HTTP/1.1 204\r\n\r\nnext"
assert_eq(@h1.scan_head(wire, max=16384), Some(16))
assert_eq(@h1.scan_head(wire[:10], max=16384), None)
let request = b"\r\nGET / HTTP/1.1\r\nHost: h\r\n\r\n"
assert_eq(
@h1.scan_head(request, max=16384, skip_leading_crlf=true),
Some(request.length()),
)
}fn write_request_head(meth : Method, target : String, headers : Headers, framing : Framing, v : Version) -> Bytes raise ParseErrortest {
let head = @h1.write_request_head(
Post,
"/items",
@mttp_core.Headers::of([("Host", "example.com")]),
Chunked,
Http11,
)
assert_eq(
head, b"POST /items HTTP/1.1\r\nhost: example.com\r\ntransfer-encoding: chunked\r\n\r\n",
)
}fn write_response_head(status : Int, headers : Headers, framing : Framing, v : Version, meth? : Method) -> Bytes raise ParseErrortest {
let head = @h1.write_response_head(
200,
@mttp_core.Headers::of([("Content-Type", "text/plain")]),
Length(5),
Http11,
)
assert_eq(
head, b"HTTP/1.1 200 OK\r\ncontent-type: text/plain\r\ncontent-length: 5\r\n\r\n",
)
let empty = @h1.write_response_head(
200,
@mttp_core.Headers::empty(),
NoBody,
Http11,
)
assert_eq(empty, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n")
let none = @h1.write_response_head(
204,
@mttp_core.Headers::empty(),
NoBody,
Http11,
)
assert_eq(none, b"HTTP/1.1 204 No Content\r\n\r\n")
}Install
Download zipHTTP for MoonBit without I/O, for libraries such as SDKs: the types, an HTTP/1.1 codec and engines, a client with retries under a budget, deadlines, rate limits, redirects and cookies, SSE, multipart, and typed endpoints with a router and OpenAPI. Apps add gaato/mttp.