///|
/// What a connection has read but not yet consumed.
///
/// A head is read with `read_some`, which hands back whatever has arrived — and a
/// client that pipelines puts the body, or the next request, in the same segment.
/// Those octets belong to what comes next, so they are held here rather than
/// dropped, and every later read on the connection draws from them first. Losing
/// them is how a keep-alive connection drops a request.
priv struct Inbox {
reader : &@io.Reader
mut held : Bytes
}
///|
fn Inbox::new(reader : &@io.Reader) -> Inbox {
{ reader, held: b"", }
}
///|
/// Read more onto what is held. `false` when the peer has closed.
async fn Inbox::fill(self : Inbox) -> Bool {
match self.reader.read_some() {
Some(more) => {
let buf = Buffer()
buf.write_bytes(self.held)
buf.write_bytes(more)
self.held = buf.to_bytes()
true
}
None => false
}
}
///|
/// Drop the first `n` held octets, which a parse has consumed.
fn Inbox::consume(self : Inbox, n : Int) -> Unit {
self.held = self.held[n:].to_owned()
}
///|
/// The next request head, or `None` when the peer went away or sent something
/// that is not one.
///
/// The parse is moonhttp's: this only feeds it until it stops saying `Partial`. A
/// head that fails any check — including the request-smuggling ones — ends the
/// connection, which is what uvicorn does with a malformed request too.
async fn Inbox::head(self : Inbox, limits : @http1.Limits) -> @http1.Request? {
for ;; {
let outcome : Result[(@http1.Request, Int), @http1.Refused] = Ok(
@http1.request(self.held[:], limits~),
) catch {
e => Err(e)
}
match outcome {
Ok((req, used)) => {
self.consume(used)
return Some(req)
}
Err(Partial) => if !self.fill() { return None }
Err(_) => return None
}
}
}
///|
/// Exactly `n` octets of body, from what is held and then from the connection.
async fn Inbox::exactly(self : Inbox, n : Int) -> Bytes {
while self.held.length() < n {
if !self.fill() {
break
}
}
let take = if self.held.length() < n { self.held.length() } else { n }
let out = self.held[0:take].to_owned()
self.consume(take)
out
}
///|
/// A chunked body, decoded, with any trailer fields read and dropped.
async fn Inbox::chunks(self : Inbox) -> Bytes {
let body = Buffer()
for ;; {
let outcome : Result[@http1.Chunk, @http1.Refused] = Ok(
@http1.chunked(self.held[:]),
) catch {
e => Err(e)
}
match outcome {
Ok(Piece(part, used)) => {
body.write_bytes(part)
self.consume(used)
}
Ok(Done(_, used)) => {
self.consume(used)
break
}
Err(Partial) => if !self.fill() { break }
Err(_) => break
}
}
body.to_bytes()
}
///|
/// Read the next request head off a connection that is between requests, giving up after
/// `Config::timeout_keep_alive` (← uvicorn's `--timeout-keep-alive`). `None` means the peer went
/// quiet or went away — either way the connection is finished, and holding it open costs a socket
/// and a task for as long as the peer cares to say nothing.
async fn next_head(inbox : Inbox, config : Config) -> @http1.Request? {
match
@async.with_timeout_opt(config.timeout_keep_alive, () => {
inbox.head(@http1.Limits::new(head=config.max_head_size))
}) {
Some(req) => req
None => None
}
}
///|
/// The fields as a map, for the helpers that look one up by name.
///
/// A repeated field keeps its last value, which is what those lookups always did.
/// The ASGI scope is built from [`fields_pairs`] instead, which keeps every one.
fn fields_map(fields : Array[@header.Header]) -> Map[String, String] {
let out : Map[String, String] = Map([])
for f in fields {
let (k, v) = f.ascii()
out[k] = v
}
out
}
///|
/// The fields as ASGI's header pairs, in order and with repeats kept.
///
/// ASGI's `headers` is a list precisely so that two `Cookie` or two `Forwarded`
/// lines survive; building it from a map would collapse them.
fn fields_pairs(fields : Array[@header.Header]) -> Array[(String, String)] {
fields.map(f => f.ascii())
}
///|
/// One chunk of a streamed response. A zero-length body is skipped so it is never
/// mistaken for the terminating chunk.
async fn write_chunk(writer : &@io.Writer, body : Bytes) -> Unit {
if body.length() == 0 {
return
}
writer.write(@http1.chunk(body[:]))
}
///|
/// Drive a moonasgi application over a single already-established HTTP/1.1
/// transport (`reader`/`writer` are the same `@tls.Tls` stream on the TLS path).
/// Builds the `Http` `Scope`, a `Receive` that streams the `Content-Length`
/// request body, and a `Send` that serialises the response:
///
/// * a single-shot response (`http.response.body` with `more_body=false` first)
/// is framed with `Content-Length`;
/// * a streamed response (`more_body=true`) switches to `Transfer-Encoding:
/// chunked`, one chunk per body event, terminated by the final `0\r\n\r\n`.
///
/// Framing headers the app supplies (`content-length` / `transfer-encoding`) are
/// dropped in favour of the ones this codec computes. Returns whether the
/// connection should be kept alive for a further request.
async fn dispatch_http1(
app : @moonasgi.AsgiApp,
req : @http1.Request,
inbox : Inbox,
writer : &@io.Writer,
scheme~ : String,
config? : Config = Config::new(),
client_addr? : @socket.Addr,
) -> Bool {
let server_headers = config.server_headers
// A WebSocket upgrade is bridged over the raw stream (with subprotocol echo) and ends the
// connection — it never returns to the keep-alive HTTP loop.
// A conforming client sends nothing after an upgrade request until it has the 101
// (RFC 6455 §4.1), so nothing is held here to hand over with the stream.
if is_websocket_upgrade(fields_map(req.fields)) {
serve_websocket_raw(app, req, inbox.reader, writer, config~, client_addr?)
return false
}
let (path, query) = split_query(req.target)
let peer = match client_addr {
Some(a) => Some(addr_pair(a))
None => None
}
let (client, req_scheme) = if config.proxy_headers {
proxy_rewrite(
fields_map(req.fields),
peer,
scheme,
trusted=config.forwarded_allow_ips,
)
} else {
(peer, scheme)
}
let scope = @moonasgi.Scope::Http({
http_version: if req.version == "HTTP/1.0" {
"1.0"
} else {
"1.1"
},
http_method: req.verb,
scheme: req_scheme,
path,
raw_path: @utf8.encode(path),
query_string: @utf8.encode(query),
root_path: config.root_path,
headers: fields_pairs(req.fields),
client,
server: None,
asgi: @moonasgi.AsgiVersion::http(),
extensions: @moonasgi.Extensions::none(),
state: Map([]),
})
let body_sent = Ref(false)
let receive : @moonasgi.Receive = () => {
if body_sent.val {
@moonasgi.Event::HttpDisconnect
} else {
body_sent.val = true
// Expect: 100-continue — let the client know it may send the body now.
if req.expects {
writer.write(@http1.continue_)
}
let body = match req.body {
Chunked => inbox.chunks()
Sized(n) => inbox.exactly(n)
_ => b""
}
@moonasgi.Event::HttpRequest(body~, more_body=false)
}
}
let resp_status = Ref(200)
let resp_headers : Ref[Array[(String, String)]] = Ref([])
let started = Ref(false)
let chunked = Ref(false)
let ended = Ref(false)
// Answer with the request's HTTP version; HTTP/1.0 has no chunked transfer-encoding,
// so a streamed 1.0 response is framed by connection close instead.
let is_http10 = req.version == "HTTP/1.0"
let resp_version = if is_http10 { "HTTP/1.0" } else { "HTTP/1.1" }
// A HEAD response carries the same headers as GET but no body (RFC 7231 §4.3.2).
let is_head = req.verb == "HEAD"
let close_delimited = Ref(false)
let head = fn(extra : String) -> String {
let sb = StringBuilder()
sb.write_string(
"\{resp_version} \{resp_status.val} \{@http1.phrase(resp_status.val)}\r\n",
)
let sent : Map[String, Bool] = Map([])
for pair in resp_headers.val {
let kl = pair.0.to_lower()
sent[kl] = true
if kl != "content-length" && kl != "transfer-encoding" {
sb.write_string("\{pair.0}: \{pair.1}\r\n")
}
}
// Configured server headers (e.g. `Server`) as defaults, unless the app already
// set the same key — matching what the async HTTP server injects on the plaintext
// path, so HTTPS responses carry them too.
for name, value in server_headers {
let kl = name.to_lower()
if !sent.contains(kl) &&
kl != "content-length" &&
kl != "transfer-encoding" {
sent[kl] = true
sb.write_string("\{name}: \{value}\r\n")
}
}
// RFC 7231 §7.1.1.2 asks an origin server with a clock to date every response.
if config.date_header && !sent.contains("date") {
sb.write_string("date: \{http_date_now()}\r\n")
}
// Signal the connection's fate unless the app already set `Connection`: a server
// that will close SHOULD say so (RFC 7230 §6.1), and HTTP/1.0 needs an explicit
// `keep-alive` since it defaults to close.
if !sent.contains("connection") {
let will_keep_alive = req.keep_alive && !close_delimited.val
if !will_keep_alive {
sb.write_string("connection: close\r\n")
} else if is_http10 {
sb.write_string("connection: keep-alive\r\n")
}
}
sb.write_string(extra)
sb.to_string()
}
let send : @moonasgi.Send = event => {
match event {
HttpResponseStart(status~, headers~, ..) => {
resp_status.val = status
resp_headers.val = headers
}
HttpResponseBody(body~, more_body~) =>
if !started.val {
started.val = true
if !more_body {
writer.write(head("content-length: \{body.length()}\r\n\r\n"))
if !is_head {
writer.write(body)
}
ended.val = true
} else if is_head {
// A HEAD response carries the framing headers a GET would but no body,
// and no chunk terminator; the head alone completes it.
writer.write(head("transfer-encoding: chunked\r\n\r\n"))
ended.val = true
} else if is_http10 {
// No chunked on HTTP/1.0: frame the stream by closing the connection.
close_delimited.val = true
writer.write(head("\r\n"))
writer.write(body)
} else {
chunked.val = true
writer.write(head("transfer-encoding: chunked\r\n\r\n"))
write_chunk(writer, body)
}
} else if chunked.val {
write_chunk(writer, body)
if !more_body {
writer.write(@http1.last_chunk())
ended.val = true
}
} else if close_delimited.val {
writer.write(body)
if !more_body {
ended.val = true
}
}
_ => ()
}
}
app(scope, receive, send)
if !started.val {
writer.write(head("content-length: 0\r\n\r\n"))
} else if chunked.val && !ended.val {
writer.write(@http1.last_chunk())
}
// A close-delimited response has no length signal, so the connection must close.
req.keep_alive && !close_delimited.val
}
///|
/// Read a (closed) pipe to its bytes — a test helper for inspecting the response a
/// `dispatch_http1` run wrote.
async fn drain_pipe(r : @pipe.PipeRead) -> Bytes {
let buf = Buffer()
for ;; {
match r.read_some() {
Some(chunk) => buf.write_bytes(chunk)
None => break
}
}
buf.to_bytes()
}
///|
/// Whether `needle` occurs anywhere in `hay` (test helper for response inspection).
fn bytes_has(hay : Bytes, needle : Bytes) -> Bool {
let n = needle.length()
if n == 0 {
return true
}
let mut i = 0
while i + n <= hay.length() {
let mut j = 0
while j < n && hay[i + j] == needle[j] {
j = j + 1
}
if j == n {
return true
}
i = i + 1
}
false
}
///|
/// Drive `dispatch_http1` for `req` with a trivial 200 app over in-memory pipes and
/// return the raw response bytes (test helper).
async fn run_h1(
req : @http1.Request,
config? : Config = Config::new(),
headers? : Array[(String, String)] = [],
) -> Bytes {
let (req_r, req_w) = @pipe.pipe()
req_w.close()
let (resp_r, resp_w) = @pipe.pipe()
let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(
@moonasgi.Event::HttpResponseStart(status=200, headers~, trailers=false),
)
send(@moonasgi.Event::HttpResponseBody(body=b"hi", more_body=false))
}
let _ = dispatch_http1(
app,
req,
Inbox::new(req_r),
resp_w,
scheme="http",
config~,
)
resp_w.close()
drain_pipe(resp_r)
}
///|
async test "http1: a HEAD request keeps content-length but suppresses the body" {
let head_resp = run_h1(h1("HEAD", "HTTP/1.1", ""))
// The framing header a GET would send is present...
assert_eq(bytes_has(head_resp, b"content-length: 2"), true)
// ...but the body ("hi") is not.
assert_eq(bytes_has(head_resp, b"hi"), false)
// A GET on the same app does include the body.
assert_eq(bytes_has(run_h1(h1_req("HTTP/1.1")), b"hi"), true)
}
///|
async test "http1: the response signals connection close and HTTP/1.0 keep-alive" {
// HTTP/1.1 with Connection: close -> the response echoes connection: close.
let close_11 = run_h1(h1("GET", "HTTP/1.1", "connection: close\r\n"))
assert_eq(bytes_has(close_11, b"connection: close"), true)
// HTTP/1.0 defaults to close -> connection: close.
assert_eq(bytes_has(run_h1(h1_req("HTTP/1.0")), b"connection: close"), true)
// HTTP/1.0 with keep-alive requested -> connection: keep-alive.
let ka_10 = run_h1(h1("GET", "HTTP/1.0", "connection: keep-alive\r\n"))
assert_eq(bytes_has(ka_10, b"connection: keep-alive"), true)
// HTTP/1.1 defaults to keep-alive -> no connection: close header.
assert_eq(bytes_has(run_h1(h1_req("HTTP/1.1")), b"connection: close"), false)
}
///|
/// A request head for driving `dispatch_http1` in tests, read through the real parser.
///
/// Built from octets rather than as a record, so keep-alive, framing and the rest
/// are derived from the version and the fields exactly as they are for a request off
/// a socket — a record could set them to values no real request would produce.
fn h1(verb : String, version : String, fields : String) -> @http1.Request {
let wire = @utf8.encode(verb + " / " + version + "\r\n" + fields + "\r\n")
@http1.request(wire[:]).0 catch {
e => abort("a test request should parse: \{e}")
}
}
///|
/// A plain `GET /` in the given version.
fn h1_req(version : String) -> @http1.Request {
h1("GET", version, "")
}
///|
async test "http1: an HTTP/1.0 request is answered with an HTTP/1.0 status line" {
let (req_r, req_w) = @pipe.pipe()
req_w.close()
let (resp_r, resp_w) = @pipe.pipe()
let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(
@moonasgi.Event::HttpResponseStart(status=200, headers=[], trailers=false),
)
send(@moonasgi.Event::HttpResponseBody(body=b"hi", more_body=false))
}
let keep = dispatch_http1(
app,
h1_req("HTTP/1.0"),
Inbox::new(req_r),
resp_w,
scheme="http",
)
resp_w.close()
let resp = drain_pipe(resp_r)
assert_eq(resp[0:8].to_owned() == b"HTTP/1.0", true)
// HTTP/1.0 defaults to closing the connection.
assert_eq(keep, false)
}
///|
async test "http1: a streamed HTTP/1.0 response is close-delimited, not chunked" {
let (req_r, req_w) = @pipe.pipe()
req_w.close()
let (resp_r, resp_w) = @pipe.pipe()
let app : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(
@moonasgi.Event::HttpResponseStart(status=200, headers=[], trailers=false),
)
send(@moonasgi.Event::HttpResponseBody(body=b"part1", more_body=true))
send(@moonasgi.Event::HttpResponseBody(body=b"part2", more_body=false))
}
let keep = dispatch_http1(
app,
h1_req("HTTP/1.0"),
Inbox::new(req_r),
resp_w,
scheme="http",
)
resp_w.close()
let resp = drain_pipe(resp_r)
let n = resp.length()
// HTTP/1.0 has no chunked, so the body is written raw and the response ends with the
// last body part, not a "0\r\n\r\n" chunk terminator.
assert_eq(resp[n - 5:n].to_owned() == b"part2", true)
// Close-delimited framing forces the connection closed.
assert_eq(keep, false)
}
///|
async test "http1: configured server headers ride the response, unless the app sets them" {
// Default: the configured `server` header is written.
let (req_r, req_w) = @pipe.pipe()
req_w.close()
let (resp_r, resp_w) = @pipe.pipe()
let plain : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(
@moonasgi.Event::HttpResponseStart(status=200, headers=[], trailers=false),
)
send(@moonasgi.Event::HttpResponseBody(body=b"hi", more_body=false))
}
let _ = dispatch_http1(
plain,
h1_req("HTTP/1.1"),
Inbox::new(req_r),
resp_w,
scheme="http",
config=Config::new(server_headers=Map([("server", "mooncat")])),
)
resp_w.close()
assert_eq(bytes_has(drain_pipe(resp_r), b"server: mooncat"), true)
// Override: an app that sets `server` itself wins over the configured default.
let (req_r2, req_w2) = @pipe.pipe()
req_w2.close()
let (resp_r2, resp_w2) = @pipe.pipe()
let overrides : @moonasgi.AsgiApp = (_scope, _receive, send) => {
send(
@moonasgi.Event::HttpResponseStart(
status=200,
headers=[("server", "myapp")],
trailers=false,
),
)
send(@moonasgi.Event::HttpResponseBody(body=b"hi", more_body=false))
}
let _ = dispatch_http1(
overrides,
h1_req("HTTP/1.1"),
Inbox::new(req_r2),
resp_w2,
scheme="http",
config=Config::new(server_headers=Map([("server", "mooncat")])),
)
resp_w2.close()
let resp2 = drain_pipe(resp_r2)
assert_eq(bytes_has(resp2, b"server: myapp"), true)
assert_eq(bytes_has(resp2, b"server: mooncat"), false)
}
///|
async test "http1: a chunked request body decodes to the joined chunk data" {
let (r, w) = @pipe.pipe()
// Two data chunks ("hello" + " world") then the terminating zero chunk.
w.write(b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n")
w.close()
assert_eq(Inbox::new(r).chunks() == b"hello world", true)
}
///|
async test "http1: a chunked body with a chunk extension and a trailer decodes cleanly" {
let (r, w) = @pipe.pipe()
// A chunk-size line carries a `;ext`, and a trailer field follows the zero chunk.
w.write(b"4;n=v\r\ndata\r\n0\r\nx-checksum: 1\r\n\r\n")
w.close()
assert_eq(Inbox::new(r).chunks() == b"data", true)
}
///|
/// The keep-alive read that both HTTPS and plaintext connections sit in between requests. A peer
/// that connects and says nothing must not be able to hold the connection open indefinitely, which
/// is what an unbounded read gave it.
async test "http1: an idle keep-alive read gives up after timeout_keep_alive" {
@async.with_task_group(g => {
// A pipe nobody ever writes to: the peer is connected and silent.
let (r, w) = @pipe.pipe()
let opened = @async.now()
// Bounded from outside as well, so a read that ignores the configured deadline fails this
// test instead of wedging the suite in the very wait it was supposed to end.
let head = @async.with_timeout_opt(3000, () => {
next_head(Inbox::new(r), Config::new(timeout_keep_alive=250))
})
let waited = @async.now() - opened
assert_eq(head is Some(None), true)
// It waited out its own timeout rather than returning at once.
assert_eq(waited >= 200L, true)
// A request that does arrive is read normally.
let feeder = g.spawn(() => {
w.write(b"GET /ok HTTP/1.1\r\nhost: x\r\n\r\n") catch {
_ => ()
}
w.close()
})
guard next_head(Inbox::new(r), Config::new(timeout_keep_alive=3000))
is Some(req) else {
fail("a request that arrives inside the timeout should be read")
}
assert_eq(req.target, "/ok")
feeder.cancel()
})
}
///|
/// The head cap the connection loop applies is the configured one, not the module default: a
/// service behind a proxy that adds a large header block has to be able to raise it.
async test "http1: the keep-alive read applies the configured head cap" {
@async.with_task_group(g => {
let head = b"GET /ok HTTP/1.1\r\nx-pad: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n"
let (r, w) = @pipe.pipe()
let feeder = g.spawn(() => {
w.write(head) catch {
_ => ()
}
w.close()
})
assert_eq(
next_head(
Inbox::new(r),
Config::new(max_head_size=64, timeout_keep_alive=3000),
)
is None,
true,
)
feeder.cancel()
// The same head under the default cap parses.
let (r2, w2) = @pipe.pipe()
let feeder2 = g.spawn(() => {
w2.write(head) catch {
_ => ()
}
w2.close()
})
guard next_head(Inbox::new(r2), Config::new(timeout_keep_alive=3000))
is Some(req) else {
fail("the default 16 KiB cap should accept a 130-byte head")
}
assert_eq(req.target, "/ok")
feeder2.cancel()
})
}
///|
/// RFC 7231 §7.1.1.2: an origin server with a clock sends `Date` on every response, and uvicorn
/// does. A cache in front of a server that omits it has to invent one.
async test "http1: every response carries a well-formed Date header" {
let resp = run_h1(h1_req("HTTP/1.1"))
assert_eq(bytes_has(resp, b"date: "), true)
// IMF-fixdate is fixed-width and always GMT, so the rendered value is exactly re-derivable.
assert_eq(bytes_has(resp, @utf8.encode(http_date_now())), true)
assert_eq(bytes_has(resp, b" GMT\r\n"), true)
}
///|
/// The self-built codec builds its own scope, so it needs its own forwarded-header handling: a
/// request that reached an HTTPS listener through a terminating proxy still has to reach the app
/// with the scheme the client actually used.
async test "http1: a trusted proxy's forwarded headers reach the self-built scope" {
let seen : Array[String] = []
let watcher : @moonasgi.AsgiApp = (scope, _receive, send) => {
guard scope is Http(hs) else { return }
seen.push(hs.scheme)
seen.push(
match hs.client {
Some((host, port)) => "\{host}:\{port}"
None => "no-client"
},
)
send(
@moonasgi.Event::HttpResponseStart(status=200, headers=[], trailers=false),
)
send(@moonasgi.Event::HttpResponseBody(body=b"ok", more_body=false))
}
let req = h1(
"GET", "HTTP/1.1", "x-forwarded-for: 203.0.113.7\r\nx-forwarded-proto: https\r\n",
)
let (req_r, req_w) = @pipe.pipe()
req_w.close()
let (_resp_r, resp_w) = @pipe.pipe()
// No peer address is known on this transport, so only the wildcard can trust the headers.
let _ = dispatch_http1(
watcher,
req,
Inbox::new(req_r),
resp_w,
scheme="http",
config=Config::new(forwarded_allow_ips=["*"]),
)
assert_eq(seen[0], "https")
assert_eq(seen[1], "203.0.113.7:0")
// The default trust set does not include an unknown peer, so nothing is rewritten.
let strict : Array[String] = []
let (req_r2, req_w2) = @pipe.pipe()
req_w2.close()
let (_resp_r2, resp_w2) = @pipe.pipe()
let _ = dispatch_http1(
watcher,
req,
Inbox::new(req_r2),
resp_w2,
scheme="http",
config=Config::new(forwarded_allow_ips=strict),
)
assert_eq(seen[2], "http")
assert_eq(seen[3], "no-client")
}
///|
/// `date_header=false` is uvicorn's `--no-date-header`: something in front is already dating the
/// response, and two `Date` headers is worse than one.
async test "http1: date_header=false leaves the Date header off" {
let resp = run_h1(h1_req("HTTP/1.1"), config=Config::new(date_header=false))
assert_eq(bytes_has(resp, b"date: "), false)
// The rest of the response is unchanged.
assert_eq(bytes_has(resp, b"content-length: 2"), true)
}
///|
/// An app that dates its own response keeps its value, and does not get a second header.
async test "http1: an app-supplied Date is not duplicated" {
let fixed = "Sun, 06 Nov 1994 08:49:37 GMT"
let resp = run_h1(h1_req("HTTP/1.1"), headers=[("date", fixed)])
assert_eq(bytes_has(resp, @utf8.encode("date: " + fixed)), true)
// Exactly one `date:` in the head — the app's.
assert_eq(count_bytes(resp, b"date: "), 1)
}
///|
/// How many times `needle` occurs in `hay` (test helper: a duplicate header is invisible to a
/// "contains" check).
fn count_bytes(hay : Bytes, needle : Bytes) -> Int {
let n = needle.length()
let mut found = 0
let mut i = 0
while i + n <= hay.length() {
let mut j = 0
while j < n && hay[i + j] == needle[j] {
j = j + 1
}
if j == n {
found = found + 1
i = i + n
} else {
i = i + 1
}
}
found
}
///|
/// uvicorn's `--h11-max-incomplete-event-size` is a knob, not a constant: a service behind a proxy
/// that adds a large header block has to be able to raise it, and one facing the open internet may
/// want it lower than the 16 KiB default.
async test "http1: the request-head cap is configurable, not fixed" {
// A head of roughly 130 bytes: under the 16 KiB default, over a 64-byte cap.
let head = b"GET /ok HTTP/1.1\r\nx-pad: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n\r\n"
let (r1, w1) = @pipe.pipe()
w1.write(head)
w1.close()
guard Inbox::new(r1).head(@http1.limits) is Some(req) else {
fail("the default cap should accept a 130-byte head")
}
assert_eq(req.target, "/ok")
let (r2, w2) = @pipe.pipe()
w2.write(head)
w2.close()
assert_eq(Inbox::new(r2).head(@http1.Limits::new(head=64)) is None, true)
}