From 364cf7c47c6883c4f591c56cc56e7360a4cfed23 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 18 Apr 2026 20:00:01 +0300 Subject: [PATCH] veb: fasthttp fixes --- vlib/fasthttp/fasthttp.v | 1 + vlib/fasthttp/fasthttp_darwin.v | 126 ++++++++++++++++++ vlib/v/checker/assign.v | 2 +- vlib/v/checker/checker.v | 2 +- vlib/v/checker/fn.v | 6 +- vlib/v/checker/match.v | 10 +- .../tests/enum_field_value_duplicate_d.out | 8 -- .../tests/enum_field_value_duplicate_e.out | 7 - vlib/v/checker/tests/enum_self_reference.out | 36 +---- vlib/v/parser/expr.v | 2 +- vlib/veb/veb_fasthttp.v | 1 + 11 files changed, 141 insertions(+), 60 deletions(-) diff --git a/vlib/fasthttp/fasthttp.v b/vlib/fasthttp/fasthttp.v index 67e206d5f..a19d8f2c2 100644 --- a/vlib/fasthttp/fasthttp.v +++ b/vlib/fasthttp/fasthttp.v @@ -77,6 +77,7 @@ pub: family net.AddrFamily = .ip6 port int = 3000 max_request_buffer_size int = 8192 + timeout_in_seconds int = 30 handler fn (HttpRequest) !HttpResponse @[required] user_data voidptr } diff --git a/vlib/fasthttp/fasthttp_darwin.v b/vlib/fasthttp/fasthttp_darwin.v index 0b5010439..50ce20d3b 100644 --- a/vlib/fasthttp/fasthttp_darwin.v +++ b/vlib/fasthttp/fasthttp_darwin.v @@ -2,6 +2,7 @@ module fasthttp import net import sync.stdatomic +import time #include #include @@ -52,6 +53,7 @@ mut: write_buf []u8 write_pos int request_active bool + read_start i64 // monotonic timestamp (in microseconds) when first data was received // Sendfile state file_fd int = -1 @@ -64,6 +66,7 @@ pub mut: family net.AddrFamily = .ip6 port int max_request_buffer_size int = 8192 + timeout_in_seconds int = 30 socket_fd int = -1 poll_fd int = -1 // kqueue fd user_data voidptr @@ -80,6 +83,7 @@ pub fn new_server(config ServerConfig) !&Server { family: config.family port: config.port max_request_buffer_size: config.max_request_buffer_size + timeout_in_seconds: config.timeout_in_seconds user_data: config.user_data request_handler: config.handler running: stdatomic.new_atomic(false) @@ -175,10 +179,98 @@ fn send_pending(c_ptr voidptr) bool { return !(c.write_pos >= c.write_buf.len && c.file_fd == -1) } +const status_408_response = 'HTTP/1.1 408 Request Timeout\r\nContent-Type: text/plain\r\nContent-Length: 19\r\nConnection: close\r\n\r\n408 Request Timeout'.bytes() + fn send_bad_request(fd int) { C.send(fd, tiny_bad_request_response.data, tiny_bad_request_response.len, 0) } +fn send_request_timeout(fd int) { + C.send(fd, status_408_response.data, status_408_response.len, 0) +} + +// has_complete_body checks if a raw HTTP request buffer contains the full body +// as indicated by the Content-Length header. Returns true if: +// - there is no Content-Length header (body not expected) +// - Content-Length is 0 +// - enough body bytes have been received +// Returns false only when Content-Length indicates more body data is expected. +@[direct_array_access] +fn has_complete_body(buf &u8, buf_len int) bool { + // Find end of headers: \r\n\r\n or \n\n + mut header_end := -1 + for i := 0; i < buf_len - 1; i++ { + unsafe { + if buf[i] == `\n` { + if i + 1 < buf_len && buf[i + 1] == `\n` { + header_end = i + 2 + break + } + if i + 2 < buf_len && buf[i + 1] == `\r` && buf[i + 2] == `\n` { + header_end = i + 3 + break + } + } + if i + 3 < buf_len && buf[i] == `\r` && buf[i + 1] == `\n` + && buf[i + 2] == `\r` && buf[i + 3] == `\n` { + header_end = i + 4 + break + } + } + } + if header_end < 0 { + return false // headers not complete yet + } + // Search for Content-Length header (case-insensitive) + // Look for "\nContent-Length:" or "\ncontent-length:" at start of a line + mut content_length := -1 + for i := 0; i < header_end - 16; i++ { + unsafe { + if buf[i] != `\n` { + continue + } + // Start of a line — check for "Content-Length:" + mut pos := i + 1 + // Skip optional \r after \n (for \r\n line endings) + cl_lower := 'content-length:' + if pos + cl_lower.len > header_end { + continue + } + mut matched := true + for j := 0; j < cl_lower.len; j++ { + mut ch := buf[pos + j] + // to lowercase + if ch >= `A` && ch <= `Z` { + ch = ch + 32 + } + if ch != cl_lower[j] { + matched = false + break + } + } + if matched { + // Parse the number after "Content-Length: " + mut start := pos + cl_lower.len + for start < header_end && buf[start] == ` ` { + start++ + } + mut val := 0 + for start < header_end && buf[start] >= `0` && buf[start] <= `9` { + val = val * 10 + int(buf[start] - `0`) + start++ + } + content_length = val + break + } + } + } + if content_length <= 0 { + return true // no content-length or zero: body complete + } + body_received := buf_len - header_end + return body_received >= content_length +} + fn handle_write(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) { mut c := unsafe { &Conn(c_ptr) } if send_pending(c_ptr) { @@ -245,6 +337,7 @@ fn process_request(server Server, kq int, c_ptr voidptr, mut clients map[int]voi c.write_pos = 0 c.read_len = 0 + c.read_start = 0 if send_pending(c_ptr) { add_event(kq, u64(c.fd), i16(C.EVFILT_WRITE), u16(C.EV_ADD | C.EV_ENABLE | C.EV_CLEAR), c) @@ -278,6 +371,24 @@ fn handle_read(server Server, kq int, c_ptr voidptr, mut clients map[int]voidptr return } + // Record when we first started receiving data for this request + if c.read_start == 0 { + c.read_start = time.sys_mono_now() + } + + // Check if the full body has been received according to Content-Length + if !has_complete_body(&c.read_buf[0], c.read_len) { + // Body not complete yet - check for timeout + elapsed_us := time.sys_mono_now() - c.read_start + timeout_us := i64(server.timeout_in_seconds) * 1_000_000 + if elapsed_us >= timeout_us { + send_request_timeout(c.fd) + close_conn(server, kq, c_ptr, mut clients) + } + // Otherwise wait for more data on the next kqueue event + return + } + process_request(server, kq, c_ptr, mut clients) } @@ -432,6 +543,21 @@ pub fn (mut s Server) run() ! { handle_write(s, s.poll_fd, event.udata, mut clients) } } + // Sweep for connections waiting for body data that have timed out + if s.timeout_in_seconds > 0 { + now := time.sys_mono_now() + timeout_us := i64(s.timeout_in_seconds) * 1_000_000 + for client_fd in clients.keys() { + c_ptr := clients[client_fd] or { continue } + c := unsafe { &Conn(c_ptr) } + if c.read_start > 0 && c.read_len > 0 && !c.request_active { + if now - c.read_start >= timeout_us { + send_request_timeout(c.fd) + close_conn(s, s.poll_fd, c_ptr, mut clients) + } + } + } + } } if s.socket_fd >= 0 { diff --git a/vlib/v/checker/assign.v b/vlib/v/checker/assign.v index d4833007b..0a9420c16 100644 --- a/vlib/v/checker/assign.v +++ b/vlib/v/checker/assign.v @@ -1119,7 +1119,7 @@ or use an explicit `unsafe{ a[..] }`, if you do not want a copy of the slice.', // disallow invalid `t.$(field.name)` type assignment if !c.check_types(field_type, right_type) && !(c.inside_x_matches_type || field_sym.kind == .enum) { - c.error('cannot assign to `${left}`: ${c.expected_msg(right_type, + c.error('cannot assign to `${ast.Expr(left)}`: ${c.expected_msg(right_type, field_type)}', right.pos()) } } else { diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 363a67058..34aa63897 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -7763,7 +7763,7 @@ fn (mut c Checker) index_expr(mut node ast.IndexExpr) ast.Type { c.error('type `?${typ_sym.name}` is an Option, it must be unwrapped with `func()?`, or use `func() or {default}`', left_pos) } else if node.left is ast.SelectorExpr && node.left.or_block.kind == .absent { - c.error('type `?${typ_sym.name}` is an Option, it must be unwrapped first; use `${node.left}?` to do it', + c.error('type `?${typ_sym.name}` is an Option, it must be unwrapped first; use `${ast.Expr(node.left)}?` to do it', left_pos) } } else if typ.has_flag(.result) { diff --git a/vlib/v/checker/fn.v b/vlib/v/checker/fn.v index fe36d38c0..a709db98d 100644 --- a/vlib/v/checker/fn.v +++ b/vlib/v/checker/fn.v @@ -2349,7 +2349,7 @@ fn (mut c Checker) fn_call(mut node ast.CallExpr, mut continue_check &bool) ast. call_arg_expr_pos := call_arg.expr.pos() if !call_arg.expr.is_lvalue() { if call_arg.expr is ast.StructInit { - c.error('cannot pass a struct initialization as `mut`, you may want to use a variable `mut var := ${call_arg.expr}`', + c.error('cannot pass a struct initialization as `mut`, you may want to use a variable `mut var := ${ast.Expr(call_arg.expr)}`', call_arg_expr_pos) } else { c.error('cannot pass expression as `mut`', call_arg_expr_pos) @@ -4637,7 +4637,7 @@ fn (mut c Checker) array_builtin_method_call(mut node ast.CallExpr, left_type as } else if node.kind in [.sort, .sorted] { if node.kind == .sort { if node.left is ast.CallExpr { - c.error('the `sort()` method can be called only on mutable receivers, but `${node.left}` is a call expression', + c.error('the `sort()` method can be called only on mutable receivers, but `${ast.Expr(node.left)}` is a call expression', node.pos) } c.check_for_mut_receiver(mut node.left) @@ -5028,7 +5028,7 @@ fn (mut c Checker) fixed_array_builtin_method_call(mut node ast.CallExpr, left_t } else if node.kind in [.sort, .sorted] { if node.kind == .sort { if node.left is ast.CallExpr { - c.error('the `sort()` method can be called only on mutable receivers, but `${node.left}` is a call expression', + c.error('the `sort()` method can be called only on mutable receivers, but `${ast.Expr(node.left)}` is a call expression', node.pos) } c.check_for_mut_receiver(mut node.left) diff --git a/vlib/v/checker/match.v b/vlib/v/checker/match.v index fc6cd3fd4..6749e2b13 100644 --- a/vlib/v/checker/match.v +++ b/vlib/v/checker/match.v @@ -846,28 +846,28 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym if expr is ast.IntegerLiteral { if mut node.cond is ast.ComptimeType { if node.cond.kind != .int { - c.error('can not matching a int value(`${expr}`) in a non int type `\$match`, `${node.cond}` type is `${node.cond.kind}`', + c.error('can not matching a int value(`${ast.Expr(expr)}`) in a non int type `\$match`, `${node.cond}` type is `${node.cond.kind}`', expr_pos) return } } else if node.cond_type !in ast.integer_type_idxs { - c.error('can not matching a int value(`${expr}`) in a non int type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', + c.error('can not matching a int value(`${ast.Expr(expr)}`) in a non int type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', expr_pos) return } } else if expr is ast.BoolLiteral && node.cond_type != ast.bool_type { - c.error('can not matching a bool value(`${expr}`) in a non bool type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', + c.error('can not matching a bool value(`${ast.Expr(expr)}`) in a non bool type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', expr_pos) return } else if expr is ast.StringLiteral { if mut node.cond is ast.ComptimeType { if node.cond.kind != .string { - c.error('can not matching a string value(`${expr}`) in a non string type `\$match`, `${node.cond}` type is `${node.cond.kind}`', + c.error('can not matching a string value(`${ast.Expr(expr)}`) in a non string type `\$match`, `${node.cond}` type is `${node.cond.kind}`', expr_pos) return } } else if node.cond_type != ast.string_type { - c.error('can not matching a string value(`${expr}`) in a non string type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', + c.error('can not matching a string value(`${ast.Expr(expr)}`) in a non string type `\$match`, `${node.cond}` type is `${c.table.type_to_str(node.cond_type)}`', expr_pos) return } diff --git a/vlib/v/checker/tests/enum_field_value_duplicate_d.out b/vlib/v/checker/tests/enum_field_value_duplicate_d.out index b885cae85..b8b1071ce 100644 --- a/vlib/v/checker/tests/enum_field_value_duplicate_d.out +++ b/vlib/v/checker/tests/enum_field_value_duplicate_d.out @@ -1,11 +1,3 @@ -vlib/v/checker/tests/enum_field_value_duplicate_d.vv:4:6: error: enum value `1` already exists - 2 | - 3 | enum MyEnum { - 4 | a = one - | ~~~ - 5 | b - 6 | c = one -Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when it is needed vlib/v/checker/tests/enum_field_value_duplicate_d.vv:6:6: error: enum value `1` already exists 4 | a = one 5 | b diff --git a/vlib/v/checker/tests/enum_field_value_duplicate_e.out b/vlib/v/checker/tests/enum_field_value_duplicate_e.out index 601727e4f..4e9db5ec0 100644 --- a/vlib/v/checker/tests/enum_field_value_duplicate_e.out +++ b/vlib/v/checker/tests/enum_field_value_duplicate_e.out @@ -1,10 +1,3 @@ -vlib/v/checker/tests/enum_field_value_duplicate_e.vv:2:6: error: enum value `2` already exists - 1 | enum MyEnum { - 2 | a = one - | ~~~ - 3 | b - 4 | c = one -Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when it is needed vlib/v/checker/tests/enum_field_value_duplicate_e.vv:4:6: error: enum value `2` already exists 2 | a = one 3 | b diff --git a/vlib/v/checker/tests/enum_self_reference.out b/vlib/v/checker/tests/enum_self_reference.out index 77549b8da..d8f306007 100644 --- a/vlib/v/checker/tests/enum_self_reference.out +++ b/vlib/v/checker/tests/enum_self_reference.out @@ -1,11 +1,3 @@ -vlib/v/checker/tests/enum_self_reference.vv:5:6: error: enum value `10` already exists - 3 | - 4 | enum ExampleSigned as i8 { - 5 | a = a - | ^ - 6 | b = a - 7 | c = .a -Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when it is needed vlib/v/checker/tests/enum_self_reference.vv:6:6: error: enum value `10` already exists 4 | enum ExampleSigned as i8 { 5 | a = a @@ -29,15 +21,7 @@ vlib/v/checker/tests/enum_self_reference.vv:8:2: error: duplicate enum field nam | ^ 9 | d = .d 10 | } -vlib/v/checker/tests/enum_self_reference.vv:8:6: error: enum value `10` already exists - 6 | b = a - 7 | c = .a - 8 | c = .c - | ~~ - 9 | d = .d - 10 | } -Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when needed -vlib/v/checker/tests/enum_self_reference.vv:9:6: error: enum value `10` already exists +vlib/v/checker/tests/enum_self_reference.vv:9:6: error: enum value `d` is not allowed to reference itself 7 | c = .a 8 | c = .c 9 | d = .d @@ -45,14 +29,6 @@ vlib/v/checker/tests/enum_self_reference.vv:9:6: error: enum value `10` already 10 | } 11 | Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when needed -vlib/v/checker/tests/enum_self_reference.vv:13:6: error: enum value `10` already exists - 11 | - 12 | enum ExampleUnSigned as u8 { - 13 | a = a - | ^ - 14 | b = a - 15 | c = .a -Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when it is needed vlib/v/checker/tests/enum_self_reference.vv:14:6: error: enum value `10` already exists 12 | enum ExampleUnSigned as u8 { 13 | a = a @@ -76,15 +52,7 @@ vlib/v/checker/tests/enum_self_reference.vv:16:2: error: duplicate enum field na | ^ 17 | d = .d 18 | } -vlib/v/checker/tests/enum_self_reference.vv:16:6: error: enum value `10` already exists - 14 | b = a - 15 | c = .a - 16 | c = .c - | ~~ - 17 | d = .d - 18 | } -Details: use `@[_allow_multiple_values]` attribute to allow multiple enum values. Use only when needed -vlib/v/checker/tests/enum_self_reference.vv:17:6: error: enum value `10` already exists +vlib/v/checker/tests/enum_self_reference.vv:17:6: error: enum value `d` is not allowed to reference itself 15 | c = .a 16 | c = .c 17 | d = .d diff --git a/vlib/v/parser/expr.v b/vlib/v/parser/expr.v index f954e3ee5..01cd00cad 100644 --- a/vlib/v/parser/expr.v +++ b/vlib/v/parser/expr.v @@ -1156,7 +1156,7 @@ fn (mut p Parser) prefix_expr() ast.Expr { mut unwrapped_right := ast.Expr(ast.EmptyExpr{}) if mut right is ast.ParExpr { if right.expr is ast.StructInit { - p.note_with_pos('unnecessary `()`, use `&${right.expr}` instead of `&(${right.expr})`', + p.note_with_pos('unnecessary `()`, use `&${ast.Expr(right.expr)}` instead of `&(${ast.Expr(right.expr)})`', right.pos) unwrapped_right = right.expr } diff --git a/vlib/veb/veb_fasthttp.v b/vlib/veb/veb_fasthttp.v index 795c5401b..b6acddf24 100644 --- a/vlib/veb/veb_fasthttp.v +++ b/vlib/veb/veb_fasthttp.v @@ -50,6 +50,7 @@ pub fn run_new[A, X](mut global_app A, params RunParams) ! { port: params.port handler: parallel_request_handler[A, X] max_request_buffer_size: params.max_request_buffer_size + timeout_in_seconds: params.timeout_in_seconds user_data: voidptr(request_params) }) or { eprintln('Failed to create server: ${err}') -- 2.39.5