///|
let cached_date_second : Ref[Int64] = Ref(0L)
///|
let cached_date_string : Ref[String] = Ref("")
///|
fn current_http_date() -> String {
let now = @async.now() / 1000L
if now != cached_date_second.val {
cached_date_second.val = now
cached_date_string.val = @httputil.format_http_date(now)
}
cached_date_string.val
}
///|
fn append_cookie_headers(
headers : Map[String, String],
cookies : Map[String, @cookie.CookieItem],
) -> Unit {
if cookies.is_empty() {
return
}
let cookie_lines = cookies
.values()
.map(cookie => cookie.to_string())
.to_array()
match cookie_lines {
[] => ()
[cookie] => headers.set("Set-Cookie", cookie)
_ => headers.set("Set-Cookie", cookie_lines.join("\r\nSet-Cookie: "))
}
}
///|
test "append_cookie_headers with empty cookies does nothing" {
let headers : Map[String, String] = Map([])
let cookies : Map[String, @cookie.CookieItem] = Map([])
append_cookie_headers(headers, cookies)
debug_inspect(headers.contains("Set-Cookie"), content="false")
}
///|
test "append_cookie_headers with single cookie sets header" {
let headers : Map[String, String] = Map([])
let cookies : Map[String, @cookie.CookieItem] = Map([])
cookies.set("session", {
name: "session",
value: "abc123",
max_age: None,
path: None,
domain: None,
secure: None,
http_only: None,
same_site: None,
})
append_cookie_headers(headers, cookies)
debug_inspect(
headers.get("Set-Cookie"),
content=(
#|Some("session=abc123")
),
)
}
///|
test "append_cookie_headers with multiple cookies joins with CRLF Set-Cookie" {
let headers : Map[String, String] = Map([])
let cookies : Map[String, @cookie.CookieItem] = Map([])
cookies.set("a", {
name: "a",
value: "1",
max_age: None,
path: None,
domain: None,
secure: None,
http_only: None,
same_site: None,
})
cookies.set("b", {
name: "b",
value: "2",
max_age: None,
path: None,
domain: None,
secure: None,
http_only: None,
same_site: None,
})
append_cookie_headers(headers, cookies)
debug_inspect(
headers.get("Set-Cookie"),
content=(
#|Some("a=1\r\nSet-Cookie: b=2")
),
)
}
///|
async fn send_raw_response_async(
conn : @http.ServerConnection,
response : HttpResponse,
headers : Map[String, String],
include_body : Bool,
) -> Unit {
if !@httputil.has_header_case_insensitive(headers, "Date") {
headers.set("Date", current_http_date())
}
if !@httputil.has_header_case_insensitive(headers, "Content-Length") {
headers.set("Content-Length", response.raw_body.length().to_string())
}
if !@httputil.has_header_case_insensitive(headers, "Connection") {
headers.set("Connection", "close")
}
let raw_response = Buffer()
raw_response.write_string_utf8("HTTP/1.1 ")
raw_response.write_string_utf8(response.status_code.to_int().to_string())
raw_response.write_string_utf8(" ")
raw_response.write_string_utf8(response.status_code.to_string())
raw_response.write_string_utf8("\r\n")
for pair in headers.to_array() {
let (key, value) = pair
raw_response.write_string_utf8(key)
raw_response.write_string_utf8(": ")
raw_response.write_string_utf8(value)
raw_response.write_string_utf8("\r\n")
}
raw_response.write_string_utf8("\r\n")
if include_body && !response.raw_body.is_empty() {
raw_response.write_bytes(response.raw_body)
}
conn.enter_passthrough_mode()
conn.write(raw_response.contents())
conn.flush()
conn.close()
}
///|
/// Applies a responder to a response and serializes pending cookies into
/// `Set-Cookie` headers. Shared by the live serving path
/// (`send_response_async`) and synthetic dispatch (`App::dispatch`) so the
/// two stay aligned: any bug in body materialization or cookie serialization
/// would otherwise have to be fixed twice (see commits bb22108, eb6f24b).
fn finalize_response_body(
response : HttpResponse,
responder : &Responder,
) -> Unit {
responder.options(response)
// Fast path: use output_bytes to avoid buffer allocation when possible
match responder.output_bytes() {
Some(bytes) => response.raw_body = bytes
None => {
let buffer = Buffer()
responder.output(buffer)
response.raw_body = buffer.to_bytes()
}
}
append_cookie_headers(response.headers, response.cookies)
}
///|
async fn send_response_async(
request : @http.Request,
conn : @http.ServerConnection,
response : HttpResponse,
responder : &Responder,
) -> Unit {
finalize_response_body(response, responder)
let headers = response.headers
if !@httputil.has_header_case_insensitive(headers, "Date") {
headers.set("Date", current_http_date())
}
if request.meth == Head {
send_raw_response_async(conn, response, headers, false)
return
}
if @httputil.has_header_case_insensitive(headers, "Content-Encoding") {
send_raw_response_async(conn, response, headers, true)
return
}
conn.send_response(
response.status_code.to_int(),
response.status_code.to_string(),
extra_headers=headers,
)
if !response.raw_body.is_empty() {
conn.write(response.raw_body)
}
conn.end_response()
}
///|
async fn send_response_and_close_async(
request : @http.Request,
conn : @http.ServerConnection,
response : HttpResponse,
) -> Unit {
let headers = response.headers
append_cookie_headers(headers, response.cookies)
if !@httputil.has_header_case_insensitive(headers, "Date") {
headers.set("Date", current_http_date())
}
if !@httputil.has_header_case_insensitive(headers, "Content-Length") {
headers.set("Content-Length", response.raw_body.length().to_string())
}
headers.set("Connection", "close")
if request.meth == Head {
send_raw_response_async(conn, response, headers, false)
return
}
conn.send_response(
response.status_code.to_int(),
response.status_code.to_string(),
extra_headers=headers,
)
if !response.raw_body.is_empty() {
conn.write(response.raw_body)
}
conn.end_response()
conn.close()
}
///|
async fn send_request_entity_too_large_async(
request : @http.Request,
conn : @http.ServerConnection,
) -> Unit {
let response = HttpResponse(status_code=RequestEntityTooLarge).body(
"Request Entity Too Large",
)
response.headers.set("Content-Type", "text/plain; charset=utf-8")
send_response_and_close_async(request, conn, response)
}
///|
async fn send_request_timeout_async(
request : @http.Request,
conn : @http.ServerConnection,
) -> Unit {
let response = HttpResponse(status_code=RequestTimeout).body(
"Request Timeout",
)
response.headers.set("Content-Type", "text/plain; charset=utf-8")
send_response_and_close_async(request, conn, response)
}
///|
async fn send_not_found_async(
request : @http.Request,
conn : @http.ServerConnection,
) -> Unit {
let response = HttpResponse(status_code=NotFound).body("Not Found")
response.headers.set("Content-Type", "text/plain; charset=utf-8")
send_response_and_close_async(request, conn, response)
}
///|
async fn send_gateway_timeout_async(
request : @http.Request,
conn : @http.ServerConnection,
) -> Unit {
let response = HttpResponse(status_code=GatewayTimeout).body(
"Gateway Timeout",
)
response.headers.set("Content-Type", "text/plain; charset=utf-8")
send_response_and_close_async(request, conn, response)
}