Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+206
View File
@@ -0,0 +1,206 @@
# fasthttp
The `fasthttp` module is a high-performance HTTP server library for V that provides low-level socket management and non-blocking I/O.
## Features
- **High Performance**: Uses platform-specific I/O multiplexing:
- `epoll` on Linux for efficient connection handling
- `kqueue` on macOS and BSD for high-performance event notification
- **Non-blocking I/O**: Handles multiple concurrent connections efficiently
- **Simple API**: Easy-to-use request handler pattern
- **Cross-platform**: Supports Linux, macOS, FreeBSD, OpenBSD, NetBSD and DragonFly
## Installation
The module is part of the standard V library. Import it in your V code:
```v
import fasthttp
```
## Quick Start
Here's a minimal HTTP server example:
```v oksyntax
import fasthttp
fn handle_request(req fasthttp.HttpRequest) ![]u8 {
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
mut body := ''
mut status_line := ''
if path == '/' {
body = 'Hello, World!\n'
status_line = 'HTTP/1.1 200 OK'
} else {
body = '${path} not found\n'
status_line = 'HTTP/1.1 404 Not Found'
}
headers := [
status_line,
'Content-Type: text/plain',
'Content-Length: ${body.len}',
'Connection: close',
]
header_string := headers.join('\r\n')
return '${header_string}\r\n\r\n${body}'.bytes()
}
fn main() {
mut server := fasthttp.new_server(fasthttp.ServerConfig{
port: 3000
handler: handle_request
}) or {
eprintln('Failed to create server: ${err}')
return
}
println('Server listening on http://localhost:3000')
server.run() or { eprintln('error: ${err}') }
}
```
## API Reference
### `HttpRequest` Struct
Represents an incoming HTTP request.
**Fields:**
- `buffer: []u8` - The raw request buffer containing the complete HTTP request
- `method: Slice` - The HTTP method (GET, POST, etc.)
- `path: Slice` - The request path
- `version: Slice` - The HTTP version (e.g., "HTTP/1.1")
- `client_conn_fd: int` - Internal socket file descriptor
### `Slice` Struct
Represents a slice of the request buffer.
**Fields:**
- `start: int` - Starting index in the buffer
- `len: int` - Length of the slice
**Usage:**
```v ignore
method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr()
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
```
## Request Handler Pattern
The handler function receives an `HttpRequest` and must return either:
- `[]u8` - A byte array containing the HTTP response body
- An error if processing failed
The handler should extract method and path information from the request and route accordingly.
**Example:**
```v ignore
fn my_handler(req fasthttp.HttpRequest) ![]u8 {
method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr()
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
match method {
'GET' {
if path == '/' {
return 'Home page'.bytes()
}
}
'POST' {
if path == '/api/data' {
return 'Data received'.bytes()
}
}
else {}
}
return '404 Not Found'.bytes()
}
```
## Response Format
Responses should be returned as byte arrays.
The server will send them directly to the client as HTTP response bodies.
```v ignore
// Simple text response
return 'Hello, World!'.bytes()
// HTML response
return '<html><body>Hello</body></html>'.bytes()
// JSON response
return '{"message": "success"}'.bytes()
```
## Example
See the complete example in `examples/fasthttp/` for a more
detailed server implementation with multiple routes and controllers.
```sh
./v examples/fasthttp
./examples/fasthttp/fasthttp
```
## Platform Support
- **Linux**: Uses `epoll` for high-performance I/O multiplexing
- **macOS**: Uses `kqueue` for event notification
- **Windows**: Currently not supported
## Performance Considerations
- The `fasthttp` module is designed for high throughput and low latency
- Handler functions should be efficient; blocking operations will affect other connections
- Use goroutines within handlers if you need to perform long-running operations without
blocking the I/O loop
## Request-scoped allocation with `-prealloc`
When an application is compiled with `-prealloc`, `fasthttp` starts a scoped
prealloc arena for each request before decoding the HTTP request and before
calling the request handler. All V allocations made by the request parser, the
handler, and code called by the handler use that request arena while the handler
is running.
The arena is freed as a unit after the response no longer needs request-owned
data. On Linux the normal response path sends the response synchronously, then
ends the request arena. On macOS and BSD the response buffer can be kept by the
connection until `kqueue` finishes writing it; in that case `fasthttp` detaches
the scope from the request thread and frees it after the write completes.
This means request-local V allocations are cheap bump-pointer allocations, and
freeing them does not require walking individual objects. Startup state, server
state, and allocations made directly by C libraries are not part of a request
arena. If a handler starts V `spawn` work while the request scope is active, the
generated thread wrapper retains that scope until the spawned function returns;
void spawned functions also run inside their own scoped arena, which is freed at
thread exit. Manual takeover responses transfer ownership to user code and
currently abandon the request arena, so long-lived takeover handlers should
manage their own allocation lifetime explicitly.
To inspect request arena usage while developing, build with:
```sh
v -prealloc -d trace_prealloc run .
```
## Notes
- HTTP headers are currently not parsed; the entire request is available in the buffer
- Only the request method, path, and version are parsed automatically
- Response status codes and headers must be manually constructed if needed
- The module provides low-level access for maximum control and performance
+290
View File
@@ -0,0 +1,290 @@
// Copyright (c) 2019-2025 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 fasthttp
import runtime
import net
import time
#include <errno.h>
$if !windows {
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
}
const max_thread_pool_size = runtime.nr_cpus()
const max_connection_size = 65536 // Max events per epoll_wait
const tiny_bad_request_response = 'HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
const status_444_response = 'HTTP/1.1 444 No Response\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
const status_413_response = 'HTTP/1.1 413 Payload Too Large\r\nContent-Length: 0\r\nConnection: close\r\n\r\n'.bytes()
fn C.socket(domain i32, typ i32, protocol i32) i32
fn C.bind(sockfd i32, addr &net.Addr, addrlen u32) i32
fn C.send(__fd i32, __buf voidptr, __n usize, __flags i32) i32
fn C.recv(__fd i32, __buf voidptr, __n usize, __flags i32) i32
fn C.setsockopt(__fd i32, __level i32, __optname i32, __optval voidptr, __optlen u32) i32
fn C.listen(__fd i32, __n i32) i32
fn C.perror(s &u8)
fn C.close(fd i32) i32
fn C.htons(__hostshort u16) u16
fn C.fcntl(fd i32, cmd i32, arg i32) i32
pub struct Slice {
pub:
start int
len int
}
// HttpRequest represents an HTTP request.
// TODO make fields immutable
pub struct HttpRequest {
pub mut:
buffer []u8 // A V slice of the read buffer for convenience
method Slice
path Slice
version Slice
header_fields Slice
body Slice
client_conn_fd int
user_data voidptr // User-defined context data
}
pub enum ResponseTakeoverMode {
none
manual
reusable
}
pub struct HttpResponse {
pub mut:
content []u8
file_path string
takeover_mode ResponseTakeoverMode
should_close bool // if true, close the connection after sending (Connection: close)
// content_owned lets the backend free or move content after it has been sent.
content_owned bool
// request_arena is a prealloc scope handle that must be freed after sending.
request_arena voidptr
}
fn (mut resp HttpResponse) free_owned_content() {
if resp.content_owned && resp.content.cap > 0 {
unsafe { resp.content.free() }
resp.content = []u8{}
}
}
fn (mut resp HttpResponse) take_or_clone_content() []u8 {
if resp.content_owned {
content := resp.content
resp.content = []u8{}
return content
}
return resp.content.clone()
}
fn end_request_arena_current_thread(request_arena voidptr) {
$if prealloc {
if request_arena != unsafe { nil } {
unsafe { prealloc_scope_end(request_arena) }
}
}
}
fn leave_request_arena_current_thread(request_arena voidptr) {
$if prealloc {
if request_arena != unsafe { nil } {
unsafe { prealloc_scope_leave(request_arena) }
}
}
}
fn abandon_request_arena_current_thread(request_arena voidptr) {
$if prealloc {
if request_arena != unsafe { nil } {
unsafe { prealloc_scope_abandon(request_arena) }
}
}
}
fn (mut resp HttpResponse) attach_request_arena_if_empty(request_arena voidptr) {
if resp.request_arena == unsafe { nil } {
resp.request_arena = request_arena
}
}
fn (mut resp HttpResponse) end_request_arena_current_thread() {
end_request_arena_current_thread(resp.request_arena)
resp.request_arena = unsafe { nil }
}
fn (mut resp HttpResponse) abandon_request_arena_current_thread() {
abandon_request_arena_current_thread(resp.request_arena)
resp.request_arena = unsafe { nil }
}
fn (mut resp HttpResponse) take_request_arena() voidptr {
request_arena := resp.request_arena
resp.request_arena = unsafe { nil }
return request_arena
}
// ServerConfig bundles the parameters needed to start a fasthttp server.
pub struct ServerConfig {
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
}
// ShutdownParams configures how long graceful shutdown should wait for in-flight requests.
@[params]
pub struct ShutdownParams {
pub:
timeout time.Duration = time.infinite
retry_period_ms int = 10
}
// WaitTillRunningParams allows parametrizing the calls to `ServerHandle.wait_till_running()`.
@[params]
pub struct WaitTillRunningParams {
pub:
max_retries int = 100
retry_period_ms int = 10
}
// ServerHandle exposes lifecycle controls for a running `fasthttp.Server`.
pub struct ServerHandle {
ptr voidptr
}
// handle returns a reusable handle for waiting on or shutting down the server.
pub fn (s &Server) handle() ServerHandle {
return ServerHandle{
ptr: s
}
}
// wait_till_running waits until the server transitions to its serving state.
pub fn (h ServerHandle) wait_till_running(params WaitTillRunningParams) !int {
if h.ptr == unsafe { nil } {
return error('server handle is not initialized')
}
$if linux || bsd {
mut server := unsafe { &Server(h.ptr) }
return server.wait_till_running_impl(params)!
} $else {
return error('fasthttp server lifecycle control is only supported on linux and BSD-family OSes')
}
}
// shutdown gracefully stops accepting new requests and waits for active requests to finish.
pub fn (h ServerHandle) shutdown(params ShutdownParams) ! {
if h.ptr == unsafe { nil } {
return error('server handle is not initialized')
}
$if linux || bsd {
mut server := unsafe { &Server(h.ptr) }
server.shutdown_impl(params)!
} $else {
return error('fasthttp server lifecycle control is only supported on linux and BSD-family OSes')
}
}
$if linux || bsd {
fn normalized_retry_period_ms(retry_period_ms int) int {
return if retry_period_ms > 0 { retry_period_ms } else { 1 }
}
fn (mut s Server) wait_till_running_impl(params WaitTillRunningParams) !int {
retry_period_ms := normalized_retry_period_ms(params.retry_period_ms)
mut attempts := 0
mut running := s.running
for !running.load() && attempts < params.max_retries {
time.sleep(retry_period_ms * time.millisecond)
attempts++
}
if !running.load() {
return error('maximum retries reached')
}
time.sleep(retry_period_ms * time.millisecond)
return attempts
}
fn (mut s Server) shutdown_impl(params ShutdownParams) ! {
mut stopped := s.stopped
if stopped.load() {
return
}
mut shutting_down := s.shutting_down
if shutting_down.compare_and_swap(false, true) {
s.stop_accepting()
}
retry_period_ms := normalized_retry_period_ms(params.retry_period_ms)
mut watch := time.new_stopwatch()
for !stopped.load() {
if params.timeout != time.infinite && watch.elapsed() >= params.timeout {
return error('graceful shutdown timed out after ${params.timeout}')
}
time.sleep(retry_period_ms * time.millisecond)
}
}
fn (s &Server) begin_request() {
mut active_requests := s.active_requests
active_requests.add(1)
}
fn (s &Server) end_request() {
mut active_requests := s.active_requests
active_requests.sub(1)
}
fn (s &Server) active_request_count() int {
mut active_requests := s.active_requests
return active_requests.load()
}
fn (s &Server) is_shutting_down() bool {
mut shutting_down := s.shutting_down
return shutting_down.load()
}
fn (s &Server) is_stopped() bool {
mut stopped := s.stopped
return stopped.load()
}
fn (mut s Server) mark_running() {
mut running := s.running
running.store(true)
mut stopped := s.stopped
stopped.store(false)
}
fn (mut s Server) mark_stopped() {
mut active_requests := s.active_requests
active_requests.store(0)
mut running := s.running
running.store(false)
mut stopped := s.stopped
stopped.store(true)
}
}
+730
View File
@@ -0,0 +1,730 @@
module fasthttp
import net
import sync.stdatomic
import time
#include <sys/event.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <signal.h>
fn C.signal(sig int, handler voidptr) voidptr
const buf_size = max_connection_size
const kqueue_max_events = 128
const backlog = max_connection_size
const kqueue_wait_timeout_ms = 100
// send_flags is OR'd into every C.send() call in this file. On OpenBSD,
// which lacks the per-socket SO_NOSIGPIPE option, we pass MSG_NOSIGNAL
// instead so writes to a disconnected peer return EPIPE rather than
// killing the process with SIGPIPE. Other BSDs use SO_NOSIGPIPE set on
// the socket at accept time (see accept_clients).
const send_flags = $if openbsd { int(C.MSG_NOSIGNAL) } $else { 0 }
fn C.kevent(kq i32, changelist &C.kevent, nchanges i32, eventlist &C.kevent, nevents i32, timeout &C.timespec) i32
fn C.kqueue() i32
fn C.fstat(fd i32, buf &C.stat) i32
// send_file_bytes has three implementations across BSD-family OSes:
// macOS: int sendfile(int fd, int s, off_t offset, off_t *len, sf_hdtr *hdtr, int flags);
// (len is in/out: caller sets bytes-to-send, kernel writes bytes-actually-sent)
// FreeBSD/NetBSD/DragonFly:
// int sendfile(int fd, int s, off_t offset, size_t nbytes, sf_hdtr *hdtr, off_t *sbytes, int flags);
// (nbytes is input, sbytes is the separate out-param for bytes actually sent)
// OpenBSD: no sendfile(2) syscall at all. We fall back to a single
// pread(2) + send(2) pair per call, using a bounded stack
// buffer. The outer send_pending() loop will call us again
// until the file is drained or the socket blocks.
const sendfile_fallback_buf_size = 16384
// send_file_bytes asks the kernel to send up to `nbytes` bytes from `file_fd` at
// `offset` into socket `sock_fd`, in a single non-blocking operation.
// Returns (ret, sent) where `ret` is 0 on success or -1 on error (errno set),
// and `sent` is the number of bytes transferred this call (may be >0 even when ret==-1).
fn send_file_bytes(file_fd i32, sock_fd i32, offset i64, nbytes i64) (int, i64) {
$if macos {
mut len := nbytes
ret := C.sendfile(file_fd, sock_fd, offset, &len, unsafe { nil }, 0)
return int(ret), len
} $else $if openbsd {
// No sendfile(2) on OpenBSD; pread into a stack buffer, then send.
// Cap one call at sendfile_fallback_buf_size so we don't starve
// other connections in the kqueue loop.
mut buf := [sendfile_fallback_buf_size]u8{}
mut want := nbytes
if want > sendfile_fallback_buf_size {
want = sendfile_fallback_buf_size
}
nread := C.pread(file_fd, &buf[0], usize(want), offset)
if nread <= 0 {
// nread == 0 is EOF (shouldn't happen given write_pos < file_len
// guards in send_pending, but treat it as an error to close the
// connection); nread < 0 propagates errno for EAGAIN handling.
return -1, i64(0)
}
nsent := C.send(sock_fd, &buf[0], usize(nread), send_flags)
if nsent < 0 {
return -1, i64(0)
}
return 0, i64(nsent)
} $else {
mut sbytes := i64(0)
ret := C.sendfile(file_fd, sock_fd, offset, usize(nbytes), unsafe { nil }, &sbytes, 0)
return int(ret), sbytes
}
}
$if macos {
// int sendfile(int fd, int s, off_t offset, off_t *len, struct sf_hdtr *hdtr, int flags);
fn C.sendfile(fd i32, s i32, offset i64, len &i64, hdtr voidptr, flags i32) i32
} $else $if openbsd {
// ssize_t pread(int fd, void *buf, size_t nbyte, off_t offset);
fn C.pread(fd i32, buf voidptr, nbyte usize, offset i64) isize
} $else {
// int sendfile(int fd, int s, off_t offset, size_t nbytes, struct sf_hdtr *hdtr, off_t *sbytes, int flags);
fn C.sendfile(fd i32, s i32, offset i64, nbytes usize, hdtr voidptr, sbytes &i64, flags i32) i32
}
struct C.kevent {
ident u64
filter i16
flags u16
fflags u32
data isize
udata voidptr
}
// Helper to set fields of a kevent struct.
fn ev_set(mut ev C.kevent, ident u64, filter i16, flags u16, fflags u32, data isize, udata voidptr) {
ev.ident = ident
ev.filter = filter
ev.flags = flags
ev.fflags = fflags
ev.data = data
ev.udata = udata
}
struct Conn {
fd int
user_data voidptr
mut:
read_buf [buf_size]u8
read_len int
read_extra []u8 // dynamic overflow buffer for large requests (e.g. chunked uploads)
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
file_len i64
file_pos i64
should_close bool
request_arena voidptr
}
fn (mut c Conn) free_write_buf() {
if c.write_buf.cap > 0 {
unsafe { c.write_buf.free() }
c.write_buf = []u8{}
}
}
fn (mut c Conn) free_request_arena() {
$if prealloc {
if c.request_arena != unsafe { nil } {
unsafe { prealloc_scope_free_after(c.request_arena) }
c.request_arena = unsafe { nil }
}
}
}
pub struct Server {
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
request_handler fn (HttpRequest) !HttpResponse @[required]
running &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(false)
shutting_down &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(false)
stopped &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(true)
active_requests &stdatomic.AtomicVal[int] = stdatomic.new_atomic(0)
}
// new_server creates and initializes a new Server instance.
pub fn new_server(config ServerConfig) !&Server {
mut server := &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)
shutting_down: stdatomic.new_atomic(false)
stopped: stdatomic.new_atomic(true)
active_requests: stdatomic.new_atomic(0)
}
return server
}
fn set_nonblocking(fd int) {
flags := C.fcntl(fd, C.F_GETFL, 0)
if flags == -1 {
return
}
C.fcntl(fd, C.F_SETFL, flags | C.O_NONBLOCK)
}
fn add_event(kq int, ident u64, filter i16, flags u16, udata voidptr) int {
mut ev := C.kevent{}
ev_set(mut &ev, ident, filter, flags, u32(0), isize(0), udata)
return C.kevent(kq, &ev, 1, unsafe { nil }, 0, unsafe { nil })
}
fn delete_event(kq int, ident u64, filter i16, udata voidptr) {
mut ev := C.kevent{}
ev_set(mut &ev, ident, filter, u16(C.EV_DELETE), u32(0), isize(0), udata)
C.kevent(kq, &ev, 1, unsafe { nil }, 0, unsafe { nil })
}
fn close_conn(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) {
mut c := unsafe { &Conn(c_ptr) }
clients.delete(c.fd)
delete_event(kq, u64(c.fd), i16(C.EVFILT_READ), c)
delete_event(kq, u64(c.fd), i16(C.EVFILT_WRITE), c)
C.close(c.fd)
if c.request_active {
server.end_request()
c.request_active = false
}
c.free_write_buf()
c.free_request_arena()
if c.read_extra.cap > 0 {
unsafe { c.read_extra.free() }
}
if c.file_fd != -1 {
C.close(c.file_fd)
c.file_fd = -1
}
unsafe { free(c_ptr) }
}
fn send_pending(c_ptr voidptr) bool {
mut c := unsafe { &Conn(c_ptr) }
// 1. Send memory buffer (headers or small response)
if c.write_pos < c.write_buf.len {
remaining := c.write_buf.len - c.write_pos
write_ptr := unsafe { &c.write_buf[0] + c.write_pos }
sent := C.send(c.fd, write_ptr, remaining, send_flags)
if sent > 0 {
c.write_pos += int(sent)
}
if sent < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
return true
}
c.should_close = true
return false
}
}
// 2. Send file if buffer is fully sent
if c.write_pos >= c.write_buf.len && c.file_fd != -1 {
remaining := c.file_len - c.file_pos
ret, sent := send_file_bytes(c.file_fd, c.fd, c.file_pos, remaining)
if sent > 0 {
c.file_pos += sent
}
if ret == -1 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
return true
}
C.close(c.file_fd)
c.file_fd = -1
c.should_close = true
return false
}
if c.file_pos >= c.file_len {
C.close(c.file_fd)
c.file_fd = -1
} else {
return true
}
}
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, send_flags)
}
fn send_request_timeout(fd int) {
C.send(fd, status_408_response.data, status_408_response.len, send_flags)
}
fn handle_write(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) {
if send_pending(c_ptr) {
return
}
complete_response(server, kq, c_ptr, mut clients, true)
}
fn complete_response(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr, remove_write_event bool) {
mut c := unsafe { &Conn(c_ptr) }
if remove_write_event {
delete_event(kq, u64(c.fd), i16(C.EVFILT_WRITE), c)
}
if server.is_shutting_down() || c.should_close {
close_conn(server, kq, c_ptr, mut clients)
return
}
if c.request_active {
server.end_request()
c.request_active = false
}
c.free_write_buf()
c.free_request_arena()
c.write_pos = 0
c.read_len = 0
if c.read_extra.cap > 0 {
unsafe { c.read_extra.free() }
c.read_extra = []u8{}
}
c.read_start = 0
c.should_close = false
}
// process_request handles a complete HTTP request: decodes, calls the handler,
// sends the response (or handles takeover/sendfile).
fn process_request(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) {
mut c := unsafe { &Conn(c_ptr) }
mut request_arena := voidptr(unsafe { nil })
$if prealloc {
request_arena = unsafe { prealloc_scope_begin() }
}
mut req_buf := c.get_full_request_data()
if c.read_extra.cap > 0 {
unsafe { c.read_extra.free() }
c.read_extra = []u8{}
}
mut decoded := decode_http_request(req_buf) or {
send_bad_request(c.fd)
end_request_arena_current_thread(request_arena)
close_conn(server, kq, c_ptr, mut clients)
return
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'fasthttp decoded request') }
}
server.begin_request()
c.request_active = true
decoded.client_conn_fd = c.fd
decoded.user_data = server.user_data
mut resp := server.request_handler(decoded) or {
send_bad_request(c.fd)
end_request_arena_current_thread(request_arena)
close_conn(server, kq, c_ptr, mut clients)
return
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'fasthttp handler returned') }
}
resp.attach_request_arena_if_empty(request_arena)
match resp.takeover_mode {
.manual {
// The handler has taken ownership of the connection.
// Remove from kqueue and tracking, but do NOT close the fd.
clients.delete(c.fd)
delete_event(kq, u64(c.fd), i16(C.EVFILT_READ), c)
delete_event(kq, u64(c.fd), i16(C.EVFILT_WRITE), c)
if c.request_active {
server.end_request()
c.request_active = false
}
resp.free_owned_content()
resp.abandon_request_arena_current_thread()
unsafe { free(c_ptr) }
return
}
.reusable {
set_nonblocking(c.fd)
c.read_len = 0
c.read_extra.clear()
c.read_start = 0
if c.request_active {
server.end_request()
c.request_active = false
}
resp.free_owned_content()
resp.end_request_arena_current_thread()
if server.is_shutting_down() || resp.should_close {
close_conn(server, kq, c_ptr, mut clients)
}
return
}
.none {}
}
c.should_close = resp.should_close
c.free_write_buf()
c.free_request_arena()
c.request_arena = resp.take_request_arena()
c.write_buf = resp.take_or_clone_content()
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'fasthttp response retained') }
}
leave_request_arena_current_thread(c.request_arena)
if resp.file_path != '' {
fd := C.open(resp.file_path.str, C.O_RDONLY, 0)
if fd != -1 {
mut st := C.stat{}
if C.fstat(fd, &st) == 0 {
c.file_fd = fd
c.file_len = st.st_size
c.file_pos = 0
} else {
C.close(fd)
}
}
}
c.write_pos = 0
c.read_len = 0
c.read_extra.clear()
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)
return
}
complete_response(server, kq, c_ptr, mut clients, false)
}
// total_read_len returns the total number of request bytes received so far,
// including both the fixed read_buf and the dynamic read_extra overflow.
fn (c &Conn) total_read_len() int {
return c.read_len + c.read_extra.len
}
// get_full_request_data copies the complete received data into a single []u8.
fn (c &Conn) get_full_request_data() []u8 {
total := c.total_read_len()
mut req_buf := []u8{cap: total}
unsafe {
req_buf.push_many(&c.read_buf[0], c.read_len)
}
if c.read_extra.len > 0 {
req_buf << c.read_extra
}
return req_buf
}
fn handle_read(server &Server, kq int, c_ptr voidptr, mut clients map[int]voidptr) {
mut c := unsafe { &Conn(c_ptr) }
// Drain the socket for this kqueue notification. EV_CLEAR only rearms once
// all readable data has been consumed.
for {
if c.read_len < buf_size {
n := C.recv(c.fd, &c.read_buf[c.read_len], buf_size - c.read_len, 0)
if n < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
break
}
C.send(c.fd, status_444_response.data, status_444_response.len, send_flags)
close_conn(server, kq, c_ptr, mut clients)
return
}
if n == 0 {
if c.total_read_len() == 0 {
close_conn(server, kq, c_ptr, mut clients)
return
}
break
}
c.read_len += int(n)
} else {
// Fixed buffer is full, read the rest into dynamic overflow.
mut tmp := []u8{len: 65536}
n := C.recv(c.fd, tmp.data, tmp.len, 0)
if n < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
break
}
C.send(c.fd, status_444_response.data, status_444_response.len, send_flags)
close_conn(server, kq, c_ptr, mut clients)
return
}
if n == 0 {
if c.total_read_len() == 0 {
close_conn(server, kq, c_ptr, mut clients)
return
}
break
}
c.read_extra << tmp[..int(n)]
}
}
total := c.total_read_len()
if total == 0 {
return
}
// Enforce the configured header limit without capping large request bodies.
mut header_end := -1
mut full_data := []u8{}
if c.read_extra.len > 0 {
full_data = c.get_full_request_data()
header_end = find_header_end_in_buf(full_data.data, full_data.len)
} else {
header_end = find_header_end_in_buf(&c.read_buf[0], c.read_len)
}
if (header_end == -1 && total >= server.max_request_buffer_size)
|| header_end > server.max_request_buffer_size {
C.send(c.fd, status_413_response.data, status_413_response.len, send_flags)
close_conn(server, kq, c_ptr, mut clients)
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.
if c.read_extra.len > 0 {
if !has_complete_body(full_data.data, full_data.len) {
elapsed_ns := time.sys_mono_now() - c.read_start
timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000
if elapsed_ns >= timeout_ns {
send_request_timeout(c.fd)
close_conn(server, kq, c_ptr, mut clients)
}
return
}
} else if !has_complete_body(&c.read_buf[0], c.read_len) {
// Body not complete yet - check for timeout
elapsed_ns := time.sys_mono_now() - c.read_start
timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000
if elapsed_ns >= timeout_ns {
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)
}
fn accept_clients(kq int, listen_fd int, mut clients map[int]voidptr) {
for {
client_fd := C.accept(listen_fd, unsafe { nil }, unsafe { nil })
if client_fd < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
break
}
C.perror(c'accept')
break
}
set_nonblocking(client_fd)
// Prevent SIGPIPE on writes to disconnected clients. macOS and
// FreeBSD/NetBSD/DragonFly expose the per-socket SO_NOSIGPIPE
// option; OpenBSD does not, and instead expects MSG_NOSIGNAL on
// each send(2) call (handled via the `send_flags` const above).
$if !openbsd {
nosigpipe_opt := 1
C.setsockopt(client_fd, C.SOL_SOCKET, C.SO_NOSIGPIPE, &nosigpipe_opt, sizeof(int))
}
mut c := &Conn{
fd: client_fd
user_data: unsafe { nil }
file_fd: -1
}
add_event(kq, u64(client_fd), i16(C.EVFILT_READ), u16(C.EV_ADD | C.EV_ENABLE | C.EV_CLEAR),
c)
clients[client_fd] = c
}
}
fn close_all_conns(server &Server, kq int, mut clients map[int]voidptr) {
for client_fd in clients.keys() {
c_ptr := clients[client_fd] or { continue }
close_conn(server, kq, c_ptr, mut clients)
}
}
fn (mut s Server) stop_accepting() {
if s.poll_fd >= 0 && s.socket_fd >= 0 {
delete_event(s.poll_fd, u64(s.socket_fd), i16(C.EVFILT_READ), unsafe { nil })
}
if s.socket_fd >= 0 {
C.close(s.socket_fd)
s.socket_fd = -1
}
}
// run starts the server and enters the main event loop (Kqueue version).
pub fn (mut s Server) run() ! {
// Ignore SIGPIPE process-wide. Writing to a disconnected socket raises
// SIGPIPE by default, which kills the process. We suppress it per-send
// (SO_NOSIGPIPE on macos/freebsd/netbsd/dragonfly, MSG_NOSIGNAL on
// openbsd), but this signal handler is a safety net for any code path
// that might miss it (e.g. spawned SSE/WebSocket threads using
// TcpConn.write).
C.signal(C.SIGPIPE, C.SIG_IGN)
s.socket_fd = C.socket(i32(s.family), i32(net.SocketType.tcp), 0)
if s.socket_fd < 0 {
C.perror(c'socket')
return error('socket creation failed')
}
opt := 1
C.setsockopt(s.socket_fd, C.SOL_SOCKET, C.SO_REUSEADDR, &opt, sizeof(int))
addr := if s.family == .ip6 {
net.new_ip6(u16(s.port), [u8(0), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]!)
} else {
net.new_ip(u16(s.port), [u8(0), 0, 0, 0]!)
}
alen := addr.len()
if C.bind(s.socket_fd, voidptr(&addr), alen) < 0 {
C.perror(c'bind')
return error('socket bind failed')
}
if C.listen(s.socket_fd, backlog) < 0 {
C.perror(c'listen')
return error('socket listen failed')
}
set_nonblocking(s.socket_fd)
s.poll_fd = C.kqueue()
if s.poll_fd < 0 {
C.perror(c'kqueue')
return error('kqueue creation failed')
}
add_event(s.poll_fd, u64(s.socket_fd), i16(C.EVFILT_READ),
u16(C.EV_ADD | C.EV_ENABLE | C.EV_CLEAR), unsafe { nil })
listen_fd := s.socket_fd
s.mark_running()
println('listening on http://0.0.0.0:${s.port}/')
mut events := [kqueue_max_events]C.kevent{}
mut clients := map[int]voidptr{}
for {
if s.is_shutting_down() && s.active_request_count() == 0 {
close_all_conns(s, s.poll_fd, mut clients)
break
}
timeout := C.timespec{
tv_sec: 0
tv_nsec: kqueue_wait_timeout_ms * 1_000_000
}
nev := C.kevent(s.poll_fd, unsafe { nil }, 0, &events[0], kqueue_max_events, &timeout)
if nev < 0 {
if C.errno == C.EINTR {
// kevent may return EINTR when the process receives a signal
// (e.g. SIGCHLD from an exec'd subprocess). Treat like a timeout.
continue
}
if s.is_shutting_down() {
continue
}
C.perror(c'kevent')
break
}
for i := 0; i < nev; i++ {
event := events[i]
if event.flags & u16(C.EV_ERROR) != 0 {
if event.ident == u64(listen_fd) {
C.perror(c'listener error')
continue
}
if event.udata != unsafe { nil } {
close_conn(s, s.poll_fd, event.udata, mut clients)
}
continue
}
if event.ident == u64(listen_fd) {
if s.is_shutting_down() {
continue
}
accept_clients(s.poll_fd, listen_fd, mut clients)
continue
}
if event.udata == unsafe { nil } {
continue
}
if event.flags & u16(C.EV_EOF) != 0 {
close_conn(s, s.poll_fd, event.udata, mut clients)
continue
}
if event.filter == i16(C.EVFILT_READ) {
if s.is_shutting_down() {
close_conn(s, s.poll_fd, event.udata, mut clients)
continue
}
handle_read(s, s.poll_fd, event.udata, mut clients)
} else if event.filter == i16(C.EVFILT_WRITE) {
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_ns := i64(s.timeout_in_seconds) * 1_000_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 {
elapsed := now - c.read_start
if elapsed >= timeout_ns {
send_request_timeout(c.fd)
close_conn(s, s.poll_fd, c_ptr, mut clients)
}
}
}
}
}
if s.socket_fd >= 0 {
C.close(s.socket_fd)
s.socket_fd = -1
}
if s.poll_fd >= 0 {
C.close(s.poll_fd)
s.poll_fd = -1
}
s.mark_stopped()
}
+885
View File
@@ -0,0 +1,885 @@
module fasthttp
import net
import sync.stdatomic
import time
#include <sys/epoll.h>
#include <sys/sendfile.h>
#include <sys/stat.h>
#include <netinet/tcp.h>
const epoll_wait_timeout_ms = 100
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 C.accept4(sockfd i32, addr &net.Addr, addrlen &u32, flags i32) i32
fn C.epoll_create1(__flags i32) i32
fn C.epoll_ctl(__epfd i32, __op i32, __fd i32, __event &C.epoll_event) i32
fn C.epoll_wait(__epfd i32, __events &C.epoll_event, __maxevents i32, __timeout i32) i32
fn C.sendfile(out_fd i32, in_fd i32, offset &i64, count usize) i32
fn C.fstat(fd i32, buf &C.stat) i32
@[typedef]
union C.epoll_data_t {
mut:
ptr voidptr
fd int
u32 u32
u64 u64
}
struct C.epoll_event {
mut:
events u32
data C.epoll_data_t
}
pub struct Server {
pub:
family net.AddrFamily = .ip6
port int = 3000
max_request_buffer_size int = 8192
timeout_in_seconds int = 30
user_data voidptr
mut:
listen_fds []int = []int{len: max_thread_pool_size, cap: max_thread_pool_size, init: -1}
epoll_fds []int = []int{len: max_thread_pool_size, cap: max_thread_pool_size, init: -1}
threads []thread = []thread{len: max_thread_pool_size, cap: max_thread_pool_size}
request_handler fn (HttpRequest) !HttpResponse @[required]
running &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(false)
shutting_down &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(false)
stopped &stdatomic.AtomicVal[bool] = stdatomic.new_atomic(true)
active_requests &stdatomic.AtomicVal[int] = stdatomic.new_atomic(0)
}
// new_server creates and initializes a new Server instance.
pub fn new_server(config ServerConfig) !&Server {
if config.max_request_buffer_size <= 0 {
return error('max_request_buffer_size must be greater than 0')
}
mut server := &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)
shutting_down: stdatomic.new_atomic(false)
stopped: stdatomic.new_atomic(true)
active_requests: stdatomic.new_atomic(0)
}
unsafe {
server.listen_fds.flags.set(.noslices | .noshrink | .nogrow)
server.epoll_fds.flags.set(.noslices | .noshrink | .nogrow)
server.threads.flags.set(.noslices | .noshrink | .nogrow)
}
return server
}
fn set_blocking(fd int, blocking bool) {
flags := C.fcntl(fd, C.F_GETFL, 0)
if flags == -1 {
// TODO: better error handling
eprintln(@LOCATION)
return
}
if blocking {
// This removes the O_NONBLOCK flag from flags and set it.
C.fcntl(fd, C.F_SETFL, flags & ~C.O_NONBLOCK)
} else {
// This adds the O_NONBLOCK flag from flags and set it.
C.fcntl(fd, C.F_SETFL, flags | C.O_NONBLOCK)
}
}
// ClientWriteState carries the pending response bytes plus optional file data
// for a connection that could not be drained in a single non-blocking write.
// It lives in `client_write_states` until both the in-memory content and the
// file body have been fully transferred. While a state exists, the connection
// is also armed for EPOLLOUT so process_events can resume the write without
// blocking the worker.
struct ClientWriteState {
mut:
content []u8
content_pos int
content_owned bool
file_fd int = -1
file_len i64
file_pos i64
should_close bool
request_arena voidptr
request_active bool
start_ns i64
// epollout_armed records whether we actually had to register EPOLLOUT for
// this connection. When set, complete_write performs a DEL+ADD on the fd to
// re-deliver any pipelined request bytes that arrived during the write as a
// fresh EPOLLIN edge (level-triggered semantics are not used here).
epollout_armed bool
}
// drain_status describes the outcome of one try_drain_write() pass.
enum DrainStatus {
done // all bytes (content + file) have been sent
blocked // the kernel send buffer is full; come back on EPOLLOUT
failed // unrecoverable error or peer closed mid-transfer
}
fn close_socket(fd int) bool {
ret := C.close(fd)
if ret == -1 {
if C.errno == C.EINTR {
// Interrupted by signal, retry is safe
return close_socket(fd)
}
eprintln('ERROR: close(fd=${fd}) failed with errno=${C.errno}')
return false
}
return true
}
fn create_server_socket(server Server) int {
// Create a socket with non-blocking mode
server_fd := C.socket(i32(server.family), i32(net.SocketType.tcp), 0)
if server_fd < 0 {
eprintln(@LOCATION)
C.perror(c'Socket creation failed')
return -1
}
set_blocking(server_fd, false)
// Enable SO_REUSEADDR and SO_REUSEPORT
opt := 1
if C.setsockopt(server_fd, C.SOL_SOCKET, C.SO_REUSEADDR, &opt, sizeof(opt)) < 0 {
eprintln(@LOCATION)
C.perror(c'setsockopt SO_REUSEADDR failed')
close_socket(server_fd)
return -1
}
if C.setsockopt(server_fd, C.SOL_SOCKET, C.SO_REUSEPORT, &opt, sizeof(opt)) < 0 {
eprintln(@LOCATION)
C.perror(c'setsockopt SO_REUSEPORT failed')
close_socket(server_fd)
return -1
}
addr := if server.family == .ip6 {
net.new_ip6(u16(server.port), [u8(0), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]!)
} else {
net.new_ip(u16(server.port), [u8(0), 0, 0, 0]!)
}
alen := addr.len()
if C.bind(server_fd, voidptr(&addr), alen) < 0 {
eprintln(@LOCATION)
C.perror(c'Bind failed')
close_socket(server_fd)
return -1
}
if C.listen(server_fd, max_connection_size) < 0 {
eprintln(@LOCATION)
C.perror(c'Listen failed')
close_socket(server_fd)
return -1
}
return server_fd
}
// add_fd_to_epoll adds a file descriptor to the epoll instance
fn add_fd_to_epoll(epoll_fd int, fd int, events u32) int {
mut ev := C.epoll_event{
events: events
}
ev.data.fd = fd
if C.epoll_ctl(epoll_fd, C.EPOLL_CTL_ADD, fd, &ev) == -1 {
eprintln(@LOCATION)
C.perror(c'epoll_ctl')
return -1
}
return 0
}
// remove_fd_from_epoll removes a file descriptor from the epoll instance
fn remove_fd_from_epoll(epoll_fd int, fd int) bool {
ret := C.epoll_ctl(epoll_fd, C.EPOLL_CTL_DEL, fd, C.NULL)
if ret == -1 {
eprintln('ERROR: epoll_ctl(DEL, fd=${fd}) failed with errno=${C.errno}')
return false
}
return true
}
fn handle_accept_loop(epoll_fd int, listen_fd int, mut client_fds map[int]bool) {
for {
client_fd := C.accept4(listen_fd, C.NULL, C.NULL, C.SOCK_NONBLOCK)
if client_fd < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
break // No more incoming connections; exit loop.
}
eprintln(@LOCATION)
C.perror(c'Accept failed')
break
}
// Enable TCP_NODELAY for lower latency
opt := 1
C.setsockopt(client_fd, C.IPPROTO_TCP, C.TCP_NODELAY, &opt, sizeof(opt))
// Register client socket with epoll
if add_fd_to_epoll(epoll_fd, client_fd, u32(C.EPOLLIN | C.EPOLLET)) == -1 {
close_socket(client_fd)
continue
}
client_fds[client_fd] = true
}
}
fn free_write_state(server &Server, client_fd int, mut client_write_states map[int]&ClientWriteState) {
mut state := client_write_states[client_fd] or { return }
if state.content_owned && state.content.cap > 0 {
unsafe { state.content.free() }
}
state.content = []u8{}
if state.file_fd != -1 {
C.close(state.file_fd)
state.file_fd = -1
}
$if prealloc {
if state.request_arena != unsafe { nil } {
unsafe { prealloc_scope_free_after(state.request_arena) }
}
}
state.request_arena = unsafe { nil }
if state.request_active {
server.end_request()
state.request_active = false
}
client_write_states.delete(client_fd)
unsafe { free(state) }
}
fn handle_client_closure(server &Server, epoll_fd int, client_fd int, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool, mut client_write_states map[int]&ClientWriteState) {
// Never close the listening socket here
if client_fd == 0 {
return
}
if client_fd <= 0 {
eprintln('ERROR: Invalid FD=${client_fd} for closure')
return
}
client_fds.delete(client_fd)
client_buffers.delete(client_fd)
client_read_starts.delete(client_fd)
closing_client_fds.delete(client_fd)
free_write_state(server, client_fd, mut client_write_states)
remove_fd_from_epoll(epoll_fd, client_fd)
close_socket(client_fd)
}
fn close_worker_clients(server &Server, epoll_fd int, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool, mut client_write_states map[int]&ClientWriteState) {
for client_fd in client_fds.keys() {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
}
}
fn drain_closing_client(server &Server, epoll_fd int, client_fd int, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool, mut client_write_states map[int]&ClientWriteState) {
mut drain_buf := []u8{len: 4096}
for {
bytes_read := C.recv(client_fd, unsafe { &drain_buf[0] }, drain_buf.len, 0)
if bytes_read < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
return
}
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
if bytes_read == 0 {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
}
}
fn send_terminal_response_and_drain(client_fd int, response []u8, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool) {
C.send(client_fd, response.data, response.len, C.MSG_NOSIGNAL)
net.shutdown(client_fd, how: .write)
client_buffers.delete(client_fd)
client_read_starts.delete(client_fd)
closing_client_fds[client_fd] = true
}
// try_drain_write attempts to push the remaining bytes of `state.content` and
// (if present) `state.file_fd` to the socket in non-blocking mode. It returns
// `.done` once all data has been transferred, `.blocked` when the kernel send
// buffer is full (EAGAIN/EWOULDBLOCK), or `.failed` on any unrecoverable error.
// The caller is responsible for parking the state until EPOLLOUT fires (when
// `.blocked` is returned) and for cleaning up via `free_write_state` once
// `.done` or `.failed` is reached.
fn try_drain_write(client_fd int, mut state ClientWriteState) DrainStatus {
// 1. Drain in-memory content (response headers + possibly a body).
for state.content_pos < state.content.len {
remaining := state.content.len - state.content_pos
sent := C.send(client_fd, unsafe { &state.content[state.content_pos] }, remaining,
C.MSG_NOSIGNAL)
if sent > 0 {
state.content_pos += sent
continue
}
if sent < 0 {
errno_val := C.errno
if errno_val == C.EAGAIN || errno_val == C.EWOULDBLOCK {
return .blocked
}
if errno_val == C.EINTR {
continue
}
eprintln('ERROR: send() failed with errno=${errno_val}')
return .failed
}
// send() returning 0 on a non-blocking TCP socket is unusual; treat it as
// a failure rather than spin.
return .failed
}
// 2. Drain the optional file body via sendfile(2). sendfile updates the
// offset pointer in place, so `state.file_pos` advances automatically.
if state.file_fd != -1 {
for state.file_pos < state.file_len {
remaining := state.file_len - state.file_pos
ssize := C.sendfile(client_fd, state.file_fd, &state.file_pos, usize(remaining))
if ssize > 0 {
continue
}
if ssize == 0 {
// Short transfer: the file shrank between fstat() and now, or the
// peer closed. We have already promised the full Content-Length,
// so the response is now truncated -- closing the connection is
// the only way to keep keep-alive clients in sync.
eprintln('ERROR: sendfile() returned 0 with ${remaining} bytes still pending; closing connection to avoid desync')
return .failed
}
errno_val := C.errno
if errno_val == C.EAGAIN || errno_val == C.EWOULDBLOCK {
return .blocked
}
if errno_val == C.EINTR {
continue
}
match errno_val {
C.EBADF {
eprintln('ERROR: sendfile() EBADF: input fd or socket not open for required access (errno=${errno_val})')
}
C.EFAULT {
eprintln('ERROR: sendfile() EFAULT: bad address for offset (errno=${errno_val})')
}
C.EINVAL {
eprintln('ERROR: sendfile() EINVAL: invalid descriptor state or non-seekable input (errno=${errno_val})')
}
C.EIO {
eprintln('ERROR: sendfile() EIO: I/O error while reading input file (errno=${errno_val})')
}
C.ENOMEM {
eprintln('ERROR: sendfile() ENOMEM: insufficient kernel memory (errno=${errno_val})')
}
C.EOVERFLOW {
eprintln('ERROR: sendfile() EOVERFLOW: count exceeds file/socket limits (errno=${errno_val})')
}
C.ESPIPE {
eprintln('ERROR: sendfile() ESPIPE: input file not seekable with offset (errno=${errno_val})')
}
else {
eprintln('ERROR: sendfile() failed with errno=${errno_val}')
}
}
return .failed
}
// Done with the file -- close the fd eagerly so keep-alive doesn't hold it open.
C.close(state.file_fd)
state.file_fd = -1
}
return .done
}
// arm_epollout switches the connection's epoll mask from EPOLLIN|EPOLLET to
// EPOLLIN|EPOLLOUT|EPOLLET so the next round of pending bytes is delivered as
// an EPOLLOUT event instead of blocking the worker.
fn arm_epollout(epoll_fd int, client_fd int) int {
mut ev := C.epoll_event{
events: u32(C.EPOLLIN | C.EPOLLOUT | C.EPOLLET)
}
ev.data.fd = client_fd
return C.epoll_ctl(epoll_fd, C.EPOLL_CTL_MOD, client_fd, &ev)
}
// complete_write tears down the per-fd write state after a successful drain,
// then either closes the connection or leaves it in keep-alive mode.
fn complete_write(server &Server, epoll_fd int, client_fd int, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool, mut client_write_states map[int]&ClientWriteState) {
state := client_write_states[client_fd] or { return }
should_close := state.should_close
epollout_was_armed := state.epollout_armed
free_write_state(server, client_fd, mut client_write_states)
client_buffers.delete(client_fd)
if server.is_shutting_down() || should_close {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
// Keep-alive: if EPOLLOUT was registered for the write, drop the fd from
// epoll and re-add it with the original EPOLLIN|EPOLLET mask. The re-add
// causes the kernel to fire a fresh edge for any pipelined request bytes
// that piled up in the recv buffer while we were write-blocked; a plain
// EPOLL_CTL_MOD would not generate that edge.
if epollout_was_armed {
remove_fd_from_epoll(epoll_fd, client_fd)
if add_fd_to_epoll(epoll_fd, client_fd, u32(C.EPOLLIN | C.EPOLLET)) == -1 {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
}
}
}
// handle_write resumes a blocked write when EPOLLOUT fires for `client_fd`.
fn handle_write(server &Server, epoll_fd int, client_fd int, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool, mut client_write_states map[int]&ClientWriteState) {
mut state := client_write_states[client_fd] or { return }
status := try_drain_write(client_fd, mut state)
match status {
.done {
complete_write(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
}
.blocked {
// Still blocked -- stay armed for the next EPOLLOUT.
}
.failed {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
}
}
}
fn process_request(server &Server, epoll_fd int, client_fd int, request_buffer []u8, mut client_fds map[int]bool, mut client_buffers map[int][]u8, mut client_read_starts map[int]i64, mut closing_client_fds map[int]bool, mut client_write_states map[int]&ClientWriteState) {
mut request_arena := voidptr(unsafe { nil })
$if prealloc {
request_arena = unsafe { prealloc_scope_begin() }
}
client_read_starts.delete(client_fd)
server.begin_request()
mut request_active := true
mut decoded_http_request := decode_http_request(request_buffer) or {
eprintln('Error decoding request ${err}')
C.send(client_fd, tiny_bad_request_response.data, tiny_bad_request_response.len,
C.MSG_NOSIGNAL)
end_request_arena_current_thread(request_arena)
if request_active {
server.end_request()
}
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'fasthttp decoded request') }
}
decoded_http_request.client_conn_fd = client_fd
decoded_http_request.user_data = server.user_data
mut response := server.request_handler(decoded_http_request) or {
eprintln('Error handling request ${err}')
C.send(client_fd, tiny_bad_request_response.data, tiny_bad_request_response.len,
C.MSG_NOSIGNAL)
end_request_arena_current_thread(request_arena)
if request_active {
server.end_request()
}
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
$if trace_prealloc ? {
unsafe { prealloc_scope_checkpoint(c'fasthttp handler returned') }
}
response.attach_request_arena_if_empty(request_arena)
match response.takeover_mode {
.manual {
// The handler has taken ownership of the connection.
// Remove from epoll and tracking, but do NOT close the fd.
client_fds.delete(client_fd)
client_buffers.delete(client_fd)
client_read_starts.delete(client_fd)
closing_client_fds.delete(client_fd)
remove_fd_from_epoll(epoll_fd, client_fd)
response.abandon_request_arena_current_thread()
response.free_owned_content()
if request_active {
server.end_request()
}
return
}
.reusable {
set_blocking(client_fd, false)
client_buffers.delete(client_fd)
response.free_owned_content()
response.end_request_arena_current_thread()
if request_active {
server.end_request()
}
if server.is_shutting_down() || response.should_close {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
}
return
}
.none {}
}
// Open the file (if any) before constructing the state so we can fail fast
// while we still own the request arena via response.
mut file_fd := -1
mut file_len := i64(0)
if response.file_path != '' {
fd := C.open(response.file_path.str, C.O_RDONLY, 0)
if fd == -1 {
eprintln('ERROR: open file failed')
response.free_owned_content()
response.end_request_arena_current_thread()
if request_active {
server.end_request()
}
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
mut st := C.stat{}
if C.fstat(fd, &st) != 0 {
eprintln('ERROR: fstat failed')
C.close(fd)
response.free_owned_content()
response.end_request_arena_current_thread()
if request_active {
server.end_request()
}
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
file_fd = fd
file_len = i64(st.st_size)
}
// Move content + arena ownership into the per-fd state. After this point we
// must not touch the response's arena via end/free (it will be released by
// free_write_state once the drain completes).
content_bytes := response.take_or_clone_content()
arena_ptr := response.take_request_arena()
leave_request_arena_current_thread(arena_ptr)
mut state := &ClientWriteState{
content: content_bytes
content_pos: 0
content_owned: true
file_fd: file_fd
file_len: file_len
file_pos: 0
should_close: response.should_close
request_arena: arena_ptr
request_active: request_active
start_ns: time.sys_mono_now()
}
// From here on, the state owns end_request bookkeeping.
request_active = false
client_write_states[client_fd] = state
status := try_drain_write(client_fd, mut state)
match status {
.done {
complete_write(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
}
.blocked {
// Kernel send buffer is full. Park the state and resume on EPOLLOUT.
client_buffers.delete(client_fd)
client_read_starts.delete(client_fd)
if arm_epollout(epoll_fd, client_fd) == -1 {
eprintln('ERROR: epoll_ctl(MOD, EPOLLOUT) failed errno=${C.errno}')
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
} else {
state.epollout_armed = true
}
}
.failed {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
}
}
}
fn process_events(server &Server, epoll_fd int, listen_fd int) {
mut events := [max_connection_size]C.epoll_event{}
mut request_buffer := []u8{len: server.max_request_buffer_size, cap: server.max_request_buffer_size}
mut client_fds := map[int]bool{}
mut client_buffers := map[int][]u8{}
mut client_read_starts := map[int]i64{}
mut closing_client_fds := map[int]bool{}
mut client_write_states := map[int]&ClientWriteState{}
unsafe {
request_buffer.flags.set(.noslices | .nogrow | .noshrink)
}
for {
if server.is_shutting_down() && server.active_request_count() == 0 {
close_worker_clients(server, epoll_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
return
}
num_events := C.epoll_wait(epoll_fd, &events[0], max_connection_size, epoll_wait_timeout_ms)
if num_events < 0 {
if C.errno == C.EINTR {
continue
}
if server.is_shutting_down() {
continue
}
eprintln('ERROR: epoll_wait() failed with errno=${C.errno}')
continue
}
for i := 0; i < num_events; i++ {
client_fd := unsafe { events[i].data.fd }
// Accept new connections when the listening socket is readable
if client_fd == listen_fd {
if server.is_shutting_down() {
continue
}
handle_accept_loop(epoll_fd, listen_fd, mut client_fds)
continue
}
if events[i].events & u32((C.EPOLLHUP | C.EPOLLERR)) != 0 {
if client_fd == listen_fd {
eprintln('ERROR: listen fd had HUP/ERR')
continue
}
if client_fd > 0 {
// Don't bother sending 444 if there is an in-flight write -- the
// peer is already disconnected and we want to tear down promptly.
if client_fd !in client_write_states {
C.send(client_fd, status_444_response.data, status_444_response.len,
C.MSG_NOSIGNAL)
}
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
} else {
eprintln('ERROR: Invalid FD from epoll: ${client_fd}')
}
continue
}
if events[i].events & u32(C.EPOLLOUT) != 0 {
handle_write(server, epoll_fd, client_fd, mut client_fds, mut client_buffers, mut
client_read_starts, mut closing_client_fds, mut client_write_states)
// If both EPOLLIN and EPOLLOUT fired, we still want to drain the
// socket -- fall through to the EPOLLIN branch below.
if client_fd !in client_fds {
continue
}
}
if events[i].events & u32(C.EPOLLIN) != 0 {
if server.is_shutting_down() {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
continue
}
if closing_client_fds[client_fd] or { false } {
drain_closing_client(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
continue
}
if client_fd in client_write_states {
// A response is still being drained for this fd. Reading a new
// request now would overwrite the in-flight ClientWriteState and
// silently drop the response/file transfer. Defer until the write
// completes -- on keep-alive completion we re-arm EPOLLIN below so
// any bytes that arrived during the write get delivered as a fresh
// edge.
continue
}
// Read all available data from the socket
mut total_bytes_read := 0
mut readed_request_buffer := client_buffers[client_fd] or {
[]u8{cap: server.max_request_buffer_size}
}
mut headers_complete := false
mut header_too_large := false
mut header_end_pos := -1
mut request_complete := false
mut peer_closed := false
mut recv_error := false
for {
bytes_read := C.recv(client_fd, unsafe { &request_buffer[0] },
server.max_request_buffer_size - 1, 0)
if bytes_read < 0 {
if C.errno == C.EAGAIN || C.errno == C.EWOULDBLOCK {
// No more data available right now
break
}
// Error occurred
eprintln('ERROR: recv() failed with errno=${C.errno}')
recv_error = true
break
} else if bytes_read == 0 {
// Connection closed by client
peer_closed = true
break
}
unsafe {
readed_request_buffer.push_many(&request_buffer[0], bytes_read)
}
total_bytes_read += bytes_read
if client_fd !in client_read_starts {
client_read_starts[client_fd] = time.sys_mono_now()
}
// Enforce the configured limit on request headers, not on the whole body.
buffer_len := readed_request_buffer.len
if !headers_complete && buffer_len >= 4 {
header_end_pos = find_header_end_in_buf(readed_request_buffer.data,
buffer_len)
if header_end_pos == -1 {
if buffer_len >= server.max_request_buffer_size {
header_too_large = true
break
}
} else {
headers_complete = true
if header_end_pos > server.max_request_buffer_size {
header_too_large = true
break
}
}
}
if headers_complete && has_complete_body(readed_request_buffer.data, buffer_len) {
request_complete = true
break
}
}
if header_too_large {
send_terminal_response_and_drain(client_fd, status_413_response, mut
client_buffers, mut client_read_starts, mut closing_client_fds)
continue
}
if request_complete {
process_request(server, epoll_fd, client_fd, readed_request_buffer, mut
client_fds, mut client_buffers, mut client_read_starts, mut
closing_client_fds, mut client_write_states)
} else if recv_error {
// Unexpected recv error - send 444 No Response
C.send(client_fd, status_444_response.data, status_444_response.len,
C.MSG_NOSIGNAL)
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
} else if peer_closed || (total_bytes_read == 0 && readed_request_buffer.len == 0) {
// Normal client closure (FIN received)
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
} else if readed_request_buffer.len > 0 {
client_buffers[client_fd] = readed_request_buffer
}
}
}
if server.timeout_in_seconds > 0 {
now := time.sys_mono_now()
timeout_ns := i64(server.timeout_in_seconds) * 1_000_000_000
for client_fd in client_read_starts.keys() {
started := client_read_starts[client_fd] or { continue }
if now - started >= timeout_ns {
send_terminal_response_and_drain(client_fd, status_408_response, mut
client_buffers, mut client_read_starts, mut closing_client_fds)
}
}
// Sweep write-side stalls: a client that armed EPOLLOUT but never
// drains within `timeout_in_seconds` gets the connection torn down so
// the worker isn't pinned waiting for a dead peer.
for client_fd in client_write_states.keys() {
state := client_write_states[client_fd] or { continue }
if state.start_ns > 0 && now - state.start_ns >= timeout_ns {
handle_client_closure(server, epoll_fd, client_fd, mut client_fds, mut
client_buffers, mut client_read_starts, mut closing_client_fds, mut
client_write_states)
}
}
}
}
}
fn (mut server Server) stop_accepting() {
for i := 0; i < max_thread_pool_size; i++ {
if server.listen_fds[i] < 0 {
continue
}
if server.epoll_fds[i] >= 0 {
remove_fd_from_epoll(server.epoll_fds[i], server.listen_fds[i])
}
close_socket(server.listen_fds[i])
server.listen_fds[i] = -1
}
}
// run starts the server and begins listening for incoming connections.
pub fn (mut server Server) run() ! {
$if windows {
eprintln('Windows is not supported yet')
return
}
for i := 0; i < max_thread_pool_size; i++ {
server.listen_fds[i] = create_server_socket(server)
if server.listen_fds[i] < 0 {
return
}
server.epoll_fds[i] = C.epoll_create1(0)
if server.epoll_fds[i] < 0 {
C.perror(c'epoll_create1 failed')
close_socket(server.listen_fds[i])
return
}
// Register the listening socket with each worker epoll for distributed accepts (edge-triggered)
if add_fd_to_epoll(server.epoll_fds[i], server.listen_fds[i], u32(C.EPOLLIN | C.EPOLLET)) == -1 {
close_socket(server.listen_fds[i])
close_socket(server.epoll_fds[i])
return
}
server.threads[i] = spawn process_events(server, server.epoll_fds[i], server.listen_fds[i])
}
server.mark_running()
println('listening on http://0.0.0.0:${server.port}/')
// Main thread waits for workers; accepts are handled in worker epoll loops
for i in 0 .. max_thread_pool_size {
server.threads[i].wait()
if server.epoll_fds[i] >= 0 {
close_socket(server.epoll_fds[i])
server.epoll_fds[i] = -1
}
}
server.mark_stopped()
}
+237
View File
@@ -0,0 +1,237 @@
module fasthttp
import net
import os
import time
const fasthttp_example_exe = os.join_path(os.cache_dir(), 'fasthttp_example_test.exe')
const reusable_takeover_port = 13019
const reusable_takeover_addr = '127.0.0.1:${reusable_takeover_port}'
fn testsuite_begin() {
// Clean up old example binary if it exists
if os.exists(fasthttp_example_exe) {
os.rm(fasthttp_example_exe) or {}
}
}
fn test_fasthttp_example_compiles() {
vexe := os.getenv('VEXE')
vroot := os.dir(vexe)
// Build the fasthttp example
build_result := os.system('${os.quoted_path(vexe)} -o ${os.quoted_path(fasthttp_example_exe)} ${os.join_path(vroot,
'examples', 'fasthttp')}')
assert build_result == 0, 'fasthttp example failed to compile'
assert os.exists(fasthttp_example_exe), 'fasthttp example binary not found after build'
}
fn test_parse_request_line() {
// Test basic GET request
request := 'GET / HTTP/1.1\r\n'.bytes()
req := decode_http_request(request) or {
assert false, 'Failed to parse valid request: ${err}'
return
}
assert req.buffer.len == request.len
assert req.method.start == 0
assert req.method.len == 3
assert req.path.start == 4
assert req.path.len == 1
assert req.version.start == 6
assert req.version.len == 8
method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr()
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
version := req.buffer[req.version.start..req.version.start + req.version.len].bytestr()
assert method == 'GET'
assert path == '/'
assert version == 'HTTP/1.1'
}
fn test_parse_request_line_with_path() {
// Test GET request with path
request := 'GET /users/123 HTTP/1.1\r\n'.bytes()
req := decode_http_request(request) or {
assert false, 'Failed to parse valid request: ${err}'
return
}
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
assert path == '/users/123'
}
fn test_parse_request_line_post() {
// Test POST request
request := 'POST /api/data HTTP/1.1\r\n'.bytes()
req := decode_http_request(request) or {
assert false, 'Failed to parse valid request: ${err}'
return
}
method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr()
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
assert method == 'POST'
assert path == '/api/data'
}
fn test_parse_request_line_invalid() {
// Test invalid request (missing \r\n)
request := 'GET / HTTP/1.1'.bytes()
decode_http_request(request) or {
assert err.msg() == 'Invalid HTTP request line: Missing CR'
return
}
assert false, 'Should have failed to parse invalid request'
}
fn test_decode_http_request() {
request := 'GET /test HTTP/1.1\r\n'.bytes()
req := decode_http_request(request) or {
assert false, 'Failed to decode request: ${err}'
return
}
method := req.buffer[req.method.start..req.method.start + req.method.len].bytestr()
assert method == 'GET'
}
fn test_new_server() {
handler := fn (req HttpRequest) !HttpResponse {
return HttpResponse{
content: 'HTTP/1.1 200 OK\r\n\r\nHello'.bytes()
}
}
server := new_server(ServerConfig{
port: 8080
handler: handler
}) or {
assert false, 'Failed to create server: ${err}'
return
}
assert server.port == 8080
}
fn test_server_ipv4_ipv6_binding() {
// Test IPv4 binding
handler := fn (req HttpRequest) !HttpResponse {
return HttpResponse{
content: 'HTTP/1.1 200 OK\r\n\r\nIPv4 test'.bytes()
}
}
server_ipv4 := new_server(ServerConfig{
family: .ip
port: 8081
handler: handler
}) or {
assert false, 'Failed to create IPv4 server: ${err}'
return
}
// Test IPv6 binding
server_ipv6 := new_server(ServerConfig{
family: .ip6
port: 8082
handler: handler
}) or {
assert false, 'Failed to create IPv6 server: ${err}'
return
}
// Verify both servers were created successfully
// Note: family field is not exported, so we can't directly test it
assert server_ipv4.port == 8081
assert server_ipv6.port == 8082
}
fn test_response_takeover_mode_reusable_keeps_connection() {
$if linux || bsd {
mut server := new_server(ServerConfig{
family: .ip
port: reusable_takeover_port
timeout_in_seconds: 2
max_request_buffer_size: 8192
handler: reusable_takeover_handler
}) or {
assert false, 'Failed to create server: ${err}'
return
}
handle := server.handle()
spawn server.run()
handle.wait_till_running(max_retries: 1000, retry_period_ms: 10) or {
assert false, 'server did not start: ${err}'
return
}
defer {
handle.shutdown(timeout: 5 * time.second) or {}
}
mut conn := net.dial_tcp(reusable_takeover_addr)!
conn.set_read_timeout(2 * time.second)
conn.set_write_timeout(2 * time.second)
defer {
conn.close() or {}
}
conn.write_string('GET /reus')!
time.sleep(50 * time.millisecond)
conn.write_string('able HTTP/1.1\r\nHost: ${reusable_takeover_addr}\r\n\r\n')!
reusable_response := read_until_contains(mut conn, '\r\n0\r\n\r\n')!
assert reusable_response.contains('manual') == true, reusable_response
assert reusable_response.contains('\r\n0\r\n\r\n') == true, reusable_response
conn.write_string('GET /normal HTTP/1.1\r\nHost: ${reusable_takeover_addr}\r\n\r\n')!
normal_response := read_until_contains(mut conn, 'normal')!
assert normal_response.contains('normal') == true, normal_response
assert normal_response.contains('Connection: close') == false, normal_response
} $else {
return
}
}
fn reusable_takeover_handler(req HttpRequest) !HttpResponse {
path := req.buffer[req.path.start..req.path.start + req.path.len].bytestr()
if path == '/reusable' {
body := 'manual'
send_raw_response(req.client_conn_fd,
'HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n${body.len:x}\r\n${body}\r\n0\r\n\r\n')
return HttpResponse{
takeover_mode: .reusable
}
}
return HttpResponse{
content: 'HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\nnormal'.bytes()
}
}
fn send_raw_response(fd int, response string) {
$if linux {
C.send(fd, response.str, response.len, C.MSG_NOSIGNAL)
} $else $if bsd {
C.send(fd, response.str, response.len, send_flags)
} $else {
C.send(fd, response.str, response.len, 0)
}
}
fn read_until_contains(mut conn net.TcpConn, marker string) !string {
mut raw := ''
mut buf := []u8{len: 1024}
for _ in 0 .. 16 {
n := conn.read(mut buf)!
if n <= 0 {
break
}
raw += buf[..n].bytestr()
if raw.contains(marker) {
break
}
}
return raw
}
+25
View File
@@ -0,0 +1,25 @@
module fasthttp
pub struct Server {
pub:
port int = 3000
max_request_buffer_size int = 8192
mut:
request_handler fn (HttpRequest) !HttpResponse @[required]
}
// new_server creates and initializes a new Server instance.
pub fn new_server(config ServerConfig) !&Server {
mut server := &Server{
port: config.port
max_request_buffer_size: config.max_request_buffer_size
request_handler: config.handler
}
return server
}
// run starts the server and begins listening for incoming connections.
pub fn (mut server Server) run() ! {
println('TODO: implement fasthttp.Server.run on windows')
}
+409
View File
@@ -0,0 +1,409 @@
module fasthttp
const empty_space = u8(` `)
const cr_char = u8(0x0d)
const lf_char = u8(0x0a)
// libc memchr is AVX2-accelerated via glibc IFUNC
@[inline]
fn find_byte(buf &u8, len int, c u8) int {
unsafe {
p := C.memchr(buf, c, len)
if p == voidptr(nil) {
return -1
}
return int(&u8(p) - buf)
}
}
// parse_http1_request_line parses the request line of an HTTP/1.1 request.
// spec: https://datatracker.ietf.org/doc/rfc9112/
// request-line is the start-line for for requests
// According to RFC 9112, the request line is structured as:
// `request-line = method SP request-target SP HTTP-version`
// where:
// METHOD is the HTTP method (e.g., GET, POST)
// SP is a single space character
// REQUEST-TARGET is the path or resource being requested
// HTTP-VERSION is the version of HTTP being used (e.g., HTTP/1.1)
// CRLF is a carriage return followed by a line feed
// returns the position after the CRLF on success
@[direct_array_access]
pub fn parse_http1_request_line(mut req HttpRequest) !int {
unsafe {
buf := &req.buffer[0]
len := req.buffer.len
if len < 12 {
return error('Too short')
}
// METHOD
pos1 := find_byte(buf, len, empty_space)
if pos1 <= 0 {
return error('Invalid method')
}
req.method = Slice{0, pos1}
// PATH - skip any extra spaces
mut pos2 := pos1 + 1
for pos2 < len && buf[pos2] == empty_space {
pos2++
}
if pos2 >= len {
return error('Missing path')
}
path_start := pos2
space_pos := find_byte(buf + pos2, len - pos2, empty_space)
cr_pos := find_byte(buf + pos2, len - pos2, cr_char)
if space_pos < 0 && cr_pos < 0 {
return error('Invalid request line')
}
// pick earliest delimiter
mut path_len := 0
mut delim_pos := 0
if space_pos >= 0 && (cr_pos < 0 || space_pos < cr_pos) {
path_len = space_pos
delim_pos = pos2 + space_pos
} else {
path_len = cr_pos
delim_pos = pos2 + cr_pos
}
req.path = Slice{path_start, path_len}
// VERSION
if buf[delim_pos] == cr_char {
// No HTTP version specified
req.version = Slice{delim_pos, 0}
} else {
version_start := delim_pos + 1
cr := find_byte(buf + version_start, len - version_start, cr_char)
if cr < 0 {
return error('Invalid HTTP request line: Missing CR')
}
req.version = Slice{version_start, cr}
delim_pos = version_start + cr
}
// Validate CRLF
if delim_pos + 1 >= len || buf[delim_pos + 1] != lf_char {
return error('Invalid CRLF')
}
return delim_pos + 2 // Return position after CRLF
}
}
// decode_http_request parses a raw HTTP request from the given byte buffer
pub fn decode_http_request(buffer []u8) !HttpRequest {
mut req := HttpRequest{
buffer: buffer
}
// header_start is the byte index immediately after the request line's \r\n
header_start := parse_http1_request_line(mut req)!
// Find the end of the header block (\r\n\r\n)
mut body_start := -1
for i := header_start; i <= buffer.len - 4; i++ {
if buffer[i] == cr_char && buffer[i + 1] == lf_char && buffer[i + 2] == cr_char
&& buffer[i + 3] == lf_char {
body_start = i + 4
// The header fields slice covers everything from header_start
// up to (but not including) the final double CRLF
req.header_fields = Slice{
start: header_start
len: i - header_start
}
break
}
}
if body_start != -1 {
req.body = Slice{
start: body_start
len: buffer.len - body_start
}
} else {
// If no body delimiter found, assume headers go to end or body is missing
req.header_fields = Slice{header_start, buffer.len - header_start - 2}
req.body = Slice{0, 0}
}
return req
}
// Helper function to convert Slice to string for debugging
fn (slice Slice) to_string(buffer []u8) string {
if slice.len <= 0 {
return ''
}
return buffer[slice.start..slice.start + slice.len].bytestr()
}
@[direct_array_access]
fn find_header_end_in_buf(buf &u8, buf_len int) int {
for i := 0; i < buf_len - 1; i++ {
unsafe {
if buf[i] == `\n` {
if i + 1 < buf_len && buf[i + 1] == `\n` {
return i + 2
}
if i + 2 < buf_len && buf[i + 1] == `\r` && buf[i + 2] == `\n` {
return i + 3
}
}
if i + 3 < buf_len && buf[i] == `\r` && buf[i + 1] == `\n` && buf[i + 2] == `\r`
&& buf[i + 3] == `\n` {
return i + 4
}
}
}
return -1
}
// has_complete_body checks if a raw HTTP request buffer contains the full body
// as indicated by the Content-Length or Transfer-Encoding headers. Returns true if:
// - there is no Content-Length header and no chunked encoding (body not expected)
// - Content-Length is 0
// - enough body bytes have been received
// - chunked encoding is complete (the zero-size chunk and trailers were parsed)
// Returns false only when more body data is expected.
@[direct_array_access]
fn has_complete_body(buf &u8, buf_len int) bool {
header_end := find_header_end_in_buf(buf, buf_len)
if header_end < 0 {
return false // headers not complete yet
}
// Check for Transfer-Encoding: chunked header (case-insensitive)
if has_chunked_transfer_encoding_in_buf(buf, header_end) {
return has_complete_chunked_body(buf, buf_len, header_end)
}
content_length := parse_content_length_from_buf(buf, header_end)
if content_length <= 0 {
return true // no content-length or zero: body complete
}
body_received := buf_len - header_end
return body_received >= content_length
}
@[direct_array_access]
fn has_complete_chunked_body(buf &u8, buf_len int, body_start int) bool {
mut pos := body_start
for {
lf_pos := find_line_lf_in_buf(buf, buf_len, pos)
if lf_pos < 0 {
return false
}
mut line_end := lf_pos
unsafe {
if line_end > pos && buf[line_end - 1] == `\r` {
line_end--
}
}
mut size_end := line_end
for i := pos; i < line_end; i++ {
unsafe {
if buf[i] == `;` {
size_end = i
break
}
}
}
mut size_start := pos
for size_start < size_end {
unsafe {
if buf[size_start] != ` ` && buf[size_start] != `\t` {
break
}
}
size_start++
}
for size_end > size_start {
unsafe {
if buf[size_end - 1] != ` ` && buf[size_end - 1] != `\t` {
break
}
}
size_end--
}
if size_start == size_end {
return true
}
mut chunk_size := 0
for i := size_start; i < size_end; i++ {
digit := chunked_hex_digit_value(unsafe { buf[i] })
if digit < 0 {
return true
}
if chunk_size > (max_int - digit) / 16 {
return true
}
chunk_size = chunk_size * 16 + digit
}
pos = lf_pos + 1
if chunk_size == 0 {
return has_complete_chunked_trailers(buf, buf_len, pos)
}
if chunk_size > buf_len - pos {
return false
}
data_end := pos + chunk_size
if data_end + 2 > buf_len {
return false
}
unsafe {
if buf[data_end] != `\r` || buf[data_end + 1] != `\n` {
return true
}
}
pos = data_end + 2
}
return false
}
@[direct_array_access]
fn has_complete_chunked_trailers(buf &u8, buf_len int, start int) bool {
mut pos := start
for {
lf_pos := find_line_lf_in_buf(buf, buf_len, pos)
if lf_pos < 0 {
return false
}
mut line_end := lf_pos
unsafe {
if line_end > pos && buf[line_end - 1] == `\r` {
line_end--
}
}
if line_end == pos {
return true
}
pos = lf_pos + 1
}
return false
}
@[direct_array_access]
fn find_line_lf_in_buf(buf &u8, buf_len int, start int) int {
for i := start; i < buf_len; i++ {
unsafe {
if buf[i] == `\n` {
return i
}
}
}
return -1
}
fn chunked_hex_digit_value(ch u8) int {
if ch >= `0` && ch <= `9` {
return int(ch - `0`)
}
if ch >= `a` && ch <= `f` {
return int(ch - `a` + 10)
}
if ch >= `A` && ch <= `F` {
return int(ch - `A` + 10)
}
return -1
}
// has_chunked_transfer_encoding_in_buf scans the header bytes for a
// "Transfer-Encoding:" header whose value contains "chunked" (case-insensitive).
@[direct_array_access]
fn has_chunked_transfer_encoding_in_buf(buf &u8, header_end int) bool {
te_lower := 'transfer-encoding:'
for i := 0; i < header_end - te_lower.len; i++ {
unsafe {
if buf[i] != `\n` {
continue
}
pos := i + 1
if pos + te_lower.len > header_end {
continue
}
mut matched := true
for j := 0; j < te_lower.len; j++ {
mut ch := buf[pos + j]
if ch >= `A` && ch <= `Z` {
ch = ch + 32
}
if ch != te_lower[j] {
matched = false
break
}
}
if matched {
chunked_str := 'chunked'
for val_start := pos + te_lower.len; val_start < header_end - chunked_str.len; val_start++ {
if buf[val_start] == `\r` || buf[val_start] == `\n` {
break
}
mut cmatch := true
for k := 0; k < chunked_str.len; k++ {
mut ch2 := buf[val_start + k]
if ch2 >= `A` && ch2 <= `Z` {
ch2 = ch2 + 32
}
if ch2 != chunked_str[k] {
cmatch = false
break
}
}
if cmatch {
return true
}
}
}
}
}
return false
}
// parse_content_length_from_buf scans the header bytes for a Content-Length header
// and returns its integer value, or -1 if not found.
@[direct_array_access]
fn parse_content_length_from_buf(buf &u8, header_end int) int {
cl_lower := 'content-length:'
for i := 0; i < header_end - cl_lower.len; i++ {
unsafe {
if buf[i] != `\n` {
continue
}
pos := i + 1
if pos + cl_lower.len > header_end {
continue
}
mut matched := true
for j := 0; j < cl_lower.len; j++ {
mut ch := buf[pos + j]
if ch >= `A` && ch <= `Z` {
ch = ch + 32
}
if ch != cl_lower[j] {
matched = false
break
}
}
if matched {
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++
}
return val
}
}
}
return -1
}
+127
View File
@@ -0,0 +1,127 @@
module fasthttp
fn test_parse_http1_request_line_valid_request() {
buffer := 'GET /path/to/resource HTTP/1.1\r\n'.bytes()
mut req := HttpRequest{
buffer: buffer
}
parse_http1_request_line(mut req) or { panic(err) }
assert req.method.to_string(req.buffer) == 'GET'
assert req.path.to_string(req.buffer) == '/path/to/resource'
assert req.version.to_string(req.buffer) == 'HTTP/1.1'
}
fn test_parse_http1_request_line_invalid_request() {
buffer := 'INVALID REQUEST LINE'.bytes()
mut req := HttpRequest{
buffer: buffer
}
mut has_error := false
parse_http1_request_line(mut req) or {
has_error = true
assert err.msg() == 'Invalid HTTP request line: Missing CR'
}
assert has_error, 'Expected error for invalid request line'
}
fn test_decode_http_request_valid_request() {
buffer := 'POST /api/resource HTTP/1.0\r\n'.bytes()
req := decode_http_request(buffer) or { panic(err) }
assert req.method.to_string(req.buffer) == 'POST'
assert req.path.to_string(req.buffer) == '/api/resource'
assert req.version.to_string(req.buffer) == 'HTTP/1.0'
}
fn test_decode_http_request_invalid_request() {
buffer := 'INVALID REQUEST LINE'.bytes()
mut has_error := false
decode_http_request(buffer) or {
has_error = true
assert err.msg() == 'Invalid HTTP request line: Missing CR'
}
assert has_error, 'Expected error for invalid request'
}
fn test_decode_http_request_with_headers_and_body() {
raw := 'POST /submit HTTP/1.1\r\n' + 'Host: localhost\r\n' +
'Content-Type: application/json\r\n' + 'Content-Length: 18\r\n' + '\r\n' +
'{"status": "ok"}'
buffer := raw.bytes()
req := decode_http_request(buffer) or { panic(err) }
assert req.method.to_string(req.buffer) == 'POST'
assert req.path.to_string(req.buffer) == '/submit'
// Verify Header Fields block
// Should contain everything between the first \r\n and the \r\n\r\n
header_str := req.header_fields.to_string(req.buffer)
assert header_str == 'Host: localhost\r\nContent-Type: application/json\r\nContent-Length: 18'
// Verify Body
assert req.body.to_string(req.buffer) == '{"status": "ok"}'
}
fn test_decode_http_request_no_body() {
// A GET request usually ends with \r\n\r\n and no body
buffer := 'GET /index.html HTTP/1.1\r\nUser-Agent: V\r\n\r\n'.bytes()
req := decode_http_request(buffer) or { panic(err) }
assert req.header_fields.to_string(req.buffer) == 'User-Agent: V'
assert req.body.len == 0
}
fn test_decode_http_request_malformed_no_double_crlf() {
// Request that never finishes headers
buffer := 'GET / HTTP/1.1\r\nHost: example.com\r\n'.bytes()
req := decode_http_request(buffer) or { panic(err) }
// Based on our implementation, if no \r\n\r\n is found,
// body should be empty and headers go to the end.
assert req.body.len == 0
assert req.header_fields.to_string(req.buffer) == 'Host: example.com'
}
fn test_has_complete_body_without_body() {
buffer := 'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n'.bytes()
assert has_complete_body(buffer.data, buffer.len)
}
fn test_has_complete_body_with_incomplete_content_length() {
buffer := 'POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n123'.bytes()
assert !has_complete_body(buffer.data, buffer.len)
}
fn test_has_complete_body_with_complete_content_length() {
buffer := 'POST /upload HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\n12345'.bytes()
assert has_complete_body(buffer.data, buffer.len)
}
fn test_has_complete_body_with_incomplete_chunked_body() {
buffer :=
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n'.bytes()
assert !has_complete_body(buffer.data, buffer.len)
}
fn test_has_complete_body_with_complete_chunked_body() {
buffer :=
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n'.bytes()
assert has_complete_body(buffer.data, buffer.len)
}
fn test_has_complete_body_with_incomplete_chunk_data_containing_terminator_bytes() {
buffer :=
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n20\r\nabc\r\n0\r\n\r\n'.bytes()
assert !has_complete_body(buffer.data, buffer.len)
}
fn test_has_complete_body_with_complete_chunk_data_containing_terminator_bytes() {
buffer :=
'POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\nd\r\nabc\r\n0\r\n\r\ndef\r\n0\r\n\r\n'.bytes()
assert has_complete_body(buffer.data, buffer.len)
}