diff --git a/vlib/net/http/http.v b/vlib/net/http/http.v index 236a5b46be..512d79e783 100644 --- a/vlib/net/http/http.v +++ b/vlib/net/http/http.v @@ -19,8 +19,7 @@ pub struct Request { pub mut: version Version = .v1_1 method Method - headers map[string]string // original requset headers - lheaders map[string]string // same as headers, but with normalized lowercased keys (for received requests) + header Header cookies map[string]string data string url string @@ -34,9 +33,9 @@ pub mut: pub struct FetchConfig { pub mut: method Method + header Header data string params map[string]string - headers map[string]string cookies map[string]string user_agent string = 'v.http' verbose bool @@ -46,8 +45,7 @@ pub mut: pub struct Response { pub: text string - headers map[string]string // original response headers, 'Set-Cookie' or 'set-Cookie', etc. - lheaders map[string]string // same as headers, but with normalized lowercased keys, like 'set-cookie' + header Header cookies map[string]string status_code int } @@ -76,9 +74,7 @@ pub fn get(url string) ?Response { pub fn post(url string, data string) ?Response { return fetch_with_method(.post, url, data: data - headers: map{ - 'Content-Type': http.content_type_default - } + header: new_header({key: .content_type, value: http.content_type_default}) ) } @@ -86,18 +82,14 @@ pub fn post(url string, data string) ?Response { pub fn post_json(url string, data string) ?Response { return fetch_with_method(.post, url, data: data - headers: map{ - 'Content-Type': 'application/json' - } + header: new_header({key: .content_type, value: 'application/json'}) ) } // post_form sends a POST HTTP request to the URL with X-WWW-FORM-URLENCODED data pub fn post_form(url string, data map[string]string) ?Response { - return fetch_with_method(.post, url, - headers: map{ - 'Content-Type': 'application/x-www-form-urlencoded' - } + return fetch_with_method(.post, url, + header: new_header({key: .content_type, value: 'application/x-www-form-urlencoded'}) data: url_encode_form_data(data) ) } @@ -106,9 +98,7 @@ pub fn post_form(url string, data map[string]string) ?Response { pub fn put(url string, data string) ?Response { return fetch_with_method(.put, url, data: data - headers: map{ - 'Content-Type': http.content_type_default - } + header: new_header({key: .content_type, value: http.content_type_default}) ) } @@ -116,9 +106,7 @@ pub fn put(url string, data string) ?Response { pub fn patch(url string, data string) ?Response { return fetch_with_method(.patch, url, data: data - headers: map{ - 'Content-Type': http.content_type_default - } + header: new_header({key: .content_type, value: http.content_type_default}) ) } @@ -143,7 +131,7 @@ pub fn fetch(_url string, config FetchConfig) ?Response { method: config.method url: url data: data - headers: config.headers + header: config.header cookies: config.cookies user_agent: config.user_agent ws_func: 0 @@ -195,32 +183,23 @@ fn build_url_from_fetch(_url string, config FetchConfig) ?string { } fn (mut req Request) free() { - unsafe { req.headers.free() } + unsafe { req.header.free() } } fn (mut resp Response) free() { - unsafe { resp.headers.free() } + unsafe { resp.header.data.free() } } // add_header adds the key and value of an HTTP request header -pub fn (mut req Request) add_header(key string, val string) { - req.headers[key] = val +// To add a custom header, use add_custom_header +pub fn (mut req Request) add_header(key CommonHeader, val string) { + req.header.add(key, val) } -// parse_headers parses HTTP header strings to mapped data -pub fn parse_headers(lines []string) map[string]string { - mut headers := map[string]string{} - for i, line in lines { - if i == 0 { - continue - } - words := line.split(': ') - if words.len != 2 { - continue - } - headers[words[0]] = words[1] - } - return headers +// add_custom_header adds the key and value of an HTTP request header +// This method may fail if the key contains characters that are not permitted +pub fn (mut req Request) add_custom_header(key string, val string) ? { + return req.header.add_custom(key, val) } // do will send the HTTP request and returns `http.Response` as soon as the response is recevied @@ -239,7 +218,7 @@ pub fn (req &Request) do() ?Response { break } // follow any redirects - mut redirect_url := resp.lheaders['location'] + mut redirect_url := resp.header.get(.location) or { '' } if redirect_url.len > 0 && redirect_url[0] == `/` { url.set_path(redirect_url) or { return error('http.request.do: invalid path in redirect: "$redirect_url"') @@ -283,9 +262,7 @@ fn (req &Request) method_and_url_to_response(method Method, url urllib.URL) ?Res } fn parse_response(resp string) Response { - // TODO: Header data type - mut headers := map[string]string{} - mut lheaders := map[string]string{} + mut header := new_header() // TODO: Cookie data type mut cookies := map[string]string{} first_header := resp.all_before('\n') @@ -295,7 +272,7 @@ fn parse_response(resp string) Response { status_code = val.int() } mut text := '' - // Build resp headers map and separate the body + // Build resp header map and separate the body mut nl_pos := 3 mut i := 1 for { @@ -312,27 +289,21 @@ fn parse_response(resp string) Response { } i++ pos := h.index(':') or { continue } - // if h.contains('Content-Type') { - // continue - // } mut key := h[..pos] - lkey := key.to_lower() - val := h[pos + 2..] - if lkey == 'set-cookie' { - parts := val.trim_space().split('=') - cookies[parts[0]] = parts[1] - } - tval := val.trim_space() - headers[key] = tval - lheaders[lkey] = tval + val := h[pos + 2..].trim_space() + header.add_custom(key, val) or { eprintln('error parsing header: $err') } } - if lheaders['transfer-encoding'] == 'chunked' || lheaders['content-length'] == '' { + // set cookies + for cookie in header.values(.set_cookie) { + parts := cookie.split_nth('=', 2) + cookies[parts[0]] = parts[1] + } + if header.get(.transfer_encoding) or { '' } == 'chunked' || header.get(.content_length) or { '' } == '' { text = chunked.decode(text) } return Response{ status_code: status_code - headers: headers - lheaders: lheaders + header: header cookies: cookies text: text } @@ -341,19 +312,20 @@ fn parse_response(resp string) Response { fn (req &Request) build_request_headers(method Method, host_name string, path string) string { ua := req.user_agent mut uheaders := []string{} - if 'Host' !in req.headers { + if !req.header.contains(.host) { uheaders << 'Host: $host_name\r\n' } - if 'User-Agent' !in req.headers { + if !req.header.contains(.user_agent) { uheaders << 'User-Agent: $ua\r\n' } - if req.data.len > 0 && 'Content-Length' !in req.headers { + if req.data.len > 0 && !req.header.contains(.content_length) { uheaders << 'Content-Length: $req.data.len\r\n' } - for key, val in req.headers { - if key == 'Cookie' { + for key in req.header.keys() { + if key == CommonHeader.cookie.str() { continue } + val := req.header.custom_values(key).join('; ') uheaders << '$key: $val\r\n' } uheaders << req.build_request_cookies_header() @@ -369,9 +341,7 @@ fn (req &Request) build_request_cookies_header() string { for key, val in req.cookies { cookie << '$key=$val' } - if 'Cookie' in req.headers && req.headers['Cookie'] != '' { - cookie << req.headers['Cookie'] - } + cookie << req.header.values(.cookie) return 'Cookie: ' + cookie.join('; ') + '\r\n' } @@ -415,5 +385,5 @@ fn (req &Request) http_do(host string, method Method, path string) ?Response { // referer returns 'Referer' header value of the given request pub fn (req &Request) referer() string { - return req.headers['Referer'] + return req.header.get(.referer) or { '' } } diff --git a/vlib/net/http/http_httpbin_test.v b/vlib/net/http/http_httpbin_test.v index cbdfcd009d..37a25eba19 100644 --- a/vlib/net/http/http_httpbin_test.v +++ b/vlib/net/http/http_httpbin_test.v @@ -77,12 +77,12 @@ fn test_http_fetch_with_params() { } } -fn test_http_fetch_with_headers() { +fn test_http_fetch_with_headers() ? { $if !network ? { return } + mut header := new_header() + header.add_custom('Test-Header', 'hello world') ? responses := http_fetch_mock([], { - headers: { - 'Test-Header': 'hello world' - } + header: header }) or { panic(err) } diff --git a/vlib/vweb/README.md b/vlib/vweb/README.md index b93a3e1133..d2bd7d165e 100644 --- a/vlib/vweb/README.md +++ b/vlib/vweb/README.md @@ -65,7 +65,7 @@ Everything, including HTML templates, is in one binary file. That's all you need ## Getting Started To start with vweb, you have to import the module `vweb`. -After the import, define a struct to hold vweb.Context +After the import, define a struct to hold vweb.Context (and any other variables your program will need). The web server can be started by calling `vweb.run(port)`. @@ -111,7 +111,7 @@ fn (mut app App) world() vweb.Result { } ``` -To pass a parameter to an endpoint, you simply define it inside +To pass a parameter to an endpoint, you simply define it inside an attribute, e. g. `['/hello/:user]`. After it is defined in the attribute, you have to add it as a function parameter. @@ -123,8 +123,9 @@ fn (mut app App) hello_user(user string) vweb.Result { } ``` -You have access to the raw request data such as headers +You have access to the raw request data such as headers or the request body by accessing `app` (which is `vweb.Context`). If you want to read the request body, you can do that by calling `app.req.data`. -To read the request headers, you just call `app.req.headers` and access the header you want, -e.g. `app.req.headers['Content-Type']` +To read the request headers, you just call `app.req.header` and access the header you want, +e.g. `app.req.header.get(.content_type)`. See `struct Header` for all +available methods (`v doc net.http Header`). diff --git a/vlib/vweb/request.v b/vlib/vweb/request.v index c5ac970a12..a42d5ccc61 100644 --- a/vlib/vweb/request.v +++ b/vlib/vweb/request.v @@ -11,28 +11,18 @@ fn parse_request(mut reader io.BufferedReader) ?http.Request { method, target, version := parse_request_line(line) ? // headers - mut h := http.new_header() + mut header := http.new_header() line = reader.read_line() ? for line != '' { key, value := parse_header(line) ? - h.add_custom(key, value) ? + header.add_custom(key, value) ? line = reader.read_line() ? } - h.coerce(canonicalize: true) - - // create map[string]string from headers - // TODO: replace headers and lheaders with http.Header type - mut headers := map[string]string{} - mut lheaders := map[string]string{} - for key in h.keys() { - values := h.custom_values(key).join('; ') - headers[key] = values - lheaders[key.to_lower()] = values - } + header.coerce(canonicalize: true) // body mut body := []byte{} - if length := h.get(.content_length) { + if length := header.get(.content_length) { n := length.int() if n > 0 { body = []byte{len: n} @@ -42,13 +32,11 @@ fn parse_request(mut reader io.BufferedReader) ?http.Request { } } } - h.free() return http.Request{ method: method url: target.str() - headers: headers - lheaders: lheaders + header: header data: body.bytestr() version: version } diff --git a/vlib/vweb/request_test.v b/vlib/vweb/request_test.v index b1275ad4c8..10301e9cfe 100644 --- a/vlib/vweb/request_test.v +++ b/vlib/vweb/request_test.v @@ -39,28 +39,16 @@ fn test_parse_request_two_headers() { req := parse_request(mut reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: B\r\n\r\n')) or { panic('did not parse: $err') } - assert req.headers == map{ - 'Test1': 'a' - 'Test2': 'B' - } - assert req.lheaders == map{ - 'test1': 'a' - 'test2': 'B' - } + assert req.header.custom_values('Test1') == ['a'] + assert req.header.custom_values('Test2') == ['B'] } fn test_parse_request_two_header_values() { req := parse_request(mut reader('GET / HTTP/1.1\r\nTest1: a; b\r\nTest2: c\r\nTest2: d\r\n\r\n')) or { panic('did not parse: $err') } - assert req.headers == map{ - 'Test1': 'a; b' - 'Test2': 'c; d' - } - assert req.lheaders == map{ - 'test1': 'a; b' - 'test2': 'c; d' - } + assert req.header.custom_values('Test1') == ['a; b'] + assert req.header.custom_values('Test2') == ['c', 'd'] } fn test_parse_request_body() { diff --git a/vlib/vweb/tests/vweb_test.v b/vlib/vweb/tests/vweb_test.v index b2ccde0142..124a906f0b 100644 --- a/vlib/vweb/tests/vweb_test.v +++ b/vlib/vweb/tests/vweb_test.v @@ -108,28 +108,28 @@ fn test_a_simple_tcp_client_html_page() { } // net.http client based tests follow: -fn assert_common_http_headers(x http.Response) { +fn assert_common_http_headers(x http.Response) ? { assert x.status_code == 200 - assert x.headers['Server'] == 'VWeb' - assert x.headers['Content-Length'].int() > 0 - assert x.headers['Connection'] == 'close' + assert x.header.get(.server) ? == 'VWeb' + assert x.header.get(.content_length) ?.int() > 0 + assert x.header.get(.connection) ? == 'close' } -fn test_http_client_index() { +fn test_http_client_index() ? { x := http.get('http://127.0.0.1:$sport/') or { panic(err) } - assert_common_http_headers(x) - assert x.headers['Content-Type'] == 'text/plain' + assert_common_http_headers(x) ? + assert x.header.get(.content_type) ? == 'text/plain' assert x.text == 'Welcome to VWeb' } -fn test_http_client_chunk_transfer() { +fn test_http_client_chunk_transfer() ? { x := http.get('http://127.0.0.1:$sport/chunk') or { panic(err) } - assert_common_http_headers(x) - assert x.headers['Transfer-Encoding'] == 'chunked' + assert_common_http_headers(x) ? + assert x.header.get(.transfer_encoding) ? == 'chunked' assert x.text == 'Lorem ipsum dolor sit amet, consetetur sadipscing' } -fn test_http_client_404() { +fn test_http_client_404() ? { url_404_list := [ 'http://127.0.0.1:$sport/zxcnbnm', 'http://127.0.0.1:$sport/JHKAJA', @@ -141,37 +141,37 @@ fn test_http_client_404() { } } -fn test_http_client_simple() { +fn test_http_client_simple() ? { x := http.get('http://127.0.0.1:$sport/simple') or { panic(err) } - assert_common_http_headers(x) - assert x.headers['Content-Type'] == 'text/plain' + assert_common_http_headers(x) ? + assert x.header.get(.content_type) ? == 'text/plain' assert x.text == 'A simple result' } -fn test_http_client_html_page() { +fn test_http_client_html_page() ? { x := http.get('http://127.0.0.1:$sport/html_page') or { panic(err) } - assert_common_http_headers(x) - assert x.headers['Content-Type'] == 'text/html' + assert_common_http_headers(x) ? + assert x.header.get(.content_type) ? == 'text/html' assert x.text == '

ok

' } -fn test_http_client_settings_page() { +fn test_http_client_settings_page() ? { x := http.get('http://127.0.0.1:$sport/bilbo/settings') or { panic(err) } - assert_common_http_headers(x) + assert_common_http_headers(x) ? assert x.text == 'username: bilbo' // y := http.get('http://127.0.0.1:$sport/kent/settings') or { panic(err) } - assert_common_http_headers(y) + assert_common_http_headers(y) ? assert y.text == 'username: kent' } -fn test_http_client_user_repo_settings_page() { +fn test_http_client_user_repo_settings_page() ? { x := http.get('http://127.0.0.1:$sport/bilbo/gostamp/settings') or { panic(err) } - assert_common_http_headers(x) + assert_common_http_headers(x) ? assert x.text == 'username: bilbo | repository: gostamp' // y := http.get('http://127.0.0.1:$sport/kent/golang/settings') or { panic(err) } - assert_common_http_headers(y) + assert_common_http_headers(y) ? assert y.text == 'username: kent | repository: golang' // z := http.get('http://127.0.0.1:$sport/missing/golang/settings') or { panic(err) } @@ -183,7 +183,7 @@ struct User { age int } -fn test_http_client_json_post() { +fn test_http_client_json_post() ? { ouser := User{ name: 'Bilbo' age: 123 @@ -193,7 +193,7 @@ fn test_http_client_json_post() { $if debug_net_socket_client ? { eprintln('/json_echo endpoint response: $x') } - assert x.headers['Content-Type'] == 'application/json' + assert x.header.get(.content_type) ? == 'application/json' assert x.text == json_for_ouser nuser := json.decode(User, x.text) or { User{} } assert '$ouser' == '$nuser' @@ -202,7 +202,7 @@ fn test_http_client_json_post() { $if debug_net_socket_client ? { eprintln('/json endpoint response: $x') } - assert x.headers['Content-Type'] == 'application/json' + assert x.header.get(.content_type) ? == 'application/json' assert x.text == json_for_ouser nuser2 := json.decode(User, x.text) or { User{} } assert '$ouser' == '$nuser2' diff --git a/vlib/vweb/tests/vweb_test_server.v b/vlib/vweb/tests/vweb_test_server.v index a956d69e15..1276aacb4d 100644 --- a/vlib/vweb/tests/vweb_test_server.v +++ b/vlib/vweb/tests/vweb_test_server.v @@ -81,7 +81,7 @@ pub fn (mut app App) user_repo_settings(username string, repository string) vweb ['/json_echo'; post] pub fn (mut app App) json_echo() vweb.Result { // eprintln('>>>>> received http request at /json_echo is: $app.req') - app.set_content_type(app.req.headers['Content-Type']) + app.set_content_type(app.req.header.get(.content_type) or { '' }) return app.ok(app.req.data) } @@ -89,7 +89,7 @@ pub fn (mut app App) json_echo() vweb.Result { [post] pub fn (mut app App) json() vweb.Result { // eprintln('>>>>> received http request at /json is: $app.req') - app.set_content_type(app.req.headers['Content-Type']) + app.set_content_type(app.req.header.get(.content_type) or { '' }) return app.ok(app.req.data) } diff --git a/vlib/vweb/vweb.v b/vlib/vweb/vweb.v index f1990b1ec0..e81a5f63ff 100644 --- a/vlib/vweb/vweb.v +++ b/vlib/vweb/vweb.v @@ -281,7 +281,7 @@ pub fn (mut ctx Context) add_header(key string, val string) { // Returns the header data from the key pub fn (ctx &Context) get_header(key string) string { - return ctx.req.lheaders[key.to_lower()] + return ctx.req.header.get_custom(key) or { '' } } pub fn run(port int) { @@ -333,8 +333,8 @@ fn handle_conn(mut conn net.TcpConn, mut app T) { page_gen_start: page_gen_start } if req.method in vweb.methods_with_form { - if 'multipart/form-data' in req.lheaders['content-type'].split('; ') { - boundary := req.lheaders['content-type'].split('; ').filter(it.starts_with('boundary=')) + if 'multipart/form-data' in req.header.values(.content_type) { + boundary := req.header.values(.content_type).filter(it.starts_with('boundary=')) if boundary.len != 1 { send_string(mut conn, vweb.http_400) or {} return @@ -575,9 +575,9 @@ pub fn (mut ctx Context) serve_static(url string, file_path string, mime_type st // Returns the ip address from the current user pub fn (ctx &Context) ip() string { - mut ip := ctx.req.lheaders['x-forwarded-for'] + mut ip := ctx.req.header.get(.x_forwarded_for) or { '' } if ip == '' { - ip = ctx.req.lheaders['x-real-ip'] + ip = ctx.req.header.get_custom('X-Real-Ip') or { '' } } if ip.contains(',') {