///|
pub(all) struct Response {
code : Int
reason : String
headers : Map[String, String]
cookies : Array[Cookie]
}
///|
pub struct Cookie {
name : String
value : String
path : String?
expires_raw : String?
max_age : Int64?
domain : String?
secure : Bool
http_only : Bool
extensions : Array[String]
} derive(@debug.Debug)
///|
#alias(new, deprecated)
pub fn Cookie::Cookie(
name : String,
value : String,
path? : String,
expires_raw? : String,
max_age? : Int64,
domain? : String,
secure? : Bool = false,
http_only? : Bool = false,
extensions? : Array[String] = [],
) -> Cookie {
{
name,
value,
path,
expires_raw,
max_age,
domain,
secure,
http_only,
extensions,
}
}
///|
fn Cookie::parse(line : StringView) -> Cookie raise {
guard line.find("=") is Some(name_end) else {
raise HttpError::InvalidUrl("invalid Set-Cookie header")
}
let name = line[:name_end].to_owned()
guard line[name_end + 1:].find(";") is Some(value_end) else {
Cookie(name, line[name_end + 1:].to_owned())
}
let value_end = name_end + 1 + value_end
let value = line[name_end + 1:value_end].to_owned()
let mut path = None
let mut expires_raw = None
let mut max_age = None
let mut domain = None
let mut secure = false
let mut http_only = false
let extensions = []
for parsed = value_end + 1 {
let attr_end = if line[parsed:].find(";") is Some(attr_end) {
parsed + attr_end
} else {
line.length()
}
let attr = line[parsed:attr_end].trim()
if attr.find("=") is Some(attr_name_end) {
let name = attr[:attr_name_end]
let value = attr[attr_name_end + 1:]
match name.to_lower() {
"path" => path = Some(value.to_owned())
"expires" => expires_raw = Some(value.to_owned())
"max-age" => {
let value = @string.parse_int64(value) catch {
_ => raise HttpError::InvalidUrl("invalid Set-Cookie header")
}
max_age = Some(value)
}
"domain" => domain = Some(value.to_owned())
_ => extensions.push(attr.to_owned())
}
} else {
match attr.to_lower() {
"secure" => secure = true
"httponly" => http_only = true
_ => extensions.push(attr.to_owned())
}
}
if attr_end == line.length() {
break
} else {
continue attr_end + 1
}
}
{
name,
value,
path,
expires_raw,
max_age,
domain,
secure,
http_only,
extensions,
}
}
///|
fn Cookie::to_header_value(cookie : Cookie) -> String {
let buf = StringBuilder::new()
buf.write_string(cookie.name)
buf.write_char('=')
buf.write_string(cookie.value)
if cookie.path is Some(path) {
buf.write_string("; Path=")
buf.write_string(path)
}
if cookie.expires_raw is Some(expires_raw) {
buf.write_string("; Expires=")
buf.write_string(expires_raw)
}
if cookie.max_age is Some(max_age) {
buf.write_string("; Max-Age=")
buf.write_string(max_age.to_string())
}
if cookie.domain is Some(domain) {
buf.write_string("; Domain=")
buf.write_string(domain)
}
if cookie.secure {
buf.write_string("; Secure")
}
if cookie.http_only {
buf.write_string("; HttpOnly")
}
for ext in cookie.extensions {
buf.write_string("; ")
buf.write_string(ext)
}
buf.to_string()
}
///|
fn parse_cookies_json(json_str : String) -> Array[Cookie] {
let parsed = @json.parse(json_str) catch { _ => return [] }
parse_cookies_value(parsed)
}
///|
fn parse_headers_json(json_str : String) -> Map[String, String] {
let headers : Map[String, String] = Map([])
let parsed = @json.parse(json_str) catch { _ => return headers }
match parsed {
Object(map) =>
map.each(fn(k, v) {
match v {
String(s) => headers[normalize_header_key(k)] = s
_ => ()
}
})
_ => ()
}
headers
}
///|
fn has_ascii_upper(value : String) -> Bool {
for i in 0..= 'A'.to_int() && c <= 'Z'.to_int() {
return true
}
}
false
}
///|
fn normalize_header_key(value : String) -> String {
if has_ascii_upper(value) {
value.to_lower()
} else {
value
}
}
///|
fn parse_cookies_value(value : Json) -> Array[Cookie] {
let cookies : Array[Cookie] = []
let parsed = value
match parsed {
Array(items) =>
for item in items {
match item {
String(s) =>
try cookies.push(Cookie::parse(s)) catch {
_ => ()
} noraise {
_ => ()
}
_ => ()
}
}
_ => ()
}
cookies
}
///|
/// Append `s` to `buf` as a JSON string literal (quotes + escape).
///
/// Replaces a previous `Json::string(s).stringify()` round-trip: building
/// a Json AST + a fresh `String` per value was the bulk of http's hot
/// path on the JS target. The fast path here just walks the source
/// chars; only `"`, `\`, and ASCII controls trigger an escape branch.
fn write_json_string(buf : StringBuilder, s : String) -> Unit {
buf.write_char('"')
let len = s.length()
let mut i = 0
while i < len {
let c = s.get_char(i).unwrap_or('\u{0}')
let code = c.to_int()
if code == '"'.to_int() {
buf.write_string("\\\"")
} else if code == '\\'.to_int() {
buf.write_string("\\\\")
} else if code == '\n'.to_int() {
buf.write_string("\\n")
} else if code == '\r'.to_int() {
buf.write_string("\\r")
} else if code == '\t'.to_int() {
buf.write_string("\\t")
} else if code == 0x08 {
buf.write_string("\\b")
} else if code == 0x0C {
buf.write_string("\\f")
} else if code < 0x20 {
buf.write_string("\\u00")
let hi = code >> 4
let lo = code & 0xF
buf.write_char(hex_digit(hi))
buf.write_char(hex_digit(lo))
} else {
buf.write_char(c)
}
i = i + 1
}
buf.write_char('"')
}
///|
fn hex_digit(n : Int) -> Char {
if n < 10 {
(n + '0'.to_int()).unsafe_to_char()
} else {
(n - 10 + 'a'.to_int()).unsafe_to_char()
}
}
///|
fn cookies_to_json(cookies : Array[Cookie]) -> String {
let buf = StringBuilder::new()
buf.write_char('[')
let mut first = true
for cookie in cookies {
if !first {
buf.write_char(',')
}
first = false
write_json_string(buf, cookie.to_header_value())
}
buf.write_char(']')
buf.to_string()
}
///|
pub(all) struct Addr {
text : String
port : Int
}
///|
pub(all) enum RequestMethod {
Get
Head
Post
Put
Delete
Connect
Options
Trace
Patch
} derive(Eq, Compare, Hash, @debug.Debug)
///|
pub impl Show for RequestMethod with fn output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
pub(all) struct Request {
meth : RequestMethod
path : String
headers : Map[String, String]
}
///|
pub suberror HttpError {
NetworkError(String)
InvalidUrl(String)
TimeoutError
NotSupported
} derive(@debug.Debug)
///|
pub impl Show for HttpError with fn output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
fn classify_http_error(message : String) -> HttpError {
let lower = message.to_lower()
if lower.contains("timeout") {
TimeoutError
} else if lower.contains("not supported") || lower.contains("unsupported") {
NotSupported
} else {
NetworkError(message)
}
}
///|
fn response_has_no_body(code : Int, meth : RequestMethod?) -> Bool {
match meth {
Some(Head) => true
Some(Connect) => code >= 200 && code < 300
_ =>
(code >= 100 && code < 200) || code == 204 || code == 205 || code == 304
}
}
///|
fn headers_to_json(headers : Map[String, String]) -> String {
let buf = StringBuilder::new()
buf.write_char('{')
let mut first = true
for k, v in headers {
if !first {
buf.write_char(',')
}
first = false
write_json_string(buf, k)
buf.write_char(':')
write_json_string(buf, v)
}
buf.write_char('}')
buf.to_string()
}