Skip to content

Commit 805372e

Browse files
authored
Merge pull request #61 from bgreni/header-refactor
Refactor header parsing and data structure
2 parents 313595b + 1483253 commit 805372e

34 files changed

+1139
-2326
lines changed

README.md

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,15 @@ Once you have a Mojo project set up locally,
8888
@value
8989
struct Printer(HTTPService):
9090
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
91-
var uri = req.uri()
92-
print("Request URI: ", to_string(uri.request_uri()))
93-
94-
var header = req.header
95-
print("Request protocol: ", header.protocol_str())
96-
print("Request method: ", to_string(header.method()))
97-
print("Request Content-Type: ", to_string(header.content_type()))
91+
var uri = req.uri
92+
print("Request URI: ", to_string(uri.request_uri))
93+
94+
var header = req.headers
95+
print("Request protocol: ", req.protocol)
96+
print("Request method: ", req.method)
97+
print(
98+
"Request Content-Type: ", to_string(header[HeaderKey.CONTENT_TYPE])
99+
)
98100
99101
var body = req.body_raw
100102
print("Request Body: ", to_string(body))
@@ -103,9 +105,11 @@ Once you have a Mojo project set up locally,
103105
```
104106
6. Start a server listening on a port with your service like so.
105107
```mojo
108+
from lightbug_http import Welcome, SysServer
109+
106110
fn main() raises:
107111
var server = SysServer()
108-
var handler = Printer()
112+
var handler = Welcome()
109113
server.listen_and_serve("0.0.0.0:8080", handler)
110114
```
111115
Feel free to change the settings in `listen_and_serve()` to serve on a particular host and port.
@@ -123,15 +127,15 @@ from lightbug_http import *
123127
struct ExampleRouter(HTTPService):
124128
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
125129
var body = req.body_raw
126-
var uri = req.uri()
130+
var uri = req.uri
127131
128-
if uri.path() == "/":
132+
if uri.path == "/":
129133
print("I'm on the index path!")
130-
if uri.path() == "/first":
134+
if uri.path == "/first":
131135
print("I'm on /first!")
132-
elif uri.path() == "/second":
136+
elif uri.path == "/second":
133137
print("I'm on /second!")
134-
elif uri.path() == "/echo":
138+
elif uri.path == "/echo":
135139
print(to_string(body))
136140
137141
return OK(body)
@@ -152,21 +156,21 @@ from lightbug_http import *
152156
@value
153157
struct Welcome(HTTPService):
154158
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
155-
var uri = req.uri()
159+
var uri = req.uri
156160
157-
if uri.path() == "/":
161+
if uri.path == "/":
158162
var html: Bytes
159163
with open("static/lightbug_welcome.html", "r") as f:
160164
html = f.read_bytes()
161165
return OK(html, "text/html; charset=utf-8")
162-
163-
if uri.path() == "/logo.png":
166+
167+
if uri.path == "/logo.png":
164168
var image: Bytes
165169
with open("static/logo.png", "r") as f:
166170
image = f.read_bytes()
167171
return OK(image, "image/png")
168-
169-
return NotFound(uri.path())
172+
173+
return NotFound(uri.path)
170174
```
171175

172176
### Using the client
@@ -178,33 +182,34 @@ from lightbug_http import *
178182
from lightbug_http.sys.client import MojoClient
179183
180184
fn test_request(inout client: MojoClient) raises -> None:
181-
var uri = URI("http://httpbin.org/status/404")
182-
try:
183-
uri.parse()
184-
except e:
185-
print("error parsing uri: " + e.__str__())
186-
185+
var uri = URI.parse_raises("http://httpbin.org/status/404")
186+
var headers = Header("Host", "httpbin.org")
187187
188-
var request = HTTPRequest(uri)
189-
var response = client.do(request)
188+
var request = HTTPRequest(uri, headers)
189+
var response = client.do(request^)
190190
191191
# print status code
192-
print("Response:", response.header.status_code())
192+
print("Response:", response.status_code)
193193
194194
# print parsed headers (only some are parsed for now)
195-
print("Content-Type:", to_string(response.header.content_type()))
196-
print("Content-Length", response.header.content_length())
197-
print("Server:", to_string(response.header.server()))
195+
print("Content-Type:", response.headers["Content-Type"])
196+
print("Content-Length", response.headers["Content-Length"])
197+
print("Server:", to_string(response.headers["Server"]))
198198
199-
print("Is connection set to connection-close? ", response.header.connection_close())
199+
print(
200+
"Is connection set to connection-close? ", response.connection_close()
201+
)
200202
201203
# print body
202-
print(to_string(response.get_body_bytes()))
204+
print(to_string(response.body_raw))
203205
204206
205-
fn main() raises -> None:
206-
var client = MojoClient()
207-
test_request(client)
207+
fn main() -> None:
208+
try:
209+
var client = MojoClient()
210+
test_request(client)
211+
except e:
212+
print(e)
208213
```
209214

210215
Pure Mojo-based client is available by default. This client is also used internally for testing the server.

bench.mojo

Lines changed: 149 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,152 @@
1-
import benchmark
2-
from lightbug_http.sys.server import SysServer
3-
from lightbug_http.python.server import PythonServer
4-
from lightbug_http.service import TechEmpowerRouter
1+
from benchmark import *
2+
from lightbug_http.io.bytes import bytes, Bytes
3+
from lightbug_http.header import Headers, Header
4+
from lightbug_http.utils import ByteReader, ByteWriter
5+
from lightbug_http.http import HTTPRequest, HTTPResponse, encode
6+
from lightbug_http.uri import URI
57
from tests.utils import (
68
TestStruct,
79
FakeResponder,
810
new_fake_listener,
911
FakeServer,
10-
getRequest,
1112
)
1213

14+
alias headers = bytes(
15+
"""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"""
16+
)
17+
alias body = bytes(String("I am the body of an HTTP request") * 5)
18+
alias Request = bytes(
19+
"""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"""
20+
) + body
21+
alias Response = bytes(
22+
"HTTP/1.1 200 OK\r\nserver: lightbug_http\r\ncontent-type:"
23+
" application/octet-stream\r\nconnection: keep-alive\r\ncontent-length:"
24+
" 13\r\ndate: 2024-06-02T13:41:50.766880+00:00\r\n\r\n"
25+
) + body
26+
1327

1428
fn main():
29+
run_benchmark()
30+
31+
32+
fn run_benchmark():
1533
try:
16-
var server = SysServer(tcp_keep_alive=True)
17-
var handler = TechEmpowerRouter()
18-
server.listen_and_serve("0.0.0.0:8080", handler)
19-
except e:
20-
print("Error starting server: " + e.__str__())
21-
return
34+
var config = BenchConfig(warmup_iters=100)
35+
config.verbose_timing = True
36+
config.tabular_view = True
37+
var m = Bench(config)
38+
m.bench_function[lightbug_benchmark_header_encode](
39+
BenchId("HeaderEncode")
40+
)
41+
m.bench_function[lightbug_benchmark_header_parse](
42+
BenchId("HeaderParse")
43+
)
44+
m.bench_function[lightbug_benchmark_request_encode](
45+
BenchId("RequestEncode")
46+
)
47+
m.bench_function[lightbug_benchmark_request_parse](
48+
BenchId("RequestParse")
49+
)
50+
m.bench_function[lightbug_benchmark_response_encode](
51+
BenchId("ResponseEncode")
52+
)
53+
m.bench_function[lightbug_benchmark_response_parse](
54+
BenchId("ResponseParse")
55+
)
56+
m.dump_report()
57+
except:
58+
print("failed to start benchmark")
59+
60+
61+
var headers_struct = Headers(
62+
Header("Content-Type", "application/json"),
63+
Header("Content-Length", "1234"),
64+
Header("Connection", "close"),
65+
Header("Date", "some-datetime"),
66+
Header("SomeHeader", "SomeValue"),
67+
)
68+
69+
70+
@parameter
71+
fn lightbug_benchmark_response_encode(inout b: Bencher):
72+
@always_inline
73+
@parameter
74+
fn response_encode():
75+
var res = HTTPResponse(body, headers=headers_struct)
76+
_ = encode(res^)
77+
78+
b.iter[response_encode]()
79+
80+
81+
@parameter
82+
fn lightbug_benchmark_response_parse(inout b: Bencher):
83+
@always_inline
84+
@parameter
85+
fn response_parse():
86+
var res = Response
87+
try:
88+
_ = HTTPResponse.from_bytes(res^)
89+
except:
90+
pass
91+
92+
b.iter[response_parse]()
93+
94+
95+
@parameter
96+
fn lightbug_benchmark_request_parse(inout b: Bencher):
97+
@always_inline
98+
@parameter
99+
fn request_parse():
100+
var r = Request
101+
try:
102+
_ = HTTPRequest.from_bytes("127.0.0.1/path", 4096, r^)
103+
except:
104+
pass
105+
106+
b.iter[request_parse]()
107+
108+
109+
@parameter
110+
fn lightbug_benchmark_request_encode(inout b: Bencher):
111+
@always_inline
112+
@parameter
113+
fn request_encode():
114+
var req = HTTPRequest(
115+
URI.parse("http://127.0.0.1:8080/some-path")[URI],
116+
headers=headers_struct,
117+
body=body,
118+
)
119+
_ = encode(req^)
120+
121+
b.iter[request_encode]()
122+
123+
124+
@parameter
125+
fn lightbug_benchmark_header_encode(inout b: Bencher):
126+
@always_inline
127+
@parameter
128+
fn header_encode():
129+
var b = ByteWriter()
130+
var h = headers_struct
131+
h.encode_to(b)
132+
133+
b.iter[header_encode]()
134+
135+
136+
@parameter
137+
fn lightbug_benchmark_header_parse(inout b: Bencher):
138+
@always_inline
139+
@parameter
140+
fn header_parse():
141+
try:
142+
var b = headers
143+
var header = Headers()
144+
var reader = ByteReader(b^)
145+
_ = header.parse_raw(reader)
146+
except:
147+
print("failed")
148+
149+
b.iter[header_parse]()
22150

23151

24152
fn lightbug_benchmark_server():
@@ -28,19 +156,26 @@ fn lightbug_benchmark_server():
28156

29157

30158
fn lightbug_benchmark_misc() -> None:
31-
var direct_set_report = benchmark.run[init_test_and_set_a_direct](max_iters=1)
159+
var direct_set_report = benchmark.run[init_test_and_set_a_direct](
160+
max_iters=1
161+
)
32162

33-
var recreating_set_report = benchmark.run[init_test_and_set_a_copy](max_iters=1)
163+
var recreating_set_report = benchmark.run[init_test_and_set_a_copy](
164+
max_iters=1
165+
)
34166

35167
print("Direct set: ")
36168
direct_set_report.print(benchmark.Unit.ms)
37169
print("Recreating set: ")
38170
recreating_set_report.print(benchmark.Unit.ms)
39171

40172

173+
var GetRequest = HTTPRequest(URI.parse("http://127.0.0.1/path")[URI])
174+
175+
41176
fn run_fake_server():
42177
var handler = FakeResponder()
43-
var listener = new_fake_listener(2, getRequest)
178+
var listener = new_fake_listener(2, encode(GetRequest))
44179
var server = FakeServer(listener, handler)
45180
server.serve()
46181

bench_server.mojo

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from lightbug_http.sys.server import SysServer
2+
from lightbug_http.service import TechEmpowerRouter
3+
4+
5+
def main():
6+
try:
7+
var server = SysServer(tcp_keep_alive=True)
8+
var handler = TechEmpowerRouter()
9+
server.listen_and_serve("0.0.0.0:8080", handler)
10+
except e:
11+
print("Error starting server: " + e.__str__())
12+
return

client.mojo

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
from lightbug_http import *
22
from lightbug_http.sys.client import MojoClient
33

4-
fn test_request(inout client: MojoClient) raises -> None:
5-
var uri = URI("http://httpbin.org/status/404")
6-
try:
7-
uri.parse()
8-
except e:
9-
print("error parsing uri: " + e.__str__())
104

5+
fn test_request(inout client: MojoClient) raises -> None:
6+
var uri = URI.parse_raises("http://httpbin.org/status/404")
7+
var headers = Header("Host", "httpbin.org")
118

12-
var request = HTTPRequest(uri)
13-
var response = client.do(request)
9+
var request = HTTPRequest(uri, headers)
10+
var response = client.do(request^)
1411

1512
# print status code
16-
print("Response:", response.header.status_code())
13+
print("Response:", response.status_code)
1714

1815
# print parsed headers (only some are parsed for now)
19-
print("Content-Type:", to_string(response.header.content_type()))
20-
print("Content-Length", response.header.content_length())
21-
print("Server:", to_string(response.header.server()))
16+
print("Content-Type:", response.headers["Content-Type"])
17+
print("Content-Length", response.headers["Content-Length"])
18+
print("Server:", to_string(response.headers["Server"]))
2219

23-
print("Is connection set to connection-close? ", response.header.connection_close())
20+
print(
21+
"Is connection set to connection-close? ", response.connection_close()
22+
)
2423

2524
# print body
26-
print(to_string(response.get_body_bytes()))
25+
print(to_string(response.body_raw))
2726

2827

29-
fn main() raises -> None:
30-
var client = MojoClient()
31-
test_request(client)
28+
fn main() -> None:
29+
try:
30+
var client = MojoClient()
31+
test_request(client)
32+
except e:
33+
print(e)

0 commit comments

Comments
 (0)