diff --git a/benchmark/bench.mojo b/benchmark/bench.mojo index accd1ad5..b0f505b1 100644 --- a/benchmark/bench.mojo +++ b/benchmark/bench.mojo @@ -99,15 +99,19 @@ fn lightbug_benchmark_request_parse(mut b: Bencher): fn lightbug_benchmark_request_encode(mut b: Bencher): @always_inline @parameter - fn request_encode(): + fn request_encode() raises: + var uri = URI.parse("http://127.0.0.1:8080/some-path") var req = HTTPRequest( - URI.parse("http://127.0.0.1:8080/some-path"), - headers=headers_struct, - body=body_bytes, + uri=uri, + headers=headers_struct, + body=body_bytes, ) _ = encode(req^) - - b.iter[request_encode]() + + try: + b.iter[request_encode]() + except e: + print("failed to encode request, error: ", e) @parameter diff --git a/lightbug_http/client.mojo b/lightbug_http/client.mojo index 2f655199..ac03a023 100644 --- a/lightbug_http/client.mojo +++ b/lightbug_http/client.mojo @@ -160,7 +160,10 @@ struct Client: raise Error("Client._handle_redirect: `Location` header was not received in the response.") if new_location and new_location.startswith("http"): - new_uri = URI.parse(new_location) + try: + new_uri = URI.parse(new_location) + except e: + raise Error("Client._handle_redirect: Failed to parse the new URI: " + str(e)) original_req.headers[HeaderKey.HOST] = new_uri.host else: new_uri = original_req.uri diff --git a/lightbug_http/uri.mojo b/lightbug_http/uri.mojo index d56295ea..5f7aeb1f 100644 --- a/lightbug_http/uri.mojo +++ b/lightbug_http/uri.mojo @@ -1,3 +1,4 @@ +from collections import Dict from utils import Variant from lightbug_http.io.bytes import Bytes, bytes from lightbug_http.strings import ( @@ -10,6 +11,20 @@ from lightbug_http.strings import ( https, ) +alias QueryMap = Dict[String, String] + + +struct QueryDelimiters: + alias STRING_START = "?" + alias ITEM = "&" + alias ITEM_ASSIGN = "=" + + +struct URIDelimiters: + alias SCHEMA = "://" + alias PATH = strSlash + alias ROOT_PATH = strSlash + @value struct URI(Writable, Stringable, Representable): @@ -17,6 +32,7 @@ struct URI(Writable, Stringable, Representable): var scheme: String var path: String var query_string: String + var queries: QueryMap var _hash: String var host: String @@ -27,11 +43,11 @@ struct URI(Writable, Stringable, Representable): var password: String @staticmethod - fn parse(uri: String) -> URI: + fn parse(uri: String) raises -> URI: var proto_str = String(strHttp11) var is_https = False - var proto_end = uri.find("://") + var proto_end = uri.find(URIDelimiters.SCHEMA) var remainder_uri: String if proto_end >= 0: proto_str = uri[:proto_end] @@ -41,7 +57,7 @@ struct URI(Writable, Stringable, Representable): else: remainder_uri = uri - var path_start = remainder_uri.find("/") + var path_start = remainder_uri.find(URIDelimiters.PATH) var host_and_port: String var request_uri: String var host: String @@ -60,7 +76,7 @@ struct URI(Writable, Stringable, Representable): else: scheme = http - var n = request_uri.find("?") + var n = request_uri.find(QueryDelimiters.STRING_START) var original_path: String var query_string: String if n >= 0: @@ -70,11 +86,26 @@ struct URI(Writable, Stringable, Representable): original_path = request_uri query_string = "" + var queries = QueryMap() + if query_string: + var query_items = query_string.split(QueryDelimiters.ITEM) + + for item in query_items: + var key_val = item[].split(QueryDelimiters.ITEM_ASSIGN, 1) + + if key_val[0]: + queries[key_val[0]] = "" + if len(key_val) == 2: + # TODO: Query values are going to be URI encoded strings and should be decoded as part of the + # query processing + queries[key_val[0]] = key_val[1] + return URI( _original_path=original_path, scheme=scheme, path=original_path, query_string=query_string, + queries=queries, _hash="", host=host, full_uri=uri, @@ -84,9 +115,9 @@ struct URI(Writable, Stringable, Representable): ) fn __str__(self) -> String: - var result = String.write(self.scheme, "://", self.host, self.path) + var result = String.write(self.scheme, URIDelimiters.SCHEMA, self.host, self.path) if len(self.query_string) > 0: - result.write("?", self.query_string) + result.write(QueryDelimiters.STRING_START, self.query_string) return result^ fn __repr__(self) -> String: diff --git a/tests/lightbug_http/test_uri.mojo b/tests/lightbug_http/test_uri.mojo index 7f332841..39b1cc93 100644 --- a/tests/lightbug_http/test_uri.mojo +++ b/tests/lightbug_http/test_uri.mojo @@ -88,6 +88,58 @@ def test_uri_parse_http_with_query_string(): testing.assert_equal(uri._original_path, "/job") testing.assert_equal(uri.request_uri, "/job?title=engineer") testing.assert_equal(uri.query_string, "title=engineer") + testing.assert_equal(uri.queries["title"], "engineer") + +def test_uri_parse_multiple_query_parameters(): + var uri = URI.parse("http://example.com/search?q=python&page=1&limit=20") + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/search") + testing.assert_equal(uri.query_string, "q=python&page=1&limit=20") + testing.assert_equal(uri.queries["q"], "python") + testing.assert_equal(uri.queries["page"], "1") + testing.assert_equal(uri.queries["limit"], "20") + testing.assert_equal(uri.request_uri, "/search?q=python&page=1&limit=20") + +def test_uri_parse_query_with_special_characters(): + var uri = URI.parse("https://example.com/path?name=John+Doe&email=john%40example.com") + testing.assert_equal(uri.scheme, "https") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/path") + testing.assert_equal(uri.query_string, "name=John+Doe&email=john%40example.com") + # testing.assert_equal(uri.queries["name"], "John Doe") - fails, contains John+Doe + # testing.assert_equal(uri.queries["email"], "john@example.com") - fails, contains john%40example.com + +def test_uri_parse_empty_query_values(): + var uri = URI.parse("http://example.com/api?key=&token=&empty") + testing.assert_equal(uri.query_string, "key=&token=&empty") + testing.assert_equal(uri.queries["key"], "") + testing.assert_equal(uri.queries["token"], "") + testing.assert_equal(uri.queries["empty"], "") + +def test_uri_parse_complex_query(): + var uri = URI.parse("https://example.com/search?q=test&filter[category]=books&filter[price]=10-20&sort=desc&page=1") + testing.assert_equal(uri.scheme, "https") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/search") + testing.assert_equal(uri.query_string, "q=test&filter[category]=books&filter[price]=10-20&sort=desc&page=1") + testing.assert_equal(uri.queries["q"], "test") + testing.assert_equal(uri.queries["filter[category]"], "books") + testing.assert_equal(uri.queries["filter[price]"], "10-20") + testing.assert_equal(uri.queries["sort"], "desc") + testing.assert_equal(uri.queries["page"], "1") + +def test_uri_parse_query_with_unicode(): + var uri = URI.parse("http://example.com/search?q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA") + testing.assert_equal(uri.query_string, "q=%E2%82%AC&lang=%F0%9F%87%A9%F0%9F%87%AA") + # testing.assert_equal(uri.queries["q"], "€") - fails, contains %E2%82%AC + # testing.assert_equal(uri.queries["lang"], "🇩🇪") - fails, contains %F0%9F%87%A9%F0%9F%87%AA + +# def test_uri_parse_query_with_fragments(): +# var uri = URI.parse("http://example.com/page?id=123#section1") +# testing.assert_equal(uri.query_string, "id=123") +# testing.assert_equal(uri.queries["id"], "123") +# testing.assert_equal(...) - how do we treat fragments? def test_uri_parse_http_with_hash():