From 0a4e5d50f3cbdfbd51fb4ad568528918ee903459 Mon Sep 17 00:00:00 2001 From: meiseayoung Date: Tue, 6 Jan 2026 15:58:24 +0800 Subject: [PATCH] fix: HTTP/1.0 connection not closing causes ab benchmark to hang - Add HTTP version detection in parse_http_request_usockets - Return Connection: close header for HTTP/1.0 requests - Call shutdown() for graceful connection close after HTTP/1.0 response - Fix missing is_http11 parameter in WebSocket upgrade handler - Support HTTP/1.0 with Connection: Keep-Alive header (ab -k mode) - Optimize: only check Connection header value for keep-alive (like Go net/http) Root cause: ab uses HTTP/1.0 by default, but server always returned Connection: keep-alive, causing ab to wait for server to close connection. --- usockets_server.v | 143 ++++++++++++++++++++++++++++++++++++++++------ v.mod | 2 +- 2 files changed, 126 insertions(+), 19 deletions(-) diff --git a/usockets_server.v b/usockets_server.v index 9996f7f..64d61d9 100644 --- a/usockets_server.v +++ b/usockets_server.v @@ -183,7 +183,7 @@ fn handle_usockets_ws_upgrade(s usockets.Socket, raw_data string, route_match Co // Check if this is a WebSocket upgrade response if response.status_code != 101 { // Not a WebSocket upgrade, send the response as-is - send_usockets_response(s, mutable_ctx, response) + send_usockets_response(s, mutable_ctx, response, true) return false } @@ -227,7 +227,7 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket { // 解析 HTTP 请求 raw_data := unsafe { tos(&u8(data), length) } - method, path, query_map, body := parse_http_request_usockets(raw_data) + method, path, query_map, body, is_http11 := parse_http_request_usockets(raw_data) if method.len == 0 { s.write_bytes('HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\n\r\nBad Request') @@ -259,11 +259,11 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket { // 优化1:零中间件快速路径 if !g_usockets_app.has_middlewares { response := route_match.handler.handle(mut hono_ctx) - send_usockets_response(s, hono_ctx, response) + send_usockets_response(s, hono_ctx, response, is_http11) } else { middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path) response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler) - send_usockets_response(s, hono_ctx, response) + send_usockets_response(s, hono_ctx, response, is_http11) } response_sent = true } @@ -289,11 +289,11 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket { // 优化1:零中间件快速路径 if !g_usockets_app.has_middlewares { response := route_match.handler.handle(mut hono_ctx) - send_usockets_response(s, hono_ctx, response) + send_usockets_response(s, hono_ctx, response, is_http11) } else { middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path) response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler) - send_usockets_response(s, hono_ctx, response) + send_usockets_response(s, hono_ctx, response, is_http11) } response_sent = true } @@ -305,9 +305,13 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket { if handler := g_usockets_app.not_found_handler { response := handler(mut hono_ctx) - send_usockets_response(s, hono_ctx, response) + send_usockets_response(s, hono_ctx, response, is_http11) } else { - s.write_bytes('HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: keep-alive\r\n\r\nNot Found') + conn_header := if is_http11 { 'keep-alive' } else { 'close' } + s.write_bytes('HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: ${conn_header}\r\n\r\nNot Found') + if !is_http11 { + s.shutdown() + } } } @@ -334,15 +338,17 @@ fn usockets_on_end(s usockets.Socket) usockets.Socket { // 解析 HTTP 请求 - 零分配优化版 // 使用指针遍历避免创建临时数组 -fn parse_http_request_usockets(raw string) (string, string, map[string]string, string) { +// 返回: (method, path, query_map, body, is_http11) +fn parse_http_request_usockets(raw string) (string, string, map[string]string, string, bool) { mut method := '' mut path := '' mut query_map := map[string]string{} mut body := '' + mut is_http11 := true len := raw.len if len == 0 { - return method, path, query_map, body + return method, path, query_map, body, is_http11 } // 1. 查找第一行结束位置(\r\n) @@ -354,7 +360,7 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s } } if line_end == -1 { - return method, path, query_map, body + return method, path, query_map, body, is_http11 } // 2. 解析 method(到第一个空格) @@ -363,7 +369,7 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s method_end++ } if method_end == 0 || method_end >= line_end { - return method, path, query_map, body + return method, path, query_map, body, is_http11 } method = raw[..method_end] @@ -391,7 +397,25 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s path = raw[path_start..path_end] } - // 4. 查找 body(\r\n\r\n 之后) + // 4. 检测 HTTP 版本 - "HTTP/1.0" vs "HTTP/1.1" + // 请求行格式: "GET /path HTTP/1.1\r\n" + // HTTP/1.x 版本字符串固定为 8 字符,直接检查第 8 个字符 + mut version_start := path_end + 1 + for version_start < line_end && raw[version_start] == ` ` { + version_start++ + } + // "HTTP/1.0" 或 "HTTP/1.1",检查索引 7 的字符(0-based) + if version_start + 7 < line_end { + is_http11 = raw[version_start + 7] != `0` + } + + // 对于 HTTP/1.0,检查 Connection 头是否包含 keep-alive + // 参考 Go net/http: wantsHttp10KeepAlive() 只检查 Connection 头的值 + if !is_http11 { + is_http11 = has_connection_keep_alive(raw, line_end, len) + } + + // 5. 查找 body(\r\n\r\n 之后) for i in line_end .. len - 3 { if raw[i] == `\r` && raw[i + 1] == `\n` && raw[i + 2] == `\r` && raw[i + 3] == `\n` { if i + 4 < len { @@ -401,10 +425,82 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s } } - return method, path, query_map, body + return method, path, query_map, body, is_http11 } -// 快速解析 query string(单次遍历,避免 split) +// 检查 Connection 头是否包含 keep-alive(参考 Go net/http 实现) +// 只检查 "Connection:" 头的值,避免误判请求体中的内容 +@[inline] +fn has_connection_keep_alive(raw string, headers_start int, len int) bool { + mut i := headers_start + + // 遍历每一行头部 + for i < len - 12 { // 至少需要 "Connection:" + 一些值 + // 跳过 \r\n + if raw[i] == `\r` && i + 1 < len && raw[i + 1] == `\n` { + i += 2 + // 检查是否到达头部结束(空行) + if i < len - 1 && raw[i] == `\r` && raw[i + 1] == `\n` { + break + } + } + + // 检查是否是 "Connection:" 头(不区分大小写) + if (raw[i] == `C` || raw[i] == `c`) && i + 11 < len { + // 检查 "onnection:" + if (raw[i+1] == `o` || raw[i+1] == `O`) && + (raw[i+2] == `n` || raw[i+2] == `N`) && + (raw[i+3] == `n` || raw[i+3] == `N`) && + (raw[i+4] == `e` || raw[i+4] == `E`) && + (raw[i+5] == `c` || raw[i+5] == `C`) && + (raw[i+6] == `t` || raw[i+6] == `T`) && + (raw[i+7] == `i` || raw[i+7] == `I`) && + (raw[i+8] == `o` || raw[i+8] == `O`) && + (raw[i+9] == `n` || raw[i+9] == `N`) && + raw[i+10] == `:` { + // 找到 Connection 头,检查值是否包含 keep-alive + mut value_start := i + 11 + // 跳过空格 + for value_start < len && raw[value_start] == ` ` { + value_start++ + } + // 查找行尾 + mut value_end := value_start + for value_end < len && raw[value_end] != `\r` && raw[value_end] != `\n` { + value_end++ + } + // 在值中查找 "keep-alive"(不区分大小写) + return contains_keep_alive_token(raw, value_start, value_end) + } + } + i++ + } + return false +} + +// 检查字符串片段是否包含 keep-alive token +@[inline] +fn contains_keep_alive_token(raw string, start int, end int) bool { + // 查找 "keep-alive" 或 "Keep-Alive"(支持多值如 "keep-alive, upgrade") + mut i := start + for i <= end - 10 { + if raw[i] == `k` || raw[i] == `K` { + if (raw[i+1] == `e` || raw[i+1] == `E`) && + (raw[i+2] == `e` || raw[i+2] == `E`) && + (raw[i+3] == `p` || raw[i+3] == `P`) && + raw[i+4] == `-` && + (raw[i+5] == `a` || raw[i+5] == `A`) && + (raw[i+6] == `l` || raw[i+6] == `L`) && + (raw[i+7] == `i` || raw[i+7] == `I`) && + (raw[i+8] == `v` || raw[i+8] == `V`) && + (raw[i+9] == `e` || raw[i+9] == `E`) { + return true + } + } + i++ + } + return false +}// 快速解析 query string(单次遍历,避免 split) @[inline] fn parse_query_string_fast(raw string, start int, end int) map[string]string { mut query_map := map[string]string{} @@ -576,7 +672,7 @@ fn exec_middlewares_usockets(idx int, middlewares []ContextMiddleware, mut ctx C } // 发送 uSockets 响应 - 优化版 -fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response) { +fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response, is_http11 bool) { // Check if this is a streaming response if is_streaming_response(ctx) { // Handle streaming response @@ -630,14 +726,25 @@ fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response resp.write_string(response.body.len.str()) resp.write_string('\r\n') - // Connection - resp.write_string('Connection: keep-alive\r\n') + // Connection - HTTP/1.0 用 close,HTTP/1.1 用 keep-alive + if is_http11 { + resp.write_string('Connection: keep-alive\r\n') + } else { + resp.write_string('Connection: close\r\n') + } // 空行 + body resp.write_string('\r\n') resp.write_string(response.body) s.write_bytes(resp.str()) + + // HTTP/1.0 请求完成后关闭连接 + // 只调用 shutdown() 发送 FIN,让 on_end 回调处理 close() + // 这样可以确保发送缓冲区的数据完全发送 + if !is_http11 { + s.shutdown() + } } // ============================================================================ diff --git a/v.mod b/v.mod index 5901005..f7edbc7 100644 --- a/v.mod +++ b/v.mod @@ -1,7 +1,7 @@ Module { name: 'hono' description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.' - version: '0.7.6' + version: '0.7.7' license: 'MIT' dependencies: [] }