Initial project import
This commit is contained in:
commit
23f2d29e32
11957 files changed
+1754577
No files matched your search
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user