// vtest build: !windows import net.http import net.urllib import net import io import time struct StringReader { text string mut: place int } fn (mut s StringReader) read(mut buf []u8) !int { if s.place >= s.text.len { return io.Eof{} } max_bytes := 100 end := if s.place + max_bytes >= s.text.len { s.text.len } else { s.place + max_bytes } n := copy(mut buf, s.text[s.place..end].bytes()) s.place += n return n } fn reader(s string) &io.BufferedReader { return io.new_buffered_reader( reader: &StringReader{ text: s } ) } fn test_parse_request_not_http() { mut reader__ := reader('hello') http.parse_request(mut reader__) or { return } panic('should not have parsed') } fn test_parse_request_no_headers() { mut reader_ := reader('GET / HTTP/1.1\r\n\r\n') req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } assert req.method == .get assert req.url == '/' assert req.version == .v1_1 } fn test_parse_request_two_headers() { mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: B\r\n\r\n') req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } assert req.header.custom_values('Test1') == ['a'] assert req.header.custom_values('Test2') == ['B'] } fn test_parse_request_two_header_values() { mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a; b\r\nTest2: c\r\nTest2: d\r\n\r\n') req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } assert req.header.custom_values('Test1') == ['a; b'] assert req.header.custom_values('Test2') == ['c', 'd'] } fn test_parse_request_body() { mut reader_ := reader('GET / HTTP/1.1\r\nTest1: a\r\nTest2: b\r\nContent-Length: 4\r\n\r\nbodyabc') req := http.parse_request(mut reader_) or { panic('did not parse: ${err}') } assert req.data == 'body' } fn test_parse_request_line() { method, target, version := http.parse_request_line('GET /target HTTP/1.1') or { panic('did not parse: ${err}') } assert method == .get assert target.str() == '/target' assert version == .v1_1 } fn test_parse_request_uri_with_consecutive_slashes() { url := urllib.parse_request_uri('//another.html') or { panic('did not parse: ${err}') } assert url.host == '' assert url.path == '//another.html' assert url.str() == '//another.html' absolute := urllib.parse_request_uri('http://localhost:8080//another.html') or { panic('did not parse: ${err}') } assert absolute.host == 'localhost:8080' assert absolute.path == '//another.html' assert absolute.str() == 'http://localhost:8080//another.html' } fn test_parse_request_line_with_consecutive_slashes() { method, target, version := http.parse_request_line('GET //another.html HTTP/1.1') or { panic('did not parse: ${err}') } assert method == .get assert target.host == '' assert target.path == '//another.html' assert target.str() == '//another.html' assert version == .v1_1 } fn test_parse_form() { assert http.parse_form('foo=bar&bar=baz') == { 'foo': 'bar' 'bar': 'baz' } assert http.parse_form('foo=bar=&bar=baz') == { 'foo': 'bar=' 'bar': 'baz' } assert http.parse_form('foo=bar%3D&bar=baz') == { 'foo': 'bar=' 'bar': 'baz' } assert http.parse_form('foo=b%26ar&bar=baz') == { 'foo': 'b&ar' 'bar': 'baz' } assert http.parse_form('a=b& c=d') == { 'a': 'b' ' c': 'd' } assert http.parse_form('a=b&c= d ') == { 'a': 'b' 'c': ' d ' } assert http.parse_form('{json}') == { 'json': '{json}' } assert http.parse_form('{ "_id": "76c", "friends": [ { "id": 0, "name": "Mason Luna" } ], "greeting": "Hello." }') == { 'json': '{ "_id": "76c", "friends": [ { "id": 0, "name": "Mason Luna" } ], "greeting": "Hello." }' } } fn test_parse_multipart_form() { boundary := '6844a625b1f0b299' names := ['foo', 'fooz'] file := 'bar.v' ct := 'application/octet-stream' contents := ['baz', 'buzz'] data := "--${boundary} Content-Disposition: form-data; name=\"${names[0]}\"; filename=\"${file}\"\r Content-Type: ${ct}\r \r ${contents[0]}\r --${boundary}\r Content-Disposition: form-data; name=\"${names[1]}\"\r \r ${contents[1]}\r --${boundary}--\r " form, files := http.parse_multipart_form(data, boundary) assert files == { names[0]: [ http.FileData{ filename: file content_type: ct data: contents[0] }, ] } assert form == { names[1]: contents[1] } } fn test_parse_multipart_form2() { boundary := '---------------------------27472781931927549291906391339' data := '--${boundary}\r Content-Disposition: form-data; name="username"\r \r admin\r --${boundary}\r Content-Disposition: form-data; name="password"\r \r admin123\r --${boundary}--\r ' form, files := http.parse_multipart_form(data, boundary) for k, v in form { eprintln('> k: ${k} | v: ${v}') eprintln('>> k.bytes(): ${k.bytes()}') eprintln('>> v.bytes(): ${v.bytes()}') } assert form['username'] == 'admin' assert form['password'] == 'admin123' } fn test_multipart_form_body() { files := { 'foo': [ http.FileData{ filename: 'bar.v' content_type: 'application/octet-stream' data: 'baz' }, ] } form := { 'fooz': 'buzz' } body, boundary := http.multipart_form_body(form, files) parsed_form, parsed_files := http.parse_multipart_form(body, boundary) assert parsed_files == files assert parsed_form == form } fn test_parse_large_body() { body := 'A'.repeat(10_001) // greater than max_bytes req := 'GET / HTTP/1.1\r\nContent-Length: ${body.len}\r\n\r\n${body}' mut reader_ := reader(req) result := http.parse_request(mut reader_)! assert result.data.len == body.len assert result.data == body } fn test_parse_multipart_form_empty_body() { body := '' boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b' form, files := http.parse_multipart_form(body, boundary) assert form.len == 0 assert files.len == 0 } fn test_parse_multipart_form_issue_26204__do_not_panic_for_small_or_partial_forms() { boundary := '----01KDN6J6BKWY9WMYWRW4MG5J59' body := '${boundary}\r\nContent-Disposition: form-data; name="fooz"${boundary}--\r\n' form, files := http.parse_multipart_form(body, boundary) assert form.len == 0 assert files.len == 0 } fn test_parse_multipart_form_issue_24974_raw() { body := r'------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n' boundary := r'----WebKitFormBoundaryQcBIkwnOACVsvR8b' form, files := http.parse_multipart_form(body, boundary) assert form.len == 0 assert files.len == 0 } fn test_parse_multipart_form_issue_24974_cooked() { body := '------WebKiormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="michael-sum-LEpfefQf4rU-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b\r\nContent-Disposition: form-data; name="files"; filename="mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg"\r\nContent-Type: image/jpeg\r\n\r\n\r\n------WebKitFormBoundaryQcBIkwnOACVsvR8b--\r\n' boundary := '----WebKitFormBoundaryQcBIkwnOACVsvR8b' form, files := http.parse_multipart_form(body, boundary) assert form.len == 0 assert files.len == 1 assert files['files'][0].filename == 'mikhail-vasilyev-IFxjDdqK_0U-unsplash.jpg' assert files['files'][0].content_type == 'image/jpeg' } fn test_parse_request_head_str_basic() { s := 'GET / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 0\r\n\r\n' req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } assert req.method == .get assert req.url == '/' assert req.version == .v1_1 assert req.host == 'example.com' } fn test_parse_request_head_str_post_with_headers() { s := 'POST /api HTTP/1.1\r\nHost: test.com\r\nContent-Type: application/json\r\nContent-Length: 10\r\n\r\n' req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } assert req.method == .post assert req.url == '/api' assert req.version == .v1_1 assert req.host == 'test.com' assert req.header.custom_values('Content-Type') == ['application/json'] } fn test_parse_request_head_str_post_with_headers_and_body() { s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}' req := http.parse_request_head_str(s) or { assert false, 'did not parse: ${err}' return } assert req.method == .post assert req.url == '/index' assert req.version == .v1_1 assert req.host == 'localhost:9008' assert req.header.custom_values('User-Agent') == ['curl/7.68.0'] assert req.header.custom_values('Accept') == ['*/*'] assert req.header.custom_values('Content-Type') == ['application/json'] assert req.header.custom_values('Connection') == ['keep-alive'] assert req.data == '' } fn test_parse_request_head_post_with_headers_and_body() { s := 'POST /index HTTP/1.1\r\nHost: localhost:9008\r\nUser-Agent: curl/7.68.0\r\nAccept: */*\r\nContent-Type: application/json\r\nContent-Length: 24\r\nConnection: keep-alive\r\n\r\n{"username": "test"}' req := http.parse_request_str(s) or { assert false, 'did not parse: ${err}' return } assert req.data == '{"username": "test"}' } fn test_parse_request_head_str_with_spaces_in_header_values() { s := 'GET /path HTTP/1.1\r\nX-Custom-Header: value with spaces\r\n\r\n' req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } assert req.method == .get assert req.url == '/path' assert req.header.custom_values('X-Custom-Header') == ['value with spaces'] } fn test_parse_request_head_str_multiple_same_header() { s := 'GET / HTTP/1.1\r\nHost: example.com\r\nSet-Cookie: session=abc\r\nSet-Cookie: user=xyz\r\n\r\n' req := http.parse_request_head_str(s) or { panic('did not parse: ${err}') } assert req.method == .get assert req.host == 'example.com' assert req.header.custom_values('Set-Cookie') == ['session=abc', 'user=xyz'] } fn test_get_does_not_wait_for_timeout_when_content_length_is_complete() { mut listener := net.listen_tcp(.ip, '127.0.0.1:0')! port := listener.addr()!.port()! t := spawn fn (mut listener net.TcpListener) { mut conn := listener.accept() or { listener.close() or {} return } defer { conn.close() or {} listener.close() or {} } mut request_buf := []u8{len: 2048} _ = conn.read(mut request_buf) or { return } response := 'HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: keep-alive\r\n\r\nok' conn.write(response.bytes()) or { return } conn.set_read_timeout(5 * time.second) mut drain_buf := []u8{len: 128} for { n := conn.read(mut drain_buf) or { break } if n <= 0 { break } } }(mut listener) mut req := http.new_request(.get, 'http://127.0.0.1:${port}', '') req.read_timeout = 2 * time.second start := time.now() res := req.do()! elapsed := time.since(start) t.wait() assert res.status() == .ok assert res.body == 'ok' assert elapsed < time.second } fn test_prepare_uses_fetch_config_timeouts() { req := http.prepare( url: 'http://example.com' read_timeout: 123 * time.millisecond write_timeout: 456 * time.millisecond )! assert req.read_timeout == 123 * time.millisecond assert req.write_timeout == 456 * time.millisecond } fn test_get_does_not_wait_for_timeout_when_chunked_body_is_complete() { mut listener := net.listen_tcp(.ip, '127.0.0.1:0')! port := listener.addr()!.port()! t := spawn fn (mut listener net.TcpListener) { mut conn := listener.accept() or { listener.close() or {} return } defer { conn.close() or {} listener.close() or {} } mut request_buf := []u8{len: 2048} _ = conn.read(mut request_buf) or { return } response := 'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: keep-alive\r\n\r\n2\r\nok\r\n0\r\n\r\n' conn.write(response.bytes()) or { return } conn.set_read_timeout(5 * time.second) mut drain_buf := []u8{len: 128} for { n := conn.read(mut drain_buf) or { break } if n <= 0 { break } } }(mut listener) mut req := http.new_request(.get, 'http://127.0.0.1:${port}', '') req.read_timeout = 2 * time.second start := time.now() res := req.do()! elapsed := time.since(start) t.wait() assert res.status() == .ok assert res.body == 'ok' assert elapsed < time.second }