From 307a670c1fdd44c017d91cb91517658594cefbd0 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Sat, 18 Apr 2026 02:02:41 +0300 Subject: [PATCH] veb: add missing server_new_veb.v and veb_fasthttp.v files --- vlib/veb/server_new_veb.v | 44 ++++++++++ vlib/veb/veb_fasthttp.v | 163 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 vlib/veb/server_new_veb.v create mode 100644 vlib/veb/veb_fasthttp.v diff --git a/vlib/veb/server_new_veb.v b/vlib/veb/server_new_veb.v new file mode 100644 index 000000000..91929f154 --- /dev/null +++ b/vlib/veb/server_new_veb.v @@ -0,0 +1,44 @@ +module veb + +import fasthttp + +@[heap] +pub struct Server { + handle fasthttp.ServerHandle + lifecycle_control bool +} + +fn new_server_with_lifecycle(handle fasthttp.ServerHandle) &Server { + return &Server{ + handle: handle + lifecycle_control: true + } +} + +fn new_server_without_lifecycle() &Server { + return &Server{} +} + +fn (s &Server) ensure_lifecycle_control() ! { + if !s.lifecycle_control { + return error('veb server lifecycle control is not available with SSL') + } +} + +// wait_till_running waits until the server starts accepting requests. +pub fn (s &Server) wait_till_running(params WaitTillRunningParams) !int { + s.ensure_lifecycle_control()! + return s.handle.wait_till_running(fasthttp.WaitTillRunningParams{ + max_retries: params.max_retries + retry_period_ms: params.retry_period_ms + })! +} + +// shutdown gracefully stops accepting new requests and waits for in-flight requests to finish. +pub fn (s &Server) shutdown(params ShutdownParams) ! { + s.ensure_lifecycle_control()! + s.handle.shutdown(fasthttp.ShutdownParams{ + timeout: params.timeout + retry_period_ms: params.retry_period_ms + })! +} diff --git a/vlib/veb/veb_fasthttp.v b/vlib/veb/veb_fasthttp.v new file mode 100644 index 000000000..795c5401b --- /dev/null +++ b/vlib/veb/veb_fasthttp.v @@ -0,0 +1,163 @@ +// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +module veb + +import fasthttp +import net.http +import time +import net.urllib + +struct RequestParams { + global_app voidptr + controllers_sorted []&ControllerPath + routes &map[string]Route + benchmark_page_generation bool +} + +const http_ok_response = 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() + +pub fn run_at[A, X](mut global_app A, params RunParams) ! { + run_new[A, X](mut global_app, params)! +} + +// run_new - start a new veb server using the parallel fasthttp backend. +pub fn run_new[A, X](mut global_app A, params RunParams) ! { + if params.port <= 0 || params.port > 65535 { + return error('invalid port number `${params.port}`, it should be between 1 and 65535') + } + if ssl_enabled(params) { + maybe_init_server[A](mut global_app, new_server_without_lifecycle()) + run_at_with_ssl[A, X](mut global_app, params)! + return + } + + // Generate routes and controllers just like the original run() function. + routes := generate_routes[A, X](global_app)! + controllers_sorted := check_duplicate_routes_in_controllers[A](global_app, routes)! + + // Allocate params on the heap to keep it valid for the server lifetime + request_params := &RequestParams{ + global_app: unsafe { voidptr(&global_app) } + controllers_sorted: controllers_sorted + routes: &routes + benchmark_page_generation: params.benchmark_page_generation + } + + // Configure and run the fasthttp server + mut server := fasthttp.new_server(fasthttp.ServerConfig{ + family: params.family + port: params.port + handler: parallel_request_handler[A, X] + max_request_buffer_size: params.max_request_buffer_size + user_data: voidptr(request_params) + }) or { + eprintln('Failed to create server: ${err}') + return + } + maybe_init_server[A](mut global_app, new_server_with_lifecycle(server.handle())) + println('[veb] Running multi-threaded app on ${server_protocol(params)}://${startup_host(params)}:${params.port}/') + flush_stdout() + $if A is BeforeAcceptApp { + global_app.before_accept_loop() + } + server.run() or { panic(err) } +} + +fn parallel_request_handler[A, X](req fasthttp.HttpRequest) !fasthttp.HttpResponse { + // Get parameters from user_data - copy to avoid use-after-free + params := unsafe { *(&RequestParams(req.user_data)) } + mut global_app := unsafe { &A(params.global_app) } + + client_fd := req.client_conn_fd + + s := req.buffer.bytestr() + // Parse the raw request bytes into a standard `http.Request`. + req2 := http.parse_request_str(s.clone()) or { + return fasthttp.HttpResponse{ + content: 'HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes() + } + } + // Create and populate the `veb.Context`. + completed_context := handle_request_and_route[A, X](mut global_app, req2, client_fd, params) + + if completed_context.takeover { + // The handler has taken over the connection (e.g. for SSE or WebSocket). + // The response was already sent directly over ctx.conn. + // Tell fasthttp to hand off the fd without closing it. + return fasthttp.HttpResponse{ + takeover: true + } + } + + if completed_context.return_type == .file { + return fasthttp.HttpResponse{ + content: completed_context.res.bytes() + file_path: completed_context.return_file + should_close: completed_context.client_wants_to_close + } + } + + // The fasthttp server expects a complete response buffer to be returned. + return fasthttp.HttpResponse{ + content: completed_context.res.bytes() + should_close: completed_context.client_wants_to_close + } +} // handle_request_and_route is a unified function that creates the context, + +// runs middleware, and finds the correct route for a request. +fn handle_request_and_route[A, X](mut app A, req http.Request, _client_fd int, params RequestParams) &Context { + // Create and populate the `veb.Context` from the request. + mut url := urllib.parse_request_uri(req.url) or { + // This should be rare if http.parse_request succeeded. + mut bad_ctx := &Context{ + req: req + } + bad_ctx.not_found() + return bad_ctx + } + query := parse_query_from_url(url) + form, files := parse_form_from_request(req) or { + mut bad_ctx := &Context{ + req: req + } + bad_ctx.request_error('Failed to parse form data: ${err.msg()}') + return bad_ctx + } + host_with_port := req.header.get(.host) or { '' } + host, _ := urllib.split_host_port(host_with_port) + page_gen_start := if params.benchmark_page_generation { time.ticks() } else { 0 } + mut ctx := &Context{ + req: req + page_gen_start: page_gen_start + client_fd: _client_fd + client_wants_to_close: true // fasthttp always closes connections after response + query: query + form: form + files: files + } + $if A is StaticApp { + ctx.custom_mime_types = app.static_mime_types.clone() + mut user_context := X{} + user_context.Context = ctx + if serve_if_static[X](static_handler_config(app.static_files, app.static_mime_types, + app.static_hosts, app.enable_static_gzip, app.enable_static_zstd, + app.enable_static_compression, app.static_compression_max_size, + app.static_compression_mime_types, app.enable_markdown_negotiation), mut user_context, + url, host) + { + return &user_context.Context + } + } + // Match controller paths first + $if A is ControllerInterface { + if completed_context := handle_controllers[X](params.controllers_sorted, ctx, mut url, host) { + return completed_context + } + } + // Create a new user context and pass veb's context + mut user_context := X{} + user_context.Context = ctx + handle_route[A, X](mut app, mut user_context, url, host, params.routes) + return &user_context.Context +} -- 2.39.5