net.http: add header_from_map and join functions (#10747)
parent
581280e6fc
commit
3039092b89
|
@ -353,22 +353,50 @@ pub fn new_header(kvs ...HeaderConfig) Header {
|
|||
return h
|
||||
}
|
||||
|
||||
// Append a value to the header key.
|
||||
// new_header_from_map creates a Header from key value pairs
|
||||
pub fn new_header_from_map(kvs map[CommonHeader]string) Header {
|
||||
mut h := new_header()
|
||||
h.add_map(kvs)
|
||||
return h
|
||||
}
|
||||
|
||||
// new_custom_header_from_map creates a Header from string key value pairs
|
||||
pub fn new_custom_header_from_map(kvs map[string]string) ?Header {
|
||||
mut h := new_header()
|
||||
h.add_custom_map(kvs) ?
|
||||
return h
|
||||
}
|
||||
|
||||
// add appends a value to the header key.
|
||||
pub fn (mut h Header) add(key CommonHeader, value string) {
|
||||
k := key.str()
|
||||
h.data[k] << value
|
||||
h.add_key(k)
|
||||
}
|
||||
|
||||
// Append a value to a custom header key. This function will return an error
|
||||
// if the key contains invalid header characters.
|
||||
// add_custom appends a value to a custom header key. This function will
|
||||
// return an error if the key contains invalid header characters.
|
||||
pub fn (mut h Header) add_custom(key string, value string) ? {
|
||||
is_valid(key) ?
|
||||
h.data[key] << value
|
||||
h.add_key(key)
|
||||
}
|
||||
|
||||
// Sets the key-value pair. This function will clear any other values
|
||||
// add_map appends the value for each header key.
|
||||
pub fn (mut h Header) add_map(kvs map[CommonHeader]string) {
|
||||
for k, v in kvs {
|
||||
h.add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// add_custom_map appends the value for each custom header key.
|
||||
pub fn (mut h Header) add_custom_map(kvs map[string]string) ? {
|
||||
for k, v in kvs {
|
||||
h.add_custom(k, v) ?
|
||||
}
|
||||
}
|
||||
|
||||
// set sets the key-value pair. This function will clear any other values
|
||||
// that exist for the CommonHeader.
|
||||
pub fn (mut h Header) set(key CommonHeader, value string) {
|
||||
k := key.str()
|
||||
|
@ -376,21 +404,22 @@ pub fn (mut h Header) set(key CommonHeader, value string) {
|
|||
h.add_key(k)
|
||||
}
|
||||
|
||||
// Sets the key-value pair for a custom header key. This function will
|
||||
// clear any other values that exist for the header. This function will
|
||||
// return an error if the key contains invalid header characters.
|
||||
// set_custom sets the key-value pair for a custom header key. This
|
||||
// function will clear any other values that exist for the header. This
|
||||
// function will return an error if the key contains invalid header
|
||||
// characters.
|
||||
pub fn (mut h Header) set_custom(key string, value string) ? {
|
||||
is_valid(key) ?
|
||||
h.data[key] = [value]
|
||||
h.add_key(key)
|
||||
}
|
||||
|
||||
// Delete all values for a key.
|
||||
// delete deletes all values for a key.
|
||||
pub fn (mut h Header) delete(key CommonHeader) {
|
||||
h.delete_custom(key.str())
|
||||
}
|
||||
|
||||
// Delete all values for a custom header key.
|
||||
// delete_custom deletes all values for a custom header key.
|
||||
pub fn (mut h Header) delete_custom(key string) {
|
||||
h.data.delete(key)
|
||||
|
||||
|
@ -405,7 +434,8 @@ pub struct HeaderCoerceConfig {
|
|||
canonicalize bool
|
||||
}
|
||||
|
||||
// Coerce data by joining keys that match case-insensitively into one entry
|
||||
// coerce coerces data in the Header by joining keys that match
|
||||
// case-insensitively into one entry.
|
||||
pub fn (mut h Header) coerce(flags ...HeaderCoerceConfig) {
|
||||
canon := flags.any(it.canonicalize)
|
||||
|
||||
|
@ -428,7 +458,7 @@ pub fn (mut h Header) coerce(flags ...HeaderCoerceConfig) {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns whether the header key exists in the map.
|
||||
// contains returns whether the header key exists in the map.
|
||||
pub fn (h Header) contains(key CommonHeader) bool {
|
||||
return h.contains_custom(key.str())
|
||||
}
|
||||
|
@ -437,7 +467,7 @@ pub struct HeaderQueryConfig {
|
|||
exact bool
|
||||
}
|
||||
|
||||
// Returns whether the custom header key exists in the map.
|
||||
// contains_custom returns whether the custom header key exists in the map.
|
||||
pub fn (h Header) contains_custom(key string, flags ...HeaderQueryConfig) bool {
|
||||
if flags.any(it.exact) {
|
||||
return key in h.data
|
||||
|
@ -445,14 +475,14 @@ pub fn (h Header) contains_custom(key string, flags ...HeaderQueryConfig) bool {
|
|||
return key.to_lower() in h.keys
|
||||
}
|
||||
|
||||
// Gets the first value for the CommonHeader, or none if the key does
|
||||
// not exist.
|
||||
// get gets the first value for the CommonHeader, or none if the key
|
||||
// does not exist.
|
||||
pub fn (h Header) get(key CommonHeader) ?string {
|
||||
return h.get_custom(key.str())
|
||||
}
|
||||
|
||||
// Gets the first value for the custom header, or none if the key does
|
||||
// not exist.
|
||||
// get_custom gets the first value for the custom header, or none if
|
||||
// the key does not exist.
|
||||
pub fn (h Header) get_custom(key string, flags ...HeaderQueryConfig) ?string {
|
||||
mut data_key := key
|
||||
if !flags.any(it.exact) {
|
||||
|
@ -469,7 +499,8 @@ pub fn (h Header) get_custom(key string, flags ...HeaderQueryConfig) ?string {
|
|||
return h.data[data_key][0]
|
||||
}
|
||||
|
||||
// Gets the first value of the header starting with key, or none if the key does not exist.
|
||||
// starting_with gets the first header starting with key, or none if
|
||||
// the key does not exist.
|
||||
pub fn (h Header) starting_with(key string) ?string {
|
||||
for k, _ in h.data {
|
||||
if k.starts_with(key) {
|
||||
|
@ -479,12 +510,12 @@ pub fn (h Header) starting_with(key string) ?string {
|
|||
return none
|
||||
}
|
||||
|
||||
// Gets all values for the CommonHeader.
|
||||
// values gets all values for the CommonHeader.
|
||||
pub fn (h Header) values(key CommonHeader) []string {
|
||||
return h.custom_values(key.str())
|
||||
}
|
||||
|
||||
// Gets all values for the custom header.
|
||||
// custom_values gets all values for the custom header.
|
||||
pub fn (h Header) custom_values(key string, flags ...HeaderQueryConfig) []string {
|
||||
if flags.any(it.exact) {
|
||||
return h.data[key]
|
||||
|
@ -497,7 +528,7 @@ pub fn (h Header) custom_values(key string, flags ...HeaderQueryConfig) []string
|
|||
return values
|
||||
}
|
||||
|
||||
// Gets all header keys as strings
|
||||
// keys gets all header keys as strings
|
||||
pub fn (h Header) keys() []string {
|
||||
return h.data.keys()
|
||||
}
|
||||
|
@ -508,8 +539,8 @@ pub struct HeaderRenderConfig {
|
|||
canonicalize bool
|
||||
}
|
||||
|
||||
// Renders the Header into a string for use in sending
|
||||
// HTTP requests. All header lines will end in `\r\n`
|
||||
// render renders the Header into a string for use in sending HTTP
|
||||
// requests. All header lines will end in `\r\n`
|
||||
[manualfree]
|
||||
pub fn (h Header) render(flags HeaderRenderConfig) string {
|
||||
// estimate ~48 bytes per header
|
||||
|
@ -556,7 +587,24 @@ pub fn (h Header) render(flags HeaderRenderConfig) string {
|
|||
return res
|
||||
}
|
||||
|
||||
// Canonicalize an HTTP header key
|
||||
// join combines two Header structs into a new Header struct
|
||||
pub fn (h Header) join(other Header) Header {
|
||||
mut combined := Header{
|
||||
data: h.data.clone()
|
||||
keys: h.keys.clone()
|
||||
}
|
||||
for k in other.keys() {
|
||||
for v in other.custom_values(k, exact: true) {
|
||||
combined.add_custom(k, v) or {
|
||||
// panic because this should never fail
|
||||
panic('unexpected error: $err')
|
||||
}
|
||||
}
|
||||
}
|
||||
return combined
|
||||
}
|
||||
|
||||
// canonicalize canonicalizes an HTTP header key
|
||||
// Common headers are determined by the common_header_map
|
||||
// Custom headers are capitalized on the first letter and any letter after a '-'
|
||||
// NOTE: Assumes sl is lowercase, since the caller usually already has the lowercase key
|
||||
|
@ -584,7 +632,7 @@ struct HeaderKeyError {
|
|||
invalid_char byte
|
||||
}
|
||||
|
||||
// Checks if the header token is valid
|
||||
// is_valid checks if the header token contains all valid bytes
|
||||
fn is_valid(header string) ? {
|
||||
for _, c in header {
|
||||
if int(c) >= 128 || !is_token(c) {
|
||||
|
@ -598,7 +646,7 @@ fn is_valid(header string) ? {
|
|||
}
|
||||
}
|
||||
|
||||
// Checks if the byte is valid for a header token
|
||||
// is_token checks if the byte is valid for a header token
|
||||
fn is_token(b byte) bool {
|
||||
return match b {
|
||||
33, 35...39, 42, 43, 45, 46, 48...57, 65...90, 94...122, 124, 126 { true }
|
||||
|
@ -606,8 +654,8 @@ fn is_token(b byte) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Returns the headers string as seen in HTTP/1.1 requests
|
||||
// Key order is not guaranteed
|
||||
// str returns the headers string as seen in HTTP/1.1 requests.
|
||||
// Key order is not guaranteed.
|
||||
pub fn (h Header) str() string {
|
||||
return h.render(version: .v1_1)
|
||||
}
|
||||
|
|
|
@ -274,3 +274,52 @@ fn test_str() ? {
|
|||
assert h.str() == 'Accept: text/html,image/jpeg\r\nX-custom: Hello\r\n'
|
||||
|| h.str() == 'X-custom: Hello\r\nAccept:text/html,image/jpeg\r\n'
|
||||
}
|
||||
|
||||
fn test_header_from_map() ? {
|
||||
h := new_header_from_map(map{
|
||||
CommonHeader.accept: 'nothing'
|
||||
CommonHeader.expires: 'yesterday'
|
||||
})
|
||||
assert h.contains(.accept)
|
||||
assert h.contains(.expires)
|
||||
assert h.get(.accept) or { '' } == 'nothing'
|
||||
assert h.get(.expires) or { '' } == 'yesterday'
|
||||
}
|
||||
|
||||
fn test_custom_header_from_map() ? {
|
||||
h := new_custom_header_from_map(map{
|
||||
'Server': 'VWeb'
|
||||
'foo': 'bar'
|
||||
}) ?
|
||||
assert h.contains_custom('server')
|
||||
assert h.contains_custom('foo')
|
||||
assert h.get_custom('server') or { '' } == 'VWeb'
|
||||
assert h.get_custom('foo') or { '' } == 'bar'
|
||||
}
|
||||
|
||||
fn test_header_join() ? {
|
||||
h1 := new_header_from_map(map{
|
||||
CommonHeader.accept: 'nothing'
|
||||
CommonHeader.expires: 'yesterday'
|
||||
})
|
||||
h2 := new_custom_header_from_map(map{
|
||||
'Server': 'VWeb'
|
||||
'foo': 'bar'
|
||||
}) ?
|
||||
h3 := h1.join(h2)
|
||||
// h1 is unchanged
|
||||
assert h1.contains(.accept)
|
||||
assert h1.contains(.expires)
|
||||
assert !h1.contains_custom('Server')
|
||||
assert !h1.contains_custom('foo')
|
||||
// h2 is unchanged
|
||||
assert !h2.contains(.accept)
|
||||
assert !h2.contains(.expires)
|
||||
assert h2.contains_custom('Server')
|
||||
assert h2.contains_custom('foo')
|
||||
// h3 has all four headers
|
||||
assert h3.contains(.accept)
|
||||
assert h3.contains(.expires)
|
||||
assert h3.contains_custom('Server')
|
||||
assert h3.contains_custom('foo')
|
||||
}
|
||||
|
|
|
@ -13,56 +13,37 @@ import time
|
|||
|
||||
pub const (
|
||||
methods_with_form = [http.Method.post, .put, .patch]
|
||||
headers_close = http.new_custom_header_from_map(map{
|
||||
'Server': 'VWeb'
|
||||
http.CommonHeader.connection.str(): 'close'
|
||||
}) or { panic('should never fail') }
|
||||
|
||||
http_400 = http.Response{
|
||||
version: .v1_1
|
||||
status_code: 400
|
||||
text: '400 Bad Request'
|
||||
header: fn () http.Header {
|
||||
mut h := http.new_header()
|
||||
h.add(.content_type, 'text/plain')
|
||||
h.add(.content_length, '15')
|
||||
close := headers_close()
|
||||
for k in close.keys() {
|
||||
for v in close.custom_values(k) {
|
||||
h.add_custom(k, v) or {}
|
||||
}
|
||||
}
|
||||
return h
|
||||
}()
|
||||
header: http.new_header_from_map(map{
|
||||
http.CommonHeader.content_type: 'text/plain'
|
||||
http.CommonHeader.content_length: '15'
|
||||
}).join(headers_close)
|
||||
}
|
||||
http_404 = http.Response{
|
||||
version: .v1_1
|
||||
status_code: 404
|
||||
text: '404 Not Found'
|
||||
header: fn () http.Header {
|
||||
mut h := http.new_header()
|
||||
h.add(.content_type, 'text/plain')
|
||||
h.add(.content_length, '13')
|
||||
close := headers_close()
|
||||
for k in close.keys() {
|
||||
for v in close.custom_values(k) {
|
||||
h.add_custom(k, v) or {}
|
||||
}
|
||||
}
|
||||
return h
|
||||
}()
|
||||
header: http.new_header_from_map(map{
|
||||
http.CommonHeader.content_type: 'text/plain'
|
||||
http.CommonHeader.content_length: '13'
|
||||
}).join(headers_close)
|
||||
}
|
||||
http_500 = http.Response{
|
||||
version: .v1_1
|
||||
status_code: 500
|
||||
text: '500 Internal Server Error'
|
||||
header: fn () http.Header {
|
||||
mut h := http.new_header()
|
||||
h.add(.content_type, 'text/plain')
|
||||
h.add(.content_length, '25')
|
||||
close := headers_close()
|
||||
for k in close.keys() {
|
||||
for v in close.custom_values(k) {
|
||||
h.add_custom(k, v) or {}
|
||||
}
|
||||
}
|
||||
return h
|
||||
}()
|
||||
header: http.new_header_from_map(map{
|
||||
http.CommonHeader.content_type: 'text/plain'
|
||||
http.CommonHeader.content_length: '25'
|
||||
}).join(headers_close)
|
||||
}
|
||||
mime_types = map{
|
||||
'.css': 'text/css; charset=utf-8'
|
||||
|
@ -161,7 +142,7 @@ pub fn (mut ctx Context) send_response_to_client(mimetype string, res string) bo
|
|||
}
|
||||
sb.write_string(ctx.headers)
|
||||
sb.write_string('\r\n')
|
||||
sb.write_string(headers_close().str())
|
||||
sb.write_string(vweb.headers_close.str())
|
||||
sb.write_string('\r\n')
|
||||
if ctx.chunked_transfer {
|
||||
mut i := 0
|
||||
|
@ -236,7 +217,7 @@ pub fn (mut ctx Context) redirect(url string) Result {
|
|||
return Result{}
|
||||
}
|
||||
ctx.done = true
|
||||
send_string(mut ctx.conn, 'HTTP/1.1 302 Found\r\nLocation: $url$ctx.headers\r\n$headers_close().str()\r\n') or {
|
||||
send_string(mut ctx.conn, 'HTTP/1.1 302 Found\r\nLocation: $url$ctx.headers\r\n$vweb.headers_close\r\n') or {
|
||||
return Result{}
|
||||
}
|
||||
return Result{}
|
||||
|
@ -707,9 +688,3 @@ pub type RawHtml = string
|
|||
fn send_string(mut conn net.TcpConn, s string) ? {
|
||||
conn.write(s.bytes()) ?
|
||||
}
|
||||
|
||||
fn headers_close() http.Header {
|
||||
mut h := http.new_header(key: .connection, value: 'close')
|
||||
h.add_custom('Server', 'VWeb') or {}
|
||||
return h
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue