Skip to content

Commit 0d320e7

Browse files
authored
Merge pull request #235 from thatstoasty/fix-socket
Enable the __del__ method for socket
2 parents 350d31b + e0ef3d4 commit 0d320e7

24 files changed

+703
-545
lines changed

benchmark/bench.mojo

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ from memory import Span
22
from benchmark import *
33
from lightbug_http.io.bytes import bytes, Bytes
44
from lightbug_http.header import Headers, Header
5-
from lightbug_http.utils import ByteReader, ByteWriter
5+
from lightbug_http.io.bytes import ByteReader, ByteWriter
66
from lightbug_http.http import HTTPRequest, HTTPResponse, encode
77
from lightbug_http.uri import URI
88

@@ -11,9 +11,7 @@ alias headers = "GET /index.html HTTP/1.1\r\nHost: example.com\r\nUser-Agent: Mo
1111
alias body = "I am the body of an HTTP request" * 5
1212
alias body_bytes = bytes(body)
1313
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:"
15-
" application/octet-stream\r\nconnection: keep-alive\r\ncontent-length:"
16-
" 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body
14+
alias Response = "HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type: application/octet-stream\r\nconnection: keep-alive\r\ncontent-length: 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n" + body
1715

1816

1917
fn main():
@@ -26,24 +24,12 @@ fn run_benchmark():
2624
config.verbose_timing = True
2725
config.tabular_view = True
2826
var m = Bench(config)
29-
m.bench_function[lightbug_benchmark_header_encode](
30-
BenchId("HeaderEncode")
31-
)
32-
m.bench_function[lightbug_benchmark_header_parse](
33-
BenchId("HeaderParse")
34-
)
35-
m.bench_function[lightbug_benchmark_request_encode](
36-
BenchId("RequestEncode")
37-
)
38-
m.bench_function[lightbug_benchmark_request_parse](
39-
BenchId("RequestParse")
40-
)
41-
m.bench_function[lightbug_benchmark_response_encode](
42-
BenchId("ResponseEncode")
43-
)
44-
m.bench_function[lightbug_benchmark_response_parse](
45-
BenchId("ResponseParse")
46-
)
27+
m.bench_function[lightbug_benchmark_header_encode](BenchId("HeaderEncode"))
28+
m.bench_function[lightbug_benchmark_header_parse](BenchId("HeaderParse"))
29+
m.bench_function[lightbug_benchmark_request_encode](BenchId("RequestEncode"))
30+
m.bench_function[lightbug_benchmark_request_parse](BenchId("RequestParse"))
31+
m.bench_function[lightbug_benchmark_response_encode](BenchId("ResponseEncode"))
32+
m.bench_function[lightbug_benchmark_response_parse](BenchId("ResponseParse"))
4733
m.dump_report()
4834
except:
4935
print("failed to start benchmark")
@@ -102,12 +88,12 @@ fn lightbug_benchmark_request_encode(mut b: Bencher):
10288
fn request_encode() raises:
10389
var uri = URI.parse("http://127.0.0.1:8080/some-path")
10490
var req = HTTPRequest(
105-
uri=uri,
106-
headers=headers_struct,
107-
body=body_bytes,
91+
uri=uri,
92+
headers=headers_struct,
93+
body=body_bytes,
10894
)
10995
_ = encode(req^)
110-
96+
11197
try:
11298
b.iter[request_encode]()
11399
except e:
@@ -134,8 +120,7 @@ fn lightbug_benchmark_header_parse(mut b: Bencher):
134120
var header = Headers()
135121
var reader = ByteReader(headers.as_bytes())
136122
_ = header.parse_raw(reader)
137-
except:
138-
print("failed")
123+
except e:
124+
print("failed", e)
139125

140126
b.iter[header_parse]()
141-
File renamed without changes.

lightbug_http/_logger.mojo

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from sys.param_env import env_get_string
2+
3+
4+
struct LogLevel:
5+
alias FATAL = 0
6+
alias ERROR = 1
7+
alias WARN = 2
8+
alias INFO = 3
9+
alias DEBUG = 4
10+
11+
12+
fn get_log_level() -> Int:
13+
"""Returns the log level based on the parameter environment variable `LOG_LEVEL`.
14+
15+
Returns:
16+
The log level.
17+
"""
18+
alias level = env_get_string["LB_LOG_LEVEL", "INFO"]()
19+
if level == "INFO":
20+
return LogLevel.INFO
21+
elif level == "WARN":
22+
return LogLevel.WARN
23+
elif level == "ERROR":
24+
return LogLevel.ERROR
25+
elif level == "DEBUG":
26+
return LogLevel.DEBUG
27+
elif level == "FATAL":
28+
return LogLevel.FATAL
29+
else:
30+
return LogLevel.INFO
31+
32+
33+
alias LOG_LEVEL = get_log_level()
34+
"""Logger level determined by the `LB_LOG_LEVEL` param environment variable.
35+
36+
When building or running the application, you can set `LB_LOG_LEVEL` by providing the the following option:
37+
38+
```bash
39+
mojo build ... -D LB_LOG_LEVEL=DEBUG
40+
# or
41+
mojo ... -D LB_LOG_LEVEL=DEBUG
42+
```
43+
"""
44+
45+
46+
@value
47+
struct Logger[level: Int]:
48+
alias STDOUT = 1
49+
alias STDERR = 2
50+
51+
fn _log_message[event_level: Int](self, message: String):
52+
@parameter
53+
if level >= event_level:
54+
55+
@parameter
56+
if event_level < LogLevel.WARN:
57+
# Write to stderr if FATAL or ERROR
58+
print(message, file=Self.STDERR)
59+
else:
60+
print(message)
61+
62+
fn info[*Ts: Writable](self, *messages: *Ts):
63+
var msg = String.write("\033[36mINFO\033[0m - ")
64+
65+
@parameter
66+
fn write_message[T: Writable](message: T):
67+
msg.write(message, " ")
68+
69+
messages.each[write_message]()
70+
self._log_message[LogLevel.INFO](msg)
71+
72+
fn warn[*Ts: Writable](self, *messages: *Ts):
73+
var msg = String.write("\033[33mWARN\033[0m - ")
74+
75+
@parameter
76+
fn write_message[T: Writable](message: T):
77+
msg.write(message, " ")
78+
79+
messages.each[write_message]()
80+
self._log_message[LogLevel.WARN](msg)
81+
82+
fn error[*Ts: Writable](self, *messages: *Ts):
83+
var msg = String.write("\033[31mERROR\033[0m - ")
84+
85+
@parameter
86+
fn write_message[T: Writable](message: T):
87+
msg.write(message, " ")
88+
89+
messages.each[write_message]()
90+
self._log_message[LogLevel.ERROR](msg)
91+
92+
fn debug[*Ts: Writable](self, *messages: *Ts):
93+
var msg = String.write("\033[34mDEBUG\033[0m - ")
94+
95+
@parameter
96+
fn write_message[T: Writable](message: T):
97+
msg.write(message, " ")
98+
99+
messages.each[write_message]()
100+
self._log_message[LogLevel.DEBUG](msg)
101+
102+
fn fatal[*Ts: Writable](self, *messages: *Ts):
103+
var msg = String.write("\033[35mFATAL\033[0m - ")
104+
105+
@parameter
106+
fn write_message[T: Writable](message: T):
107+
msg.write(message, " ")
108+
109+
messages.each[write_message]()
110+
self._log_message[LogLevel.FATAL](msg)
111+
112+
113+
alias logger = Logger[LOG_LEVEL]()
File renamed without changes.

lightbug_http/client.mojo

Lines changed: 38 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
from collections import Dict
2+
from utils import StringSlice
23
from memory import UnsafePointer
3-
from lightbug_http.libc import (
4-
c_int,
5-
AF_INET,
6-
SOCK_STREAM,
7-
socket,
8-
connect,
9-
send,
10-
recv,
11-
close,
12-
)
13-
from lightbug_http.strings import to_string
144
from lightbug_http.net import default_buffer_size
155
from lightbug_http.http import HTTPRequest, HTTPResponse, encode
166
from lightbug_http.header import Headers, HeaderKey
177
from lightbug_http.net import create_connection, TCPConnection
18-
from lightbug_http.io.bytes import Bytes
19-
from lightbug_http.utils import ByteReader, logger
20-
from lightbug_http.pool_manager import PoolManager
8+
from lightbug_http.io.bytes import Bytes, ByteReader
9+
from lightbug_http._logger import logger
10+
from lightbug_http.pool_manager import PoolManager, PoolKey
11+
from lightbug_http.uri import URI, Scheme
2112

2213

2314
struct Client:
@@ -41,7 +32,7 @@ struct Client:
4132
self.allow_redirects = allow_redirects
4233
self._connections = PoolManager[TCPConnection](cached_connections)
4334

44-
fn do(mut self, owned req: HTTPRequest) raises -> HTTPResponse:
35+
fn do(mut self, owned request: HTTPRequest) raises -> HTTPResponse:
4536
"""The `do` method is responsible for sending an HTTP request to a server and receiving the corresponding response.
4637
4738
It performs the following steps:
@@ -54,81 +45,77 @@ struct Client:
5445
Note: The code assumes that the `HTTPRequest` object passed as an argument has a valid URI with a host and port specified.
5546
5647
Args:
57-
req: An `HTTPRequest` object representing the request to be sent.
48+
request: An `HTTPRequest` object representing the request to be sent.
5849
5950
Returns:
6051
The received response.
6152
6253
Raises:
6354
Error: If there is a failure in sending or receiving the message.
6455
"""
65-
if req.uri.host == "":
66-
raise Error("Client.do: Request failed because the host field is empty.")
67-
var is_tls = False
56+
if request.uri.host == "":
57+
raise Error("Client.do: Host must not be empty.")
6858

69-
if req.uri.is_https():
59+
var is_tls = False
60+
var scheme = Scheme.HTTP
61+
if request.uri.is_https():
7062
is_tls = True
63+
scheme = Scheme.HTTPS
7164

72-
var host_str: String
73-
var port: Int
74-
if ":" in req.uri.host:
75-
var host_port: List[String]
76-
try:
77-
host_port = req.uri.host.split(":")
78-
except:
79-
raise Error("Client.do: Failed to split host and port.")
80-
host_str = host_port[0]
81-
port = atol(host_port[1])
65+
var port: UInt16
66+
if request.uri.port:
67+
port = request.uri.port.value()
8268
else:
83-
host_str = req.uri.host
84-
if is_tls:
69+
if request.uri.scheme == Scheme.HTTP.value:
70+
port = 80
71+
elif request.uri.scheme == Scheme.HTTPS.value:
8572
port = 443
8673
else:
87-
port = 80
74+
raise Error("Client.do: Invalid scheme received in the URI.")
8875

76+
var pool_key = PoolKey(request.uri.host, port, scheme)
8977
var cached_connection = False
9078
var conn: TCPConnection
9179
try:
92-
conn = self._connections.take(host_str)
80+
conn = self._connections.take(pool_key)
9381
cached_connection = True
9482
except e:
9583
if str(e) == "PoolManager.take: Key not found.":
96-
conn = create_connection(host_str, port)
84+
conn = create_connection(request.uri.host, port)
9785
else:
9886
logger.error(e)
9987
raise Error("Client.do: Failed to create a connection to host.")
10088

10189
var bytes_sent: Int
10290
try:
103-
bytes_sent = conn.write(encode(req))
91+
bytes_sent = conn.write(encode(request))
10492
except e:
10593
# Maybe peer reset ungracefully, so try a fresh connection
10694
if str(e) == "SendError: Connection reset by peer.":
10795
logger.debug("Client.do: Connection reset by peer. Trying a fresh connection.")
10896
conn.teardown()
10997
if cached_connection:
110-
return self.do(req^)
98+
return self.do(request^)
11199
logger.error("Client.do: Failed to send message.")
112100
raise e
113101

114102
# TODO: What if the response is too large for the buffer? We should read until the end of the response. (@thatstoasty)
115103
var new_buf = Bytes(capacity=default_buffer_size)
116-
117104
try:
118105
_ = conn.read(new_buf)
119106
except e:
120107
if str(e) == "EOF":
121108
conn.teardown()
122109
if cached_connection:
123-
return self.do(req^)
110+
return self.do(request^)
124111
raise Error("Client.do: No response received from the server.")
125112
else:
126113
logger.error(e)
127114
raise Error("Client.do: Failed to read response from peer.")
128115

129-
var res: HTTPResponse
116+
var response: HTTPResponse
130117
try:
131-
res = HTTPResponse.from_bytes(new_buf, conn)
118+
response = HTTPResponse.from_bytes(new_buf, conn)
132119
except e:
133120
logger.error("Failed to parse a response...")
134121
try:
@@ -138,19 +125,19 @@ struct Client:
138125
raise e
139126

140127
# Redirects should not keep the connection alive, as redirects can send the client to a different server.
141-
if self.allow_redirects and res.is_redirect():
128+
if self.allow_redirects and response.is_redirect():
142129
conn.teardown()
143-
return self._handle_redirect(req^, res^)
130+
return self._handle_redirect(request^, response^)
144131
# Server told the client to close the connection, we can assume the server closed their side after sending the response.
145-
elif res.connection_close():
132+
elif response.connection_close():
146133
conn.teardown()
147134
# Otherwise, persist the connection by giving it back to the pool manager.
148135
else:
149-
self._connections.give(host_str, conn^)
150-
return res
136+
self._connections.give(pool_key, conn^)
137+
return response
151138

152139
fn _handle_redirect(
153-
mut self, owned original_req: HTTPRequest, owned original_response: HTTPResponse
140+
mut self, owned original_request: HTTPRequest, owned original_response: HTTPResponse
154141
) raises -> HTTPResponse:
155142
var new_uri: URI
156143
var new_location: String
@@ -164,9 +151,9 @@ struct Client:
164151
new_uri = URI.parse(new_location)
165152
except e:
166153
raise Error("Client._handle_redirect: Failed to parse the new URI: " + str(e))
167-
original_req.headers[HeaderKey.HOST] = new_uri.host
154+
original_request.headers[HeaderKey.HOST] = new_uri.host
168155
else:
169-
new_uri = original_req.uri
156+
new_uri = original_request.uri
170157
new_uri.path = new_location
171-
original_req.uri = new_uri
172-
return self.do(original_req^)
158+
original_request.uri = new_uri
159+
return self.do(original_request^)

lightbug_http/cookie/request_cookie_jar.mojo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ from small_time import SmallTime, TimeZone
33
from small_time.small_time import strptime
44
from lightbug_http.strings import to_string, lineBreak
55
from lightbug_http.header import HeaderKey, write_header
6-
from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space
6+
from lightbug_http.io.bytes import ByteReader, ByteWriter, is_newline, is_space
77

88

99
@value

lightbug_http/cookie/response_cookie_jar.mojo

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections import Optional, List, Dict, KeyElement
22
from lightbug_http.strings import to_string
33
from lightbug_http.header import HeaderKey, write_header
4-
from lightbug_http.utils import ByteWriter
4+
from lightbug_http.io.bytes import ByteWriter
55

66

77
@value

0 commit comments

Comments
 (0)