v2 / vlib / net / http / header.v
827 lines · 778 sloc · 22.34 KB · 45545c2fda3dfafa31fb7341b31b786ad143e67d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module http
5
6import strings
7import arrays
8
9struct HeaderKV {
10 key string
11 value string
12}
13
14pub const max_headers = 50
15
16// Header represents the key-value pairs in an HTTP header
17pub struct Header {
18pub mut:
19 // data map[string][]string
20 data [max_headers]HeaderKV
21mut:
22 cur_pos int
23 // map of lowercase header keys to their original keys
24 // in order of appearance
25 // keys map[string][]string
26}
27
28// CommonHeader is an enum of the most common HTTP headers
29pub enum CommonHeader {
30 accept
31 accept_ch
32 accept_charset
33 accept_ch_lifetime
34 accept_encoding
35 accept_language
36 accept_patch
37 accept_post
38 accept_ranges
39 access_control_allow_credentials
40 access_control_allow_headers
41 access_control_allow_methods
42 access_control_allow_origin
43 access_control_expose_headers
44 access_control_max_age
45 access_control_request_headers
46 access_control_request_method
47 age
48 allow
49 alt_svc
50 authorization
51 authority
52 cache_control
53 clear_site_data
54 connection
55 content_disposition
56 content_encoding
57 content_language
58 content_length
59 content_location
60 content_range
61 content_security_policy
62 content_security_policy_report_only
63 content_type
64 cookie
65 cross_origin_embedder_policy
66 cross_origin_opener_policy
67 cross_origin_resource_policy
68 date
69 device_memory
70 digest
71 dnt
72 early_data
73 etag
74 expect
75 expect_ct
76 expires
77 feature_policy
78 forwarded
79 from
80 host
81 if_match
82 if_modified_since
83 if_none_match
84 if_range
85 if_unmodified_since
86 index
87 keep_alive
88 large_allocation
89 last_modified
90 link
91 location
92 nel
93 origin
94 pragma
95 proxy_authenticate
96 proxy_authorization
97 range
98 referer
99 referrer_policy
100 retry_after
101 save_data
102 sec_fetch_dest
103 sec_fetch_mode
104 sec_fetch_site
105 sec_fetch_user
106 sec_websocket_accept
107 sec_websocket_key
108 server
109 server_timing
110 set_cookie
111 sourcemap
112 strict_transport_security
113 te
114 timing_allow_origin
115 tk
116 trailer
117 transfer_encoding
118 upgrade
119 upgrade_insecure_requests
120 user_agent
121 vary
122 via
123 want_digest
124 warning
125 www_authenticate
126 x_content_type_options
127 x_dns_prefetch_control
128 x_forwarded_for
129 x_forwarded_host
130 x_forwarded_proto
131 x_frame_options
132 x_xss_protection
133}
134
135pub fn (h CommonHeader) str() string {
136 return match h {
137 .accept { 'Accept' }
138 .accept_ch { 'Accept-CH' }
139 .accept_charset { 'Accept-Charset' }
140 .accept_ch_lifetime { 'Accept-CH-Lifetime' }
141 .accept_encoding { 'Accept-Encoding' }
142 .accept_language { 'Accept-Language' }
143 .accept_patch { 'Accept-Patch' }
144 .accept_post { 'Accept-Post' }
145 .accept_ranges { 'Accept-Ranges' }
146 .access_control_allow_credentials { 'Access-Control-Allow-Credentials' }
147 .access_control_allow_headers { 'Access-Control-Allow-Headers' }
148 .access_control_allow_methods { 'Access-Control-Allow-Methods' }
149 .access_control_allow_origin { 'Access-Control-Allow-Origin' }
150 .access_control_expose_headers { 'Access-Control-Expose-Headers' }
151 .access_control_max_age { 'Access-Control-Max-Age' }
152 .access_control_request_headers { 'Access-Control-Request-Headers' }
153 .access_control_request_method { 'Access-Control-Request-Method' }
154 .age { 'Age' }
155 .allow { 'Allow' }
156 .alt_svc { 'Alt-Svc' }
157 .authorization { 'Authorization' }
158 .authority { 'Authority' }
159 .cache_control { 'Cache-Control' }
160 .clear_site_data { 'Clear-Site-Data' }
161 .connection { 'Connection' }
162 .content_disposition { 'Content-Disposition' }
163 .content_encoding { 'Content-Encoding' }
164 .content_language { 'Content-Language' }
165 .content_length { 'Content-Length' }
166 .content_location { 'Content-Location' }
167 .content_range { 'Content-Range' }
168 .content_security_policy { 'Content-Security-Policy' }
169 .content_security_policy_report_only { 'Content-Security-Policy-Report-Only' }
170 .content_type { 'Content-Type' }
171 .cookie { 'Cookie' }
172 .cross_origin_embedder_policy { 'Cross-Origin-Embedder-Policy' }
173 .cross_origin_opener_policy { 'Cross-Origin-Opener-Policy' }
174 .cross_origin_resource_policy { 'Cross-Origin-Resource-Policy' }
175 .date { 'Date' }
176 .device_memory { 'Device-Memory' }
177 .digest { 'Digest' }
178 .dnt { 'DNT' }
179 .early_data { 'Early-Data' }
180 .etag { 'ETag' }
181 .expect { 'Expect' }
182 .expect_ct { 'Expect-CT' }
183 .expires { 'Expires' }
184 .feature_policy { 'Feature-Policy' }
185 .forwarded { 'Forwarded' }
186 .from { 'From' }
187 .host { 'Host' }
188 .if_match { 'If-Match' }
189 .if_modified_since { 'If-Modified-Since' }
190 .if_none_match { 'If-None-Match' }
191 .if_range { 'If-Range' }
192 .if_unmodified_since { 'If-Unmodified-Since' }
193 .index { 'Index' }
194 .keep_alive { 'Keep-Alive' }
195 .large_allocation { 'Large-Allocation' }
196 .last_modified { 'Last-Modified' }
197 .link { 'Link' }
198 .location { 'Location' }
199 .nel { 'NEL' }
200 .origin { 'Origin' }
201 .pragma { 'Pragma' }
202 .proxy_authenticate { 'Proxy-Authenticate' }
203 .proxy_authorization { 'Proxy-Authorization' }
204 .range { 'Range' }
205 .referer { 'Referer' }
206 .referrer_policy { 'Referrer-Policy' }
207 .retry_after { 'Retry-After' }
208 .save_data { 'Save-Data' }
209 .sec_fetch_dest { 'Sec-Fetch-Dest' }
210 .sec_fetch_mode { 'Sec-Fetch-Mode' }
211 .sec_fetch_site { 'Sec-Fetch-Site' }
212 .sec_fetch_user { 'Sec-Fetch-User' }
213 .sec_websocket_accept { 'Sec-WebSocket-Accept' }
214 .sec_websocket_key { 'Sec-WebSocket-Key' }
215 .server { 'Server' }
216 .server_timing { 'Server-Timing' }
217 .set_cookie { 'Set-Cookie' }
218 .sourcemap { 'SourceMap' }
219 .strict_transport_security { 'Strict-Transport-Security' }
220 .te { 'TE' }
221 .timing_allow_origin { 'Timing-Allow-Origin' }
222 .tk { 'Tk' }
223 .trailer { 'Trailer' }
224 .transfer_encoding { 'Transfer-Encoding' }
225 .upgrade { 'Upgrade' }
226 .upgrade_insecure_requests { 'Upgrade-Insecure-Requests' }
227 .user_agent { 'User-Agent' }
228 .vary { 'Vary' }
229 .via { 'Via' }
230 .want_digest { 'Want-Digest' }
231 .warning { 'Warning' }
232 .www_authenticate { 'WWW-Authenticate' }
233 .x_content_type_options { 'X-Content-Type-Options' }
234 .x_dns_prefetch_control { 'X-DNS-Prefetch-Control' }
235 .x_forwarded_for { 'X-Forwarded-For' }
236 .x_forwarded_host { 'X-Forwarded-Host' }
237 .x_forwarded_proto { 'X-Forwarded-Proto' }
238 .x_frame_options { 'X-Frame-Options' }
239 .x_xss_protection { 'X-XSS-Protection' }
240 }
241}
242
243const common_header_map = {
244 'accept': CommonHeader.accept
245 'accept-ch': .accept_ch
246 'accept-charset': .accept_charset
247 'accept-ch-lifetime': .accept_ch_lifetime
248 'accept-encoding': .accept_encoding
249 'accept-language': .accept_language
250 'accept-patch': .accept_patch
251 'accept-post': .accept_post
252 'accept-ranges': .accept_ranges
253 'access-control-allow-credentials': .access_control_allow_credentials
254 'access-control-allow-headers': .access_control_allow_headers
255 'access-control-allow-methods': .access_control_allow_methods
256 'access-control-allow-origin': .access_control_allow_origin
257 'access-control-expose-headers': .access_control_expose_headers
258 'access-control-max-age': .access_control_max_age
259 'access-control-request-headers': .access_control_request_headers
260 'access-control-request-method': .access_control_request_method
261 'age': .age
262 'allow': .allow
263 'alt-svc': .alt_svc
264 'authorization': .authorization
265 'cache-control': .cache_control
266 'clear-site-data': .clear_site_data
267 'connection': .connection
268 'content-disposition': .content_disposition
269 'content-encoding': .content_encoding
270 'content-language': .content_language
271 'content-length': .content_length
272 'content-location': .content_location
273 'content-range': .content_range
274 'content-security-policy': .content_security_policy
275 'content-security-policy-report-only': .content_security_policy_report_only
276 'content-type': .content_type
277 'cookie': .cookie
278 'cross-origin-embedder-policy': .cross_origin_embedder_policy
279 'cross-origin-opener-policy': .cross_origin_opener_policy
280 'cross-origin-resource-policy': .cross_origin_resource_policy
281 'date': .date
282 'device-memory': .device_memory
283 'digest': .digest
284 'dnt': .dnt
285 'early-data': .early_data
286 'etag': .etag
287 'expect': .expect
288 'expect-ct': .expect_ct
289 'expires': .expires
290 'feature-policy': .feature_policy
291 'forwarded': .forwarded
292 'from': .from
293 'host': .host
294 'if-match': .if_match
295 'if-modified-since': .if_modified_since
296 'if-none-match': .if_none_match
297 'if-range': .if_range
298 'if-unmodified-since': .if_unmodified_since
299 'index': .index
300 'keep-alive': .keep_alive
301 'large-allocation': .large_allocation
302 'last-modified': .last_modified
303 'link': .link
304 'location': .location
305 'nel': .nel
306 'origin': .origin
307 'pragma': .pragma
308 'proxy-authenticate': .proxy_authenticate
309 'proxy-authorization': .proxy_authorization
310 'range': .range
311 'referer': .referer
312 'referrer-policy': .referrer_policy
313 'retry-after': .retry_after
314 'save-data': .save_data
315 'sec-fetch-dest': .sec_fetch_dest
316 'sec-fetch-mode': .sec_fetch_mode
317 'sec-fetch-site': .sec_fetch_site
318 'sec-fetch-user': .sec_fetch_user
319 'sec-websocket-accept': .sec_websocket_accept
320 'sec_websocket_key': .sec_websocket_key
321 'server': .server
322 'server-timing': .server_timing
323 'set-cookie': .set_cookie
324 'sourcemap': .sourcemap
325 'strict-transport-security': .strict_transport_security
326 'te': .te
327 'timing-allow-origin': .timing_allow_origin
328 'tk': .tk
329 'trailer': .trailer
330 'transfer-encoding': .transfer_encoding
331 'upgrade': .upgrade
332 'upgrade-insecure-requests': .upgrade_insecure_requests
333 'user-agent': .user_agent
334 'vary': .vary
335 'via': .via
336 'want-digest': .want_digest
337 'warning': .warning
338 'www-authenticate': .www_authenticate
339 'x-content-type-options': .x_content_type_options
340 'x-dns-prefetch-control': .x_dns_prefetch_control
341 'x-forwarded-for': .x_forwarded_for
342 'x-forwarded-host': .x_forwarded_host
343 'x-forwarded-proto': .x_forwarded_proto
344 'x-frame-options': .x_frame_options
345 'x-xss-protection': .x_xss_protection
346}
347
348pub fn (mut h Header) free() {
349 unsafe {
350 // h.data.free()
351 // h.keys.free()
352 }
353}
354
355pub struct HeaderConfig {
356pub:
357 key CommonHeader
358 value string
359}
360
361@[inline]
362fn header_lower_ascii_byte(b u8) u8 {
363 if b >= `A` && b <= `Z` {
364 return b + 32
365 }
366 return b
367}
368
369fn header_key_eq(a string, b string) bool {
370 if a.len != b.len {
371 return false
372 }
373 for i in 0 .. a.len {
374 if header_lower_ascii_byte(a[i]) != header_lower_ascii_byte(b[i]) {
375 return false
376 }
377 }
378 return true
379}
380
381// Create a new Header object
382pub fn new_header(kvs ...HeaderConfig) Header {
383 mut h := Header{
384 // data: map[string][]string{}
385 }
386 for i, kv in kvs {
387 h.data[i] = HeaderKV{kv.key.str(), kv.value}
388 // h.add(kv.key, kv.value)
389 }
390 h.cur_pos = kvs.len
391 return h
392}
393
394// new_header_from_map creates a Header from key value pairs
395pub fn new_header_from_map(kvs map[CommonHeader]string) Header {
396 mut h := new_header()
397 h.add_map(kvs)
398 return h
399}
400
401// new_custom_header_from_map creates a Header from string key value pairs
402pub fn new_custom_header_from_map(kvs map[string]string) !Header {
403 mut h := new_header()
404 h.add_custom_map(kvs)!
405 return h
406}
407
408// add appends a value to the header key.
409pub fn (mut h Header) add(key CommonHeader, value string) {
410 k := key.str()
411 // h.data[k] << value
412 h.data[h.cur_pos] = HeaderKV{k, value}
413 h.cur_pos++
414 // h.add_key(k)
415}
416
417// add_custom appends a value to a custom header key. This function will
418// return an error if the key contains invalid header characters.
419pub fn (mut h Header) add_custom(key string, value string) ! {
420 is_valid(key)!
421 // h.data[key] << value
422 h.data[h.cur_pos] = HeaderKV{key, value}
423 h.cur_pos++
424 // h.add_key(key)
425}
426
427// add_map appends the value for each header key.
428pub fn (mut h Header) add_map(kvs map[CommonHeader]string) {
429 for k, v in kvs {
430 h.add(k, v)
431 }
432}
433
434// add_custom_map appends the value for each custom header key.
435pub fn (mut h Header) add_custom_map(kvs map[string]string) ! {
436 for k, v in kvs {
437 h.add_custom(k, v)!
438 }
439}
440
441// set sets the key-value pair. This function will clear any other values
442// that exist for the CommonHeader.
443pub fn (mut h Header) set(key CommonHeader, value string) {
444 key_str := key.str()
445
446 // for i, kv in h.data {
447 for i := 0; i < h.cur_pos; i++ {
448 if h.data[i].key == key_str {
449 h.data[i] = HeaderKV{key_str, value}
450 return
451 }
452 }
453 // Not updated, add a new one
454 h.data[h.cur_pos] = HeaderKV{key_str, value}
455 h.cur_pos++
456
457 // h.data[k] = [value]
458 // h.add_key(k)
459}
460
461// set_custom sets the key-value pair for a custom header key. This
462// function will clear any other values that exist for the header. This
463// function will return an error if the key contains invalid header
464// characters.
465pub fn (mut h Header) set_custom(key string, value string) ! {
466 is_valid(key)!
467 mut set := false
468 for i, kv in h.data {
469 if kv.key == key {
470 if !set {
471 h.data[i] = HeaderKV{key, value}
472 set = true
473 } else {
474 // Remove old duplicates
475 h.data[i] = HeaderKV{key, ''}
476 }
477 // return
478 }
479 }
480 if set {
481 return
482 }
483 // Not updated, add a new one
484 h.data[h.cur_pos] = HeaderKV{key, value}
485 h.cur_pos++
486 // h.data[key] = [value]
487 // h.add_key(key)
488}
489
490// delete deletes all values for a key.
491pub fn (mut h Header) delete(key CommonHeader) {
492 h.delete_custom(key.str())
493}
494
495// delete_custom deletes all values for a custom header key.
496pub fn (mut h Header) delete_custom(key string) {
497 for i := 0; i < h.cur_pos; i++ {
498 if h.data[i].key == key {
499 h.data[i] = HeaderKV{key, ''}
500 }
501 }
502 // h.data.delete(key)
503
504 // remove key from keys metadata
505 /*
506 kl := key.to_lower()
507 if kl in h.keys {
508 h.keys[kl] = h.keys[kl].filter(it != key)
509 }
510 */
511}
512
513// contains returns whether the header key exists in the map.
514pub fn (h Header) contains(key CommonHeader) bool {
515 if h.cur_pos == 0 {
516 return false
517 }
518 key_str := key.str()
519 for i := 0; i < h.cur_pos; i++ {
520 if header_key_eq(h.data[i].key, key_str) {
521 return true
522 }
523 }
524 return false
525 // return h.contains_custom(key.str())
526}
527
528@[params]
529pub struct HeaderQueryConfig {
530pub:
531 exact bool
532}
533
534// contains_custom returns whether the custom header key exists in the map.
535pub fn (h Header) contains_custom(key string, flags HeaderQueryConfig) bool {
536 if flags.exact {
537 for i := 0; i < h.cur_pos; i++ {
538 kv := h.data[i]
539 if kv.key == key {
540 return true
541 }
542 }
543 return false
544 } else {
545 for i := 0; i < h.cur_pos; i++ {
546 kv := h.data[i]
547 if header_key_eq(kv.key, key) {
548 return true
549 }
550 }
551 return false
552 }
553}
554
555// get gets the first value for the CommonHeader, or none if the key
556// does not exist.
557pub fn (h Header) get(key CommonHeader) !string {
558 return h.get_custom(key.str())
559}
560
561// get_custom gets the first value for the custom header, or none if
562// the key does not exist.
563pub fn (h Header) get_custom(key string, flags HeaderQueryConfig) !string {
564 if flags.exact {
565 for i := 0; i < h.cur_pos; i++ {
566 // for kv in h.data {
567 kv := h.data[i]
568 // println('${kv.key} => ${kv.value}')
569 if kv.key == key {
570 return kv.value
571 }
572 }
573 } else {
574 // for kv in h.data {
575 for i := 0; i < h.cur_pos; i++ {
576 kv := h.data[i]
577 if header_key_eq(kv.key, key) {
578 return kv.value
579 }
580 }
581 }
582 return error('none')
583}
584
585// starting_with gets the first header starting with key, or none if
586// the key does not exist.
587pub fn (h Header) starting_with(key string) !string {
588 for _, kv in h.data {
589 if kv.key.starts_with(key) {
590 return kv.key
591 }
592 }
593 return error('none')
594}
595
596// values gets all values for the CommonHeader.
597pub fn (h Header) values(key CommonHeader) []string {
598 return h.custom_values(key.str())
599}
600
601// custom_values gets all values for the custom header.
602pub fn (h Header) custom_values(key string, flags HeaderQueryConfig) []string {
603 if h.cur_pos == 0 {
604 return []
605 }
606 mut res := []string{cap: 2}
607 if flags.exact {
608 for i := 0; i < h.cur_pos; i++ {
609 kv := h.data[i]
610 if kv.key == key && kv.value != '' { // empty value means a deleted header
611 res << kv.value
612 }
613 }
614 return res
615 } else {
616 for i := 0; i < h.cur_pos; i++ {
617 kv := h.data[i]
618 if header_key_eq(kv.key, key) && kv.value != '' { // empty value means a deleted header
619 res << kv.value
620 }
621 }
622 return res
623 }
624}
625
626// keys gets all header keys as strings
627pub fn (h Header) keys() []string {
628 mut res := []string{cap: h.cur_pos}
629 for i := 0; i < h.cur_pos; i++ {
630 if h.data[i].value == '' {
631 continue
632 }
633 res << h.data[i].key
634 }
635 // Make sure keys are lower case and unique
636 return arrays.uniq(res)
637}
638
639@[params]
640pub struct HeaderRenderConfig {
641pub:
642 version Version
643 coerce bool
644 canonicalize bool
645}
646
647// render renders the Header into a string for use in sending HTTP
648// requests. All header lines will end in `\r\n`
649@[manualfree]
650pub fn (h Header) render(flags HeaderRenderConfig) string {
651 // estimate ~48 bytes per header
652 mut sb := strings.new_builder(h.data.len * 48)
653 h.render_into_sb(mut sb, flags)
654 res := sb.str()
655 unsafe { sb.free() }
656 return res
657}
658
659// render_into_sb works like render, but uses a preallocated string builder instead.
660// This method should be used only for performance critical applications.
661pub fn (h Header) render_into_sb(mut sb strings.Builder, flags HeaderRenderConfig) {
662 /*
663 if flags.coerce {
664 for kl, data_keys in h.keys {
665 key := if flags.version == .v2_0 {
666 kl
667 } else if flags.canonicalize {
668 canonicalize(kl)
669 } else {
670 data_keys[0]
671 }
672 for k in data_keys {
673 for v in h.data[k] {
674 sb.write_string(key)
675 sb.write_string(': ')
676 sb.write_string(v)
677 sb.write_string('\r\n')
678 }
679 }
680 }
681 } else {
682 */
683 // for _, kv in h.data {
684 for i := 0; i < h.cur_pos; i++ {
685 kv := h.data[i]
686 key := if flags.version == .v2_0 {
687 kv.key.to_lower()
688 } else if flags.canonicalize {
689 canonicalize(kv.key.to_lower())
690 } else {
691 kv.key
692 }
693 // XTODO handle []string ? or doesn't matter?
694 // for v in vs {
695 sb.write_string(key)
696 sb.write_string(': ')
697 sb.write_string(kv.value)
698 sb.write_string('\r\n')
699 //}
700 }
701 //}
702}
703
704// join combines two Header structs into a new Header struct
705pub fn (h Header) join(other Header) Header {
706 mut combined := Header{
707 data: h.data // h.data.clone()
708 cur_pos: h.cur_pos
709 }
710 for k in other.keys() {
711 for v in other.custom_values(k, exact: true) {
712 combined.add_custom(k, v) or {
713 // panic because this should never fail
714 panic('unexpected error: ' + err.str())
715 }
716 }
717 }
718 return combined
719}
720
721// canonicalize canonicalizes an HTTP header key
722// Common headers are determined by the common_header_map
723// Custom headers are capitalized on the first letter and any letter after a '-'
724// NOTE: Assumes sl is lowercase, since the caller usually already has the lowercase key
725fn canonicalize(name string) string {
726 // check if we have a common header
727 if name in common_header_map {
728 return common_header_map[name].str()
729 }
730 return name.split('-').map(it.capitalize()).join('-')
731}
732
733// Helper function to add a key to the keys map
734/*
735fn (mut h Header) add_key(key string) {
736 kl := key.to_lower()
737 if !h.keys[kl].contains(key) {
738 h.keys[kl] << key
739 }
740}
741*/
742
743// Custom error struct for invalid header tokens
744struct HeaderKeyError {
745 Error
746 code int
747 header string
748 invalid_char u8
749}
750
751pub fn (err HeaderKeyError) msg() string {
752 return "Invalid header key: '${err.header}'"
753}
754
755pub fn (err HeaderKeyError) code() int {
756 return err.code
757}
758
759// is_valid checks if the header token contains all valid bytes
760fn is_valid(header string) ! {
761 for _, c in header {
762 if int(c) >= 128 || !is_token(c) {
763 return HeaderKeyError{
764 code: 1
765 header: header
766 invalid_char: c
767 }
768 }
769 }
770 if header.len == 0 {
771 return HeaderKeyError{
772 code: 2
773 header: header
774 invalid_char: 0
775 }
776 }
777}
778
779// is_token checks if the byte is valid for a header token
780fn is_token(b u8) bool {
781 return match b {
782 33, 35...39, 42, 43, 45, 46, 48...57, 65...90, 94...122, 124, 126 { true }
783 else { false }
784 }
785}
786
787// str returns the headers string as seen in HTTP/1.1 requests.
788// Key order is not guaranteed.
789pub fn (h Header) str() string {
790 return h.render(version: .v1_1)
791}
792
793// parse_headers parses a newline delimited string into a Header struct
794fn parse_headers(s string) !Header {
795 mut h := new_header()
796 mut last_key := ''
797 mut last_value := ''
798 for line in s.split_into_lines() {
799 if line.len == 0 {
800 break
801 }
802 // handle header fold
803 if line[0] == ` ` || line[0] == `\t` {
804 last_value += ' ${line.trim(' \t')}'
805 continue
806 } else if last_key != '' {
807 h.add_custom(last_key, last_value)!
808 }
809 last_key, last_value = parse_header(line)!
810 }
811 h.add_custom(last_key, last_value)!
812 return h
813}
814
815fn parse_header(s string) !(string, string) {
816 if !s.contains(':') {
817 return error('missing colon in header')
818 }
819 words := s.split_nth(':', 2)
820 // TODO: parse quoted text according to the RFC
821 return words[0], words[1].trim(' \t')
822}
823
824fn parse_header_fast(s string) !int {
825 pos := s.index(':') or { return error('missing colon in header') }
826 return pos
827}
828