Skip to content

Commit ea222eb

Browse files
committed
feat(quic,http): implement QUIC FIN signaling, HTTP/1 body limit, and review fixes
QUIC FIN layer: - Add NGTCP2_WRITE_STREAM_FLAG_FIN/MORE constants and flags parameter to conn_writev_stream - Register recv_stream_data and stream_close ngtcp2 callbacks with FIN detection and overflow-safe event buffering - Add send_fin(), send_with_fin(), send_with_flags() methods - Add drain_stream_events() with error propagation on overflow - Add ensure_stream(), stream_has_fin(), stream_exists() abstraction API - Auto-create stream entries for FIN events on unknown streams HTTP/3 FIN integration: - Replace non-standard empty DATA frame end-marker with proper QUIC FIN signaling per RFC 9114 §4.1 - Client sends FIN after last frame via send_frame_with_fin() - Server detects request completion via check_fin_completions() sweep after frame processing, handling separate-packet FIN and empty-body POST/PUT/PATCH - Server coalesces response FIN with last data write - Per-connection packet_mu mutex serializing QUIC state mutations - Split process_packet_frames into ingest/decode/dispatch helpers HTTP/1 hardening: - Add max_request_body_size (10MB default) to Server struct matching HTTP/2 and HTTP/3 defaults - Add parse_request_with_limit() checking Content-Length before allocation - Strict Content-Length validation rejecting negative, non-numeric, and overflow values via validate_and_parse_content_length() - Detect truncated request bodies (unexpected EOF) - Backward-compatible Handler interface with ServerHandler adapter
1 parent ede3439 commit ea222eb

52 files changed

Lines changed: 3551 additions & 1801 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,7 @@ autofuzz.log
169169
CHANGELOG.md
170170
v
171171
vnew
172-
vnew.*
172+
vnew.*
173+
174+
docs/
175+
*.md

examples/binary_upload_server.v

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Binary Upload/Download Server Example
2+
// Demonstrates []u8 body handling for file uploads and binary responses.
3+
module main
4+
5+
import net.http
6+
import os
7+
8+
struct BinaryHandler {}
9+
10+
fn (h BinaryHandler) handle(req http.ServerRequest) http.ServerResponse {
11+
match req.path {
12+
'/upload' {
13+
if req.method != .post {
14+
return http.ServerResponse{
15+
status_code: 405
16+
body: 'Method Not Allowed'.bytes()
17+
}
18+
}
19+
content_type := req.header.get(.content_type) or { 'application/octet-stream' }
20+
println('[upload] received ${req.body.len} bytes (${content_type})')
21+
22+
os.write_file_array('/tmp/uploaded_file', req.body) or {
23+
return http.ServerResponse{
24+
status_code: 500
25+
body: 'Failed to save file: ${err}'.bytes()
26+
}
27+
}
28+
29+
mut header := http.new_header()
30+
header.add(.content_type, 'application/json')
31+
return http.ServerResponse{
32+
status_code: 200
33+
header: header
34+
body: '{"status":"ok","size":${req.body.len}}'.bytes()
35+
}
36+
}
37+
'/download' {
38+
data := os.read_bytes('/tmp/uploaded_file') or {
39+
return http.ServerResponse{
40+
status_code: 404
41+
body: 'No file uploaded yet'.bytes()
42+
}
43+
}
44+
mut header := http.new_header()
45+
header.add(.content_type, 'application/octet-stream')
46+
header.add(.content_disposition, 'attachment; filename="downloaded_file"')
47+
return http.ServerResponse{
48+
status_code: 200
49+
header: header
50+
body: data
51+
}
52+
}
53+
'/generate' {
54+
size := 1024 * 1024 // 1 MB of binary data
55+
mut data := []u8{len: size}
56+
for i in 0 .. size {
57+
data[i] = u8(i % 256)
58+
}
59+
mut header := http.new_header()
60+
header.add(.content_type, 'application/octet-stream')
61+
return http.ServerResponse{
62+
status_code: 200
63+
header: header
64+
body: data
65+
}
66+
}
67+
'/' {
68+
return http.ServerResponse{
69+
status_code: 200
70+
body: 'Binary Server\n\nEndpoints:\n POST /upload - upload binary file\n GET /download - download last uploaded file\n GET /generate - download 1MB generated binary\n'.bytes()
71+
}
72+
}
73+
else {
74+
return http.ServerResponse{
75+
status_code: 404
76+
body: 'Not found'.bytes()
77+
}
78+
}
79+
}
80+
}
81+
82+
fn main() {
83+
mut server := http.Server{
84+
addr: ':8080'
85+
handler: BinaryHandler{}
86+
}
87+
server.listen_and_serve()
88+
}

examples/http2/01_simple_server.v

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
// Simple HTTP/2 Server Example
22
// Demonstrates basic HTTP/2 server usage
3+
import net.http
34
import net.http.v2
45

56
fn main() {
6-
// Create server configuration
77
config := v2.ServerConfig{
88
addr: '0.0.0.0:8080'
99
max_concurrent_streams: 100
1010
initial_window_size: 65535
1111
max_frame_size: 16384
1212
}
1313

14-
// Create server with handler
1514
mut server := v2.new_server(config, handle_request) or {
1615
eprintln('Failed to create server: ${err}')
1716
return
@@ -21,49 +20,46 @@ fn main() {
2120
println('Test with: curl --http2-prior-knowledge http://localhost:8080/')
2221
println('Press Ctrl+C to stop')
2322

24-
// Start server (blocks)
2523
server.listen_and_serve() or { eprintln('Server error: ${err}') }
2624
}
2725

28-
// handle_request processes HTTP/2 requests
29-
fn handle_request(req v2.ServerRequest) v2.ServerResponse {
26+
fn handle_request(req http.ServerRequest) http.ServerResponse {
3027
println('Received: ${req.method} ${req.path}')
3128

32-
// Route requests
3329
match req.path {
3430
'/' {
35-
return v2.ServerResponse{
31+
return http.ServerResponse{
3632
status_code: 200
37-
headers: {
33+
header: http.from_map({
3834
'content-type': 'text/html; charset=utf-8'
39-
}
35+
})
4036
body: '<h1>Hello from HTTP/2!</h1><p>This is a V HTTP/2 server.</p>'.bytes()
4137
}
4238
}
4339
'/json' {
44-
return v2.ServerResponse{
40+
return http.ServerResponse{
4541
status_code: 200
46-
headers: {
42+
header: http.from_map({
4743
'content-type': 'application/json'
48-
}
44+
})
4945
body: '{"message":"Hello from HTTP/2","protocol":"h2"}'.bytes()
5046
}
5147
}
5248
'/echo' {
53-
return v2.ServerResponse{
49+
return http.ServerResponse{
5450
status_code: 200
55-
headers: {
51+
header: http.from_map({
5652
'content-type': 'text/plain'
57-
}
53+
})
5854
body: 'Method: ${req.method}\nPath: ${req.path}\n'.bytes()
5955
}
6056
}
6157
else {
62-
return v2.ServerResponse{
58+
return http.ServerResponse{
6359
status_code: 404
64-
headers: {
60+
header: http.from_map({
6561
'content-type': 'text/plain'
66-
}
62+
})
6763
body: 'Not Found'.bytes()
6864
}
6965
}

examples/http3/02_simple_server.v

Lines changed: 38 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,23 @@
1313
// openssl req -x509 -newkey rsa:2048 -nodes \
1414
// -keyout server.key -out server.crt -days 365 \
1515
// -subj "/CN=localhost"
16+
import net.http
1617
import net.http.v3
1718

1819
fn main() {
1920
println('=== HTTP/3 Server Example ===\n')
2021

21-
// Create HTTP/3 server configuration
2222
mut config := v3.ServerConfig{
23-
addr: '0.0.0.0:4433' // HTTP/3 typically uses port 443 or 4433
24-
max_idle_timeout: 30000 // 30 seconds
25-
max_data: 10485760 // 10MB
26-
max_stream_data: 1048576 // 1MB
27-
// TLS certificate (required for QUIC)
23+
addr: '0.0.0.0:4433'
24+
max_idle_timeout: 30000
25+
max_data: 10485760
26+
max_stream_data: 1048576
2827
cert_file: 'server.crt'
2928
key_file: 'server.key'
3029
}
3130

32-
// Set request handler
3331
config.handler = handle_request
3432

35-
// Create HTTP/3 server
3633
mut server := v3.new_server(config) or {
3734
eprintln('Failed to create HTTP/3 server: ${err}')
3835
eprintln('\nThis is expected - HTTP/3 requires full QUIC implementation.')
@@ -51,70 +48,67 @@ fn main() {
5148
println('Test with: curl --http3 https://localhost:4433/')
5249
println('Press Ctrl+C to stop\n')
5350

54-
// Start server (blocks)
55-
server.start() or { eprintln('Server error: ${err}') }
51+
server.listen_and_serve() or { eprintln('Server error: ${err}') }
5652
}
5753

58-
// handle_request processes HTTP/3 requests
59-
fn handle_request(req v3.ServerRequest) v3.ServerResponse {
54+
fn handle_request(req http.ServerRequest) http.ServerResponse {
6055
println('[HTTP/3] ${req.method} ${req.path}')
6156

62-
// Route requests
6357
match req.path {
6458
'/' {
65-
return v3.ServerResponse{
59+
return http.ServerResponse{
6660
status_code: 200
67-
headers: {
61+
header: http.from_map({
6862
'content-type': 'text/html; charset=utf-8'
69-
'alt-svc': 'h3=":4433"; ma=86400' // Advertise HTTP/3 support
70-
}
71-
body: html_home()
63+
'alt-svc': 'h3=":4433"; ma=86400'
64+
})
65+
body: html_home().bytes()
7266
}
7367
}
7468
'/json' {
75-
return v3.ServerResponse{
69+
return http.ServerResponse{
7670
status_code: 200
77-
headers: {
71+
header: http.from_map({
7872
'content-type': 'application/json'
7973
'alt-svc': 'h3=":4433"; ma=86400'
80-
}
81-
body: json_response()
74+
})
75+
body: json_response().bytes()
8276
}
8377
}
8478
'/echo' {
85-
return v3.ServerResponse{
79+
return http.ServerResponse{
8680
status_code: 200
87-
headers: {
81+
header: http.from_map({
8882
'content-type': 'text/plain'
8983
'alt-svc': 'h3=":4433"; ma=86400'
90-
}
91-
body: echo_response(req)
84+
})
85+
body: echo_response(req).bytes()
9286
}
9387
}
9488
'/stream' {
95-
return v3.ServerResponse{
89+
return http.ServerResponse{
9690
status_code: 200
97-
headers: {
91+
header: http.from_map({
9892
'content-type': 'text/plain'
9993
'alt-svc': 'h3=":4433"; ma=86400'
100-
}
101-
body: stream_response()
94+
})
95+
body: stream_response().bytes()
10296
}
10397
}
10498
else {
105-
return v3.ServerResponse{
99+
return http.ServerResponse{
106100
status_code: 404
107-
headers: {
101+
header: http.from_map({
108102
'content-type': 'text/plain'
109-
}
103+
})
110104
body: '404 Not Found\n'.bytes()
111105
}
112106
}
113107
}
114108
}
115109

116-
fn html_home() []u8 {
117-
html := '<!DOCTYPE html>
110+
fn html_home() string {
111+
return '<!DOCTYPE html>
118112
<html>
119113
<head>
120114
<title>HTTP/3 Server</title>
@@ -127,7 +121,7 @@ fn html_home() []u8 {
127121
</style>
128122
</head>
129123
<body>
130-
<h1>🚀 HTTP/3 Server (QUIC)</h1>
124+
<h1>HTTP/3 Server (QUIC)</h1>
131125
<div class="protocol">
132126
<strong>Protocol:</strong> HTTP/3 over QUIC<br>
133127
<strong>Features:</strong> Multiplexing, 0-RTT, UDP-based
@@ -143,11 +137,10 @@ fn html_home() []u8 {
143137
</div>
144138
</body>
145139
</html>'
146-
return html.bytes()
147140
}
148141

149-
fn json_response() []u8 {
150-
json := '{
142+
fn json_response() string {
143+
return '{
151144
"message": "Hello from HTTP/3!",
152145
"protocol": "h3",
153146
"transport": "QUIC",
@@ -158,30 +151,30 @@ fn json_response() []u8 {
158151
"Built-in Encryption"
159152
]
160153
}'
161-
return json.bytes()
162154
}
163155

164-
fn echo_response(req v3.ServerRequest) []u8 {
156+
fn echo_response(req http.ServerRequest) string {
165157
mut response := 'HTTP/3 Echo Response\n'
166158
response += '===================\n\n'
167159
response += 'Method: ${req.method}\n'
168160
response += 'Path: ${req.path}\n'
169161
response += 'Stream ID: ${req.stream_id}\n'
170162
response += '\nHeaders:\n'
171-
for key, value in req.headers {
163+
for key in req.header.keys() {
164+
value := req.header.get_custom(key) or { '' }
172165
response += ' ${key}: ${value}\n'
173166
}
174167
response += '\nBody Length: ${req.body.len} bytes\n'
175-
return response.bytes()
168+
return response
176169
}
177170

178-
fn stream_response() []u8 {
171+
fn stream_response() string {
179172
mut response := 'HTTP/3 Stream Data\n'
180173
response += '==================\n\n'
181174
response += 'This demonstrates HTTP/3 multiplexing.\n'
182175
response += 'Multiple streams can be sent concurrently.\n\n'
183176
for i in 1 .. 11 {
184177
response += 'Stream chunk ${i}/10\n'
185178
}
186-
return response.bytes()
179+
return response
187180
}

0 commit comments

Comments
 (0)