Skip to content

Commit 273568a

Browse files
authored
Merge pull request #85 from thatstoasty/ptrs
Improve code safety and error handling + performance improvements
2 parents cdaec54 + 4508b03 commit 273568a

22 files changed

+1970
-1124
lines changed

bench.mojo

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
1+
from memory import Span
12
from benchmark import *
23
from lightbug_http.io.bytes import bytes, Bytes
34
from lightbug_http.header import Headers, Header
45
from lightbug_http.utils import ByteReader, ByteWriter
56
from lightbug_http.http import HTTPRequest, HTTPResponse, encode
67
from lightbug_http.uri import URI
78

8-
alias headers = bytes(
9-
"""GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n"""
10-
)
11-
alias body = bytes(String("I am the body of an HTTP request") * 5)
12-
alias Request = bytes(
13-
"""GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n"""
14-
) + body
15-
alias Response = bytes(
16-
"HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type:"
9+
alias headers = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n"
10+
11+
alias body = "I am the body of an HTTP request" * 5
12+
alias body_bytes = bytes(body)
13+
alias Request = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n" + body
14+
alias Response = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type:"
1715
" application/octet-stream\r\nconnection: keep-alive\r\ncontent-length:"
18-
" 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n"
19-
) + body
16+
" 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body
2017

2118

2219
fn main():
@@ -66,7 +63,7 @@ fn lightbug_benchmark_response_encode(mut b: Bencher):
6663
@always_inline
6764
@parameter
6865
fn response_encode():
69-
var res = HTTPResponse(body, headers=headers_struct)
66+
var res = HTTPResponse(body.as_bytes(), headers=headers_struct)
7067
_ = encode(res^)
7168

7269
b.iter[response_encode]()
@@ -79,7 +76,7 @@ fn lightbug_benchmark_response_parse(mut b: Bencher):
7976
fn response_parse():
8077
var res = Response
8178
try:
82-
_ = HTTPResponse.from_bytes(res^)
79+
_ = HTTPResponse.from_bytes(res.as_bytes())
8380
except:
8481
pass
8582

@@ -93,7 +90,7 @@ fn lightbug_benchmark_request_parse(mut b: Bencher):
9390
fn request_parse():
9491
var r = Request
9592
try:
96-
_ = HTTPRequest.from_bytes("127.0.0.1/path", 4096, r^)
93+
_ = HTTPRequest.from_bytes("127.0.0.1/path", 4096, r.as_bytes())
9794
except:
9895
pass
9996

@@ -108,7 +105,7 @@ fn lightbug_benchmark_request_encode(mut b: Bencher):
108105
var req = HTTPRequest(
109106
URI.parse("http://127.0.0.1:8080/some-path")[URI],
110107
headers=headers_struct,
111-
body=body,
108+
body=body_bytes,
112109
)
113110
_ = encode(req^)
114111

@@ -122,7 +119,7 @@ fn lightbug_benchmark_header_encode(mut b: Bencher):
122119
fn header_encode():
123120
var b = ByteWriter()
124121
var h = headers_struct
125-
h.encode_to(b)
122+
b.write(h)
126123

127124
b.iter[header_encode]()
128125

@@ -135,7 +132,7 @@ fn lightbug_benchmark_header_parse(mut b: Bencher):
135132
try:
136133
var b = headers
137134
var header = Headers()
138-
var reader = ByteReader(b^)
135+
var reader = ByteReader(b.as_bytes())
139136
_ = header.parse_raw(reader)
140137
except:
141138
print("failed")

integration_test_client.mojo

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,83 @@
1+
from collections import Dict
12
from lightbug_http import *
23
from lightbug_http.client import Client
4+
from lightbug_http.utils import logger
35
from testing import *
46

57
fn u(s: String) raises -> URI:
68
return URI.parse_raises("http://127.0.0.1:8080/" + s)
79

810
struct IntegrationTest:
911
var client: Client
12+
var results: Dict[String, String]
1013

1114
fn __init__(out self):
1215
self.client = Client()
16+
self.results = Dict[String, String]()
17+
18+
fn mark_successful(mut self, name: String):
19+
self.results[name] = ""
20+
21+
fn mark_failed(mut self, name: String):
22+
self.results[name] = ""
1323

14-
fn test_redirect(mut self) raises:
15-
print("Testing redirect...")
24+
fn test_redirect(mut self):
25+
alias name = "test_redirect"
26+
logger.info("Testing redirect...")
1627
var h = Headers(Header(HeaderKey.CONNECTION, 'keep-alive'))
17-
var res = self.client.do(HTTPRequest(u("redirect"), headers=h))
18-
assert_equal(res.status_code, StatusCode.OK)
19-
assert_equal(to_string(res.body_raw), "yay you made it")
20-
assert_equal(res.headers[HeaderKey.CONNECTION], "keep-alive")
28+
try:
29+
var res = self.client.do(HTTPRequest(u("redirect"), headers=h))
30+
assert_equal(res.status_code, StatusCode.OK)
31+
assert_equal(to_string(res.body_raw), "yay you made it")
32+
var conn = res.headers.get(HeaderKey.CONNECTION)
33+
if conn:
34+
assert_equal(conn.value(), "keep-alive")
35+
self.mark_successful(name)
36+
except e:
37+
logger.error("IntegrationTest.test_redirect has run into an error.")
38+
logger.error(e)
39+
self.mark_failed(name)
40+
return
2141

22-
fn test_close_connection(mut self) raises:
23-
print("Testing close connection...")
42+
fn test_close_connection(mut self):
43+
alias name = "test_close_connection"
44+
logger.info("Testing close connection...")
2445
var h = Headers(Header(HeaderKey.CONNECTION, 'close'))
25-
var res = self.client.do(HTTPRequest(u("close-connection"), headers=h))
26-
assert_equal(res.status_code, StatusCode.OK)
27-
assert_equal(to_string(res.body_raw), "connection closed")
28-
assert_equal(res.headers[HeaderKey.CONNECTION], "close")
46+
try:
47+
var res = self.client.do(HTTPRequest(u("close-connection"), headers=h))
48+
assert_equal(res.status_code, StatusCode.OK)
49+
assert_equal(to_string(res.body_raw), "connection closed")
50+
assert_equal(res.headers[HeaderKey.CONNECTION], "close")
51+
self.mark_successful(name)
52+
except e:
53+
logger.error("IntegrationTest.test_close_connection has run into an error.")
54+
logger.error(e)
55+
self.mark_failed(name)
56+
return
2957

30-
fn test_server_error(mut self) raises:
31-
print("Testing internal server error...")
32-
var res = self.client.do(HTTPRequest(u("error")))
33-
assert_equal(res.status_code, StatusCode.INTERNAL_ERROR)
34-
assert_equal(res.status_text, "Internal Server Error")
58+
fn test_server_error(mut self):
59+
alias name = "test_server_error"
60+
logger.info("Testing internal server error...")
61+
try:
62+
var res = self.client.do(HTTPRequest(u("error")))
63+
assert_equal(res.status_code, StatusCode.INTERNAL_ERROR)
64+
assert_equal(res.status_text, "Internal Server Error")
65+
self.mark_successful(name)
66+
except e:
67+
logger.error("IntegrationTest.test_server_error has run into an error.")
68+
logger.error(e)
69+
self.mark_failed(name)
70+
return
3571

36-
37-
fn run_tests(mut self) raises:
72+
fn run_tests(mut self):
73+
logger.info("Running Client Integration Tests...")
3874
self.test_redirect()
3975
self.test_close_connection()
4076
self.test_server_error()
4177

42-
fn main() raises:
78+
for test in self.results.items():
79+
print(test[].key + ":", test[].value)
80+
81+
fn main():
4382
var test = IntegrationTest()
4483
test.run_tests()

integration_test_server.mojo

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from lightbug_http import *
22

3+
34
@value
4-
struct IntegerationTestService(HTTPService):
5+
struct IntegrationTestService(HTTPService):
56
fn func(mut self, req: HTTPRequest) raises -> HTTPResponse:
67
var p = req.uri.path
78
if p == "/redirect":
@@ -21,8 +22,9 @@ struct IntegerationTestService(HTTPService):
2122

2223
return NotFound("wrong")
2324

25+
2426
fn main() raises:
2527
var server = Server(tcp_keep_alive=True)
26-
var service = IntegerationTestService()
28+
var service = IntegrationTestService()
2729
server.listen_and_serve("127.0.0.1:8080", service)
28-
30+

lightbug_http/client.mojo

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,18 @@ from lightbug_http.http import HTTPRequest, HTTPResponse, encode
1414
from lightbug_http.header import Headers, HeaderKey
1515
from lightbug_http.net import create_connection, SysConnection
1616
from lightbug_http.io.bytes import Bytes
17-
from lightbug_http.utils import ByteReader
17+
from lightbug_http.utils import ByteReader, logger
1818
from collections import Dict
1919

2020

2121
struct Client:
22-
var host: StringLiteral
22+
var host: String
2323
var port: Int
2424
var name: String
2525

2626
var _connections: Dict[String, SysConnection]
2727

28-
fn __init__(out self):
29-
self.host = "127.0.0.1"
30-
self.port = 8888
31-
self.name = "lightbug_http_client"
32-
self._connections = Dict[String, SysConnection]()
33-
34-
fn __init__(out self, host: StringLiteral, port: Int):
28+
fn __init__(out self, host: String = "127.0.0.1", port: Int = 8888):
3529
self.host = host
3630
self.port = port
3731
self.name = "lightbug_http_client"
@@ -46,8 +40,7 @@ struct Client:
4640
pass
4741

4842
fn do(mut self, owned req: HTTPRequest) raises -> HTTPResponse:
49-
"""
50-
The `do` method is responsible for sending an HTTP request to a server and receiving the corresponding response.
43+
"""The `do` method is responsible for sending an HTTP request to a server and receiving the corresponding response.
5144
5245
It performs the following steps:
5346
1. Creates a connection to the server specified in the request.
@@ -58,72 +51,78 @@ struct Client:
5851
5952
Note: The code assumes that the `HTTPRequest` object passed as an argument has a valid URI with a host and port specified.
6053
61-
Parameters
62-
----------
63-
req : HTTPRequest :
64-
An `HTTPRequest` object representing the request to be sent.
54+
Args:
55+
req: An `HTTPRequest` object representing the request to be sent.
6556
66-
Returns
67-
-------
68-
HTTPResponse :
57+
Returns:
6958
The received response.
7059
71-
Raises
72-
------
73-
Error :
74-
If there is a failure in sending or receiving the message.
60+
Raises:
61+
Error: If there is a failure in sending or receiving the message.
7562
"""
76-
var uri = req.uri
77-
var host = uri.host
78-
79-
if host == "":
80-
raise Error("URI is nil")
63+
if req.uri.host == "":
64+
raise Error("Client.do: Request failed because the host field is empty.")
8165
var is_tls = False
8266

83-
if uri.is_https():
67+
if req.uri.is_https():
8468
is_tls = True
8569

8670
var host_str: String
8771
var port: Int
88-
89-
if ":" in host:
90-
var host_port = host.split(":")
72+
if ":" in req.uri.host:
73+
var host_port: List[String]
74+
try:
75+
host_port = req.uri.host.split(":")
76+
except:
77+
raise Error("Client.do: Failed to split host and port.")
9178
host_str = host_port[0]
9279
port = atol(host_port[1])
9380
else:
94-
host_str = host
81+
host_str = req.uri.host
9582
if is_tls:
9683
port = 443
9784
else:
9885
port = 80
9986

10087
var conn: SysConnection
10188
var cached_connection = False
102-
if host_str in self._connections:
89+
try:
10390
conn = self._connections[host_str]
10491
cached_connection = True
105-
else:
106-
conn = create_connection(socket(AF_INET, SOCK_STREAM, 0), host_str, port)
107-
self._connections[host_str] = conn
92+
except:
93+
# If connection is not cached, create a new one.
94+
try:
95+
conn = create_connection(socket(AF_INET, SOCK_STREAM, 0), host_str, port)
96+
self._connections[host_str] = conn
97+
except e:
98+
logger.error(e)
99+
raise Error("Client.do: Failed to create a connection to host.")
108100

109-
var bytes_sent = conn.write(encode(req))
110-
if bytes_sent == -1:
101+
var bytes_sent: Int
102+
try:
103+
bytes_sent = conn.write(encode(req))
104+
except e:
111105
# Maybe peer reset ungracefully, so try a fresh connection
112-
self._close_conn(host_str)
113-
if cached_connection:
114-
return self.do(req^)
115-
raise Error("Failed to send message")
106+
if str(e) == "SendError: Connection reset by peer.":
107+
logger.debug("Client.do: Connection reset by peer. Trying a fresh connection.")
108+
self._close_conn(host_str)
109+
if cached_connection:
110+
return self.do(req^)
111+
logger.error("Client.do: Failed to send message.")
112+
raise e
116113

114+
# TODO: What if the response is too large for the buffer? We should read until the end of the response.
117115
var new_buf = Bytes(capacity=default_buffer_size)
118116
var bytes_recv = conn.read(new_buf)
119117

120118
if bytes_recv == 0:
121119
self._close_conn(host_str)
122120
if cached_connection:
123121
return self.do(req^)
124-
raise Error("No response received")
122+
raise Error("Client.do: No response received from the server.")
123+
125124
try:
126-
var res = HTTPResponse.from_bytes(new_buf^, conn)
125+
var res = HTTPResponse.from_bytes(new_buf, conn)
127126
if res.is_redirect():
128127
self._close_conn(host_str)
129128
return self._handle_redirect(req^, res^)
@@ -140,9 +139,17 @@ struct Client:
140139
mut self, owned original_req: HTTPRequest, owned original_response: HTTPResponse
141140
) raises -> HTTPResponse:
142141
var new_uri: URI
143-
var new_location = original_response.headers[HeaderKey.LOCATION]
144-
if new_location.startswith("http"):
145-
new_uri = URI.parse_raises(new_location)
142+
var new_location: String
143+
try:
144+
new_location = original_response.headers[HeaderKey.LOCATION]
145+
except e:
146+
raise Error("Client._handle_redirect: `Location` header was not received in the response.")
147+
148+
if new_location and new_location.startswith("http"):
149+
try:
150+
new_uri = URI.parse_raises(new_location)
151+
except e:
152+
raise Error("Client._handle_redirect: Failed to parse the new URI - " + str(e))
146153
original_req.headers[HeaderKey.HOST] = new_uri.host
147154
else:
148155
new_uri = original_req.uri

lightbug_http/cookie/request_cookie_jar.mojo

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ struct RequestCookieJar(Writable, Stringable):
1919
self._inner[cookie[].name] = cookie[].value
2020

2121
fn parse_cookies(mut self, headers: Headers) raises:
22-
var cookie_header = headers[HeaderKey.COOKIE]
22+
var cookie_header = headers.get(HeaderKey.COOKIE)
2323
if not cookie_header:
2424
return None
25-
var cookie_strings = cookie_header.split("; ")
25+
26+
var cookie_strings = cookie_header.value().split("; ")
2627

2728
for chunk in cookie_strings:
2829
var key = String("")

0 commit comments

Comments
 (0)