NAME
Feersum - A fast PSGI/HTTP server for Perl based on EV/libev
SYNOPSIS
use Feersum;
use EV;
use IO::Socket::INET;
my $io_socket = IO::Socket::INET->new(
LocalAddr => 'localhost:5000',
Proto => 'tcp',
Listen => 1024,
Blocking => 0, # optional: use_socket sets O_NONBLOCK anyway
) or die $!;
my $ngn = Feersum->endjinn; # singleton
$ngn->use_socket($io_socket);
# register a PSGI handler
$ngn->psgi_request_handler(sub {
my $env = shift;
return [200,
['Content-Type'=>'text/plain'],
["You win one cryptosphere!\n"]];
});
# register a Feersum handler:
$ngn->request_handler(sub {
my $req = shift;
my $t; $t = EV::timer 2, 0, sub {
$req->send_response(
200,
['Content-Type' => 'text/plain'],
\"You win one cryptosphere!\n"
);
undef $t;
};
});
EV::run; # nothing is served until the loop runs
See eg/hello.pl for the same thing as a file you can run.
DESCRIPTION
Feersum is an HTTP server built on EV. It fully supports the PSGI 1.1 spec including the psgi.streaming interface and is compatible with Plack. It also has its own "native" interface which is similar in a lot of ways to PSGI, but is not compatible with PSGI or PSGI middleware.
Feersum uses a single-threaded, event-based programming architecture to scale and can handle many concurrent connections efficiently in both CPU and RAM. With built-in TLS 1.3, HTTP/2, SNI, and PROXY protocol support, Feersum can serve directly or behind a reverse proxy.
How It Works
All of the request-parsing and I/O marshalling is done using C or XS code. HTTP parsing is done by picohttpparser, which is the core of HTTP::Parser::XS. The network I/O is done via the libev library. This is made possible by EV::MakeMaker, which allows extension writers to link against the same libev that EV is using. This means that one can write an evented app using EV or AnyEvent from Perl that completely co-operates with the server's event loop.
Since the Perl "app" (handler) is executed in the same thread as the event loop, one need to be careful to not block this thread. Standard techniques include using AnyEvent or EV idle and timer watchers, using Coro to multitask, and using sub-processes to do heavy lifting (e.g. AnyEvent::Worker and AnyEvent::DBI).
Feersum also attempts to do as little copying of data as possible. Feersum uses the low-level writev system call to avoid having to copy data into a buffer. For response data, references to scalars are kept in order to avoid copying the string values (once the data is written to the socket, the reference is dropped and the data is garbage collected).
This zero-copy behaviour carries an obligation for the caller: a scalar handed to Feersum as response body data must not be modified until the response has been transmitted, which happens after the handler returns. Feersum holds a reference to the scalar, not a copy of its bytes, so reassigning it in the meantime corrupts the pending write. Pass a fresh scalar (or a literal or expression, which produces a private temporary) each time. See "Writer methods." in Feersum::Connection::Handle for details and examples.
For even faster results, Feersum can support very simple pre-forking (See feersum, Feersum::Runner or Plack::Handler::Feersum for details).
INTERFACE
There are two handler interfaces for Feersum: The PSGI handler interface and the "Feersum-native" handler interface. The PSGI handler interface is fully PSGI 1.1 compatible, supporting psgi.streaming and psgix.io. The Feersum-native handler interface is "inspired by" PSGI, but does some things differently for speed.
Feersum will use "Transfer-Encoding: chunked" for HTTP/1.1 clients and "Connection: close" streaming as a fallback. Technically "Connection: close" streaming isn't part of the HTTP/1.0 or 1.1 spec, but many browsers and agents support it anyway.
Responses with 1xx, 204, 205, or 304 status codes are sent without a body; any body the handler supplies for those statuses is discarded (RFC 9110).
A response to a HEAD request is also sent without a body, as RFC 9110 section 9.3.2 requires. The handler may return a body exactly as it would for GET: Feersum measures it to produce the same Content-Length the equivalent GET would have carried, then transmits only the headers. (Before 1.507 the body was transmitted, which desynchronised a keepalive connection for any client that correctly reads no body after a HEAD.)
POST/PUT request bodies (including chunked transfer-encoding) are fully buffered before the request callback fires, so read() on psgi.input will never block. The psgix.input.buffered env var is deliberately not set, because the handle cannot rewind; see "psgix.input.buffered".
PSGI interface
Feersum fully supports the PSGI 1.1 spec including psgi.streaming.
Response strings - body parts, streamed write() chunks, header values and the status message, for the PSGI and native interfaces alike - are byte strings. A UTF8-flagged string whose characters are all <= 255 is sent as those bytes (the flagged "caf\xE9" and the plain "caf\xE9" produce identical responses), exactly as the syswrite-based PSGI servers behave; Content-Length counts the same bytes. Characters above 255 are forbidden by the PSGI spec. Feersum does not police that: such a string is transmitted in Perl's internal UTF-8 encoding with self-consistent framing, and the connection survives - but the bytes differ from other servers' behaviour (most die or truncate mid-response), so encode such data yourself before returning it.
See also Plack::Handler::Feersum, which provides a way to use Feersum with plackup and Plack::Runner.
Call psgi_request_handler($app) to register $app as a PSGI handler.
my $app = do $filename;
Feersum->endjinn->psgi_request_handler($app);
The env hash passed in will always have the following keys in addition to dynamic ones:
psgi.version => [1,1],
psgi.nonblocking => 1,
psgi.multithread => '',
psgi.multiprocess => $bool, # true when pre_fork or set_multiprocess($true)
psgi.run_once => '',
psgi.streaming => 1,
psgi.errors => \*STDERR,
SCRIPT_NAME => "",
Feersum adds these extensions (see below for info)
psgix.output.buffered => 1,
psgix.body.scalar_refs => 1,
psgix.output.guard => 1,
psgix.io => \$magical_io_socket,
Note that SCRIPT_NAME is always blank (but defined). PATH_INFO will contain the path part of the requested URI.
PATH_INFO is fully percent-decoded, so sanitise it before using it as a filename. Feersum decodes the escapes and does nothing else to the result:
/a/%2e%2e%2fsecret -> /a/../secret (a live traversal, not collapsed)
/a%2Fb -> /a/b (a separator that was not one)
/a%00b -> /a\0b (an embedded NUL)
The .. is the one to guard: nothing here collapses it, so a route that appends PATH_INFO to a document root escapes that root. %2F matters for the opposite reason - a path that arrives as one segment becomes two, so a rule that counts or splits segments sees a different shape than the client sent. The NUL is the mildest: Perl's own open refuses a pathname containing one (it warns and fails rather than truncating), but the byte still travels into anything else you hand it to - a log line, a database query, an exec, a downstream service.
Match against a whitelist rather than pattern-matching the tail, and reject a decoded path containing a .. segment or a NUL. This leniency is what Starman and the rest of the ecosystem do too - none of them sanitise either, though Starman truncates at a NUL where Feersum keeps the whole string. The raw, undecoded target is always in REQUEST_URI if you would rather decode it yourself, and QUERY_STRING is not decoded, as PSGI requires.
Over HTTP/1.x, Feersum only accepts these request methods: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS. Anything else, CONNECT included, is answered with 405 Method Not Allowed and an Allow header, without the handler ever running, so WebDAV verbs (PROPFIND, MKCOL, LOCK, ...), TRACE, and application-specific methods are not usable on an HTTP/1.x listener. HTTP/2 does not share this restriction and passes every method through to the handler, so the same application can see a method over h2 that it can never see over HTTP/1.1. Other PSGI servers generally pass unknown methods through; if you are porting an application that relies on that, this is the difference to check first.
psgi.input always contains a valid handle. For a request without a body (e.g. GET), reads return 0 (end of file) - both read($buf, $env->{CONTENT_LENGTH}) with CONTENT_LENGTH 0 and a read requesting a positive length. The undef/EAGAIN result only occurs in streaming-input mode (after $input->poll_cb(...)) when no body data has arrived yet.
my $r = delete $env->{'psgi.input'};
$r->read($body, $env->{CONTENT_LENGTH});
# optional: choose to stop receiving further input, discard buffers:
$r->close();
The handle also implements getline/getlines and overloads the diamond operator (honouring $/, including local $/; <$fh> slurps), so code that reads psgi.input the filehandle way works; see "Reader methods" in Feersum::Connection::Handle.
read() appends to the buffer, it does not replace it. Perl's built-in read, and every PSGI server whose psgi.input is a real filehandle, overwrite the scalar on each call; Feersum concatenates onto whatever is already there. Reading a 3000 byte body 1024 bytes at a time leaves the buffer holding 1024, then 2048, then 3000 bytes, not 1024/1024/952. This is deliberate and long-standing (it makes the read-it-all-in-one-call form above free), but it means the common portable idiom silently duplicates data here:
my $buf;
while (my $n = $input->read($buf, 8192)) { $body .= $buf } # WRONG here
Either read the whole body in one call, or use a fresh lexical per iteration, or clear the buffer yourself:
$input->read(my $body, $env->{CONTENT_LENGTH}); # simplest
while (my $n = $input->read(my $chunk, 8192)) { $body .= $chunk }
while (my $n = do { $buf = q{}; $input->read($buf, 8192) }) { $body .= $buf }
Plack::Request and the usual body parsers declare the buffer inside the loop, so they are unaffected.
The psgi.streaming interface is fully supported, including the writer-object poll_cb callback feature. Feersum calls the poll_cb callback after all data has been flushed out and the socket is write-ready. The data is buffered until the callback returns at which point it will be immediately flushed to the socket.
my $app = sub {
my $env = shift;
return sub {
my $respond = shift;
my $w = $respond->([
200, ['Content-Type' => 'application/json']
]);
my $n = 0;
# Keep $w alive - a poll_cb registration does not hold it open:
$KEEP{0+$w} = $w;
$w->poll_cb(sub {
$_[0]->write(get_next_chunk());
# Key the stash off $w: $_[0] is a fresh handle for this one
# invocation, so 0+$_[0] never matches what was stored.
# will also unset the poll_cb:
if ($n++ >= 100) { delete $KEEP{0+$w}; $_[0]->close }
});
};
};
Note that $w->close() will be called when the last reference to the writer is dropped. Registering a poll_cb does not count as a reference, so a writer you do not store anywhere is closed as soon as the handler returns: the callback never fires and the client receives an empty body.
A client that goes away mid-stream is noticed on the next write to it, not before, so a long-lived stream (SSE, long-poll) that pushes nothing keeps its connection and its active_conns slot until the app writes again. A periodic heartbeat both detects the departure and releases the slot.
Treat that heartbeat as mandatory if you also set max_connections. Dead streams are not idle keepalive connections, so they are not eligible for the oldest-idle eviction that normally makes room; once enough of them accumulate to reach the cap, the server stops serving entirely and cannot recover, because the only thing that would reap them is a write from the application. write_timeout does not help here: with the response already flushed there is nothing pending to write, so its timer never arms.
PSGI extensions
- psgix.body.scalar_refs
-
Scalar refs in the response body are supported, and is indicated as an via the psgix.body.scalar_refs env variable. Passing by reference is significantly faster than copying a value onto the return stack or into an array. It's also very useful when broadcasting a message to many connected clients. This is a Feersum-native feature exposed to PSGI apps; very few other PSGI handlers will support this.
- psgix.output.buffered
-
Calls to
$w->write()will never block. This behaviour is indicated by psgix.output.buffered in the PSGI env hash. - psgix.input.buffered
-
Not set (since 1.507). Feersum does buffer the entire request body in memory before the handler runs, so
read()onpsgi.inputnever blocks - but the handle is forward-only: reads consume the buffer, andseekcannot rewind.psgix.input.bufferedpromises more than non-blocking reads: per PSGI 1.1 a true value means the handle implementsseek, and consumers rely on that - Plack::Request and HTTP::Entity::Parser rewind withseek(0,0)around their reads because of the flag. Advertising it madePlack::Request->contentreturn''once->body_parametershad consumed the buffer. With the flag unset they buffer the body into their own rewindable handle, and every read ordering works.Feersum also supports a
poll_cb()method on the reader handle. On a normal HTTP/1.x or HTTP/2 request the handler runs only after the whole body has been received, so the callback drains an already-complete buffer rather than seeing data arrive. It becomes a true incremental reader only once the application takes over the byte stream withio()/psgix.io(a WebSocket-style upgrade), where each socket read invokes it. - psgix.output.guard
-
The streaming responder has a
response_guard()method that can be used to attach a guard to the request. When the request completes (all data has been written to the socket and the connection has started its close) the guard will trigger. This is an alternate means to doing a "write completion" callback viapoll_cb()that should be more efficient. An analogy is the "on_drain" handler in AnyEvent::Handle.A "guard" in this context is some object that will do something interesting in its DESTROY/DEMOLISH method. For example, Guard.
- psgix.io
-
The raw socket extension psgix.io is provided in order to support Web::Hippie and websockets.
psgix.iois defined as part of PSGI 1.1. To obtain the IO::Socket corresponding to this connection, read this environment variable.Reading this key hands the connection to your application. The value carries get-magic: fetching it takes over the socket, after which Feersum will not send a response of its own, and an app that returns a normal PSGI response anyway gets it refused (reported through
Feersum::DIED) and the client receives nothing. That is deliberate - once you own the socket, Feersum must not write to it behind your back.The consequence is that anything reading every value in
%$envtakes the socket as a side effect:my %copy = %$env,values %$env, andData::Dumper::Dumper($env)all trip it, and the request then produces an empty response. Inspectingkeysor usingexistsis safe; so is reading any other key. If you need to copy or dump the environment - some logging and debug middleware does - either exclude this key, or turn the extension off with$server->set_psgix_io(0)(psgix_io => 0in Feersum::Runner), which removes it from%$enventirely.For plain (non-TLS) connections the returned IO::Socket::INET wraps the raw socket file descriptor (TCP or Unix domain), which has
O_NONBLOCKandFD_CLOEXECset and (for TCP)TCP_NODELAYenabled. For TLS connections, and for HTTP/2 Extended CONNECT (RFC 8441) streams,psgix.ioreturns a Unix socketpair that relays data through the TLS/H2 layer transparently. On a regular (non-tunnel) HTTP/2 streampsgix.ioisundef(the nativeio()method croaks in the same case; see "$req->io" in Feersum::Connection).That difference shows in one place worth knowing about. If you take the socket while part of the request body is still unread, the bytes Feersum has already buffered are pushed back into the handle - and on a plain connection that push-back goes through the PerlIO layer, which
sysreadbypasses: a bufferedreadorgetlinesees them,sysreadreturns nothing. Over TLS and on an HTTP/2 tunnel the same bytes are written onto the socketpair as real bytes and every read style sees them. Nothing is lost either way, but an app that takes over mid-body and only eversysreads should drainpsgi.inputfirst, or do one buffered read before switching styles.PSGI apps MUST use a
psgi.streamingresponse so that Feersum doesn't try to flush and close the connection. For HTTP/1 connections, the "respond" parameter to the streaming callback MUST NOT be called for the same reason. For HTTP/2 Extended CONNECT, calling the responder with a200response is the correct way to accept the tunnel.my $env = shift; return sub { my $fh = $env->{'psgix.io'}; syswrite $fh, "HTTP/1.1 101 Switching Protocols\r\n" . "Upgrade: myproto\r\nConnection: Upgrade\r\n\r\n"; # ... bidirectional I/O on $fh ... };HTTP/2 note: For H2 Extended CONNECT tunnels, Feersum automatically sends 200 HEADERS to accept the tunnel and silently swallows the HTTP/1.1 101 response written by the app. This means the same handler code works for both H1 and H2 without branching. See "HTTP/2 Support" for details.
- psgix.h2.trailers
-
An array-ref of alternating
name, valueentries (a flat list, not nested pairs) containing HTTP/2 trailer headers received with the request. Only present for HTTP/2 requests that included trailers. Absent for HTTP/1.x requests and H2 requests without trailers. - psgix.h2.extended_connect
-
Set to
1on HTTP/2 Extended CONNECT streams (RFC 8441). Absent for all other request types including plain HTTP/2 requests. - psgix.h2.protocol
-
Present on HTTP/2 Extended CONNECT streams. Contains the value of the H2
:protocolpseudo-header (e.g."websocket"). - psgix.proxy_tlvs
-
Present when the connection arrived via PROXY protocol v2 with TLV extensions. A hash ref mapping TLV type numbers to their raw values. See "my $tlvs = $req->proxy_tlvs" in Feersum::Connection for details.
The protocol permits a type to repeat, but a hash cannot: the last occurrence wins and earlier ones are not visible. That matters if you validate one TLV against another, since some senders emit repeated sub-TLVs.
The Feersum-native interface
The Feersum-native interface is inspired by PSGI, but is inherently incompatible with it. Apps written against this API will not work as a PSGI app.
The native interface has been stable since version 1.0; it will only change for bug fixes or backwards-compatible additions until at least the next major release.
The main entry point is a sub-ref passed to request_handler. This sub is passed a reference to an object that represents an HTTP connection. Currently the request_handler is called during the "check" and "idle" phases of the EV event loop. The handler is always called after request headers have been read. Currently, the handler will only be called after a full request entity has been received for POST/PUT/etc.
The simplest way to send a response is to use send_response:
my $req = shift;
$req->send_response(200, \@headers, ["body ", \"parts"]);
Or, if the app has everything packed into a single scalar already, just pass it in by reference.
my $req = shift;
$req->send_response(200, \@headers, \"whole body");
Both of the above will generate Content-Length header (replacing any that were pre-defined in @headers). Response strings are bytes; see the note under "PSGI interface" for how UTF8-flagged strings are handled.
An environment hash is easy to obtain, but is a method call instead of a parameter to the callback. (In PSGI, there is no $req object; the env hash is the first parameter to the callback). The hash contains the same items as it would for a PSGI handler (see above for those), except psgix.io, which is only added for PSGI handlers; from a native handler use $req->io() instead.
my $req = shift;
my $env = $req->env();
To read input from a POST/PUT, use the psgi.input item of the env hash.
if ($env->{REQUEST_METHOD} eq 'POST') {
my $body = '';
my $r = delete $env->{'psgi.input'};
$r->read($body, $env->{CONTENT_LENGTH});
# or line-wise: my $line = $r->getline; # also <$r>, honouring $/
# optional:
$r->close();
}
Starting a response in stream mode enables the write() method (which really acts more like a buffered 'print'). Calls to write() will never block.
my $req = shift;
my $w = $req->start_streaming(200, \@headers);
$w->write(\"this is a reference to some shared chunk\n");
$w->write("regular scalars are OK too\n");
$w->close(); # close off the stream
The writer object supports poll_cb as specified in PSGI. Feersum will call the callback only when all data has been flushed out at the socket level. Use close() or unset the handler ($w->poll_cb(undef)) to stop the callback from getting called.
my $req = shift;
my $w = $req->start_streaming(
"200 OK", ['Content-Type' => 'application/json']);
my $n = 0;
# Keep $w alive: registering a poll_cb does NOT hold the writer open, and
# dropping the last reference closes the response. See below.
$KEEP{0+$w} = $w;
$w->poll_cb(sub {
# $_[0] is a fresh handle for THIS invocation, not $w itself:
# key the stash off $w, which the closure already captures.
$_[0]->write(get_next_chunk());
if ($n++ >= 100) { delete $KEEP{0+$w}; $_[0]->close }
});
You must keep a reference to the writer alive for as long as you intend to stream. $w->close() is called when the last reference is dropped, so a writer that only the poll_cb registration refers to is closed as soon as the handler returns - the callback never fires and the client gets an empty body. Store it somewhere that outlives the handler (as above) and drop it when done.
poll_cb is also the only backpressured way to stream. It fires when the socket has drained, so an endless source paces itself against the client (see wbuf_low_water). A bare $w->write() loop does not: it buffers whatever you give it, so an app that produces independently of the client - SSE, a log tail - can grow unboundedly against a peer that stops reading but keeps the connection alive. Use poll_cb for any source you do not control.
On Linux the writer also supports zero-copy file responses via $w->sendfile($fh [, $offset, $length]) (not available for HTTP/2 streams); see "$w->sendfile($fh [, $offset, $length])" in Feersum::Connection::Handle.
METHODS
These are methods on the Feersum server object.
new()endjinn()-
Returns the
Feersumsingleton. Takes no parameters. new_instance()-
Creates a new independent Feersum server instance. Unlike
new(), each call returns a separate server object with its own listeners, configuration, and request handler. Use this when you need multiple independent servers in the same process.my $http = Feersum->new_instance(); my $https = Feersum->new_instance();Create instances at startup and keep them. A server object is retained for the life of the process even after you drop your reference to it (its ev watchers and any live connections point into it), so calling this repeatedly in a loop accumulates memory.
Dropping the reference does not stop the server: it goes on accepting and serving on its listeners, and you no longer hold a handle with which to stop it. Call
unlisten()orgraceful_shutdown()before letting an instance go out of scope.See
eg/multi-instance.plfor two instances with separate handlers. use_socket($sock)-
Use the file-descriptor attached to a listen-socket to accept connections.
Note: Pre-encrypted sockets (e.g. IO::Socket::SSL) are not supported. Feersum operates on the raw file descriptor and will ignore any userspace encryption layer. To enable TLS, use
set_tls()after adding the socket; Feersum handles encryption internally via picotls.A reference to
$sockis kept internally to prevent garbage collection. Registering the same socket twice does not add a second reference.The listening descriptor must be non-blocking. Feersum accepts several connections per event-loop iteration, so on a blocking listener the
accept()that follows the last pending connection blocks and wedges the entire loop. Feersum setsO_NONBLOCKon the descriptor for you, but a socket you continue to use elsewhere is affected by that change. accept_on_fd($fileno)-
Use the specified fileno to accept connections. May be used as an alternative to
use_socket. The same non-blocking requirement applies. unlisten()-
Stop listening on all sockets previously added via
use_socket()oraccept_on_fd(): the accept watchers are stopped and the listener slots freed. The underlying socket file descriptors are not closed -- they stay open until the Perl socket objects are garbage-collected, so they can be handed toaccept_on_fd()again (this is how pre-fork worker respawn re-arms accept). To actually close the listen fds, usegraceful_shutdown().A connection reads
SERVER_NAMEandSERVER_PORTfrom its listener slot at request time, not at accept time. If a keep-alive connection is still open when you unlisten and then listen on a different socket, the freed slot is reused and that connection's later requests report the new listener's name and port. The respawn pattern above re-registers the same sockets, so nothing changes there; if you are swapping in different ones, drain first. pause_accept()-
Temporarily stop accepting new connections. Existing connections continue to be processed. Returns true if any listener was newly paused, false if all listeners were already paused or during shutdown.
Useful for load shedding or controlled traffic management.
resume_accept()-
Resume accepting new connections after a pause_accept() call. Returns true if any listener was resumed, false if none were user-paused or during shutdown.
accept_is_paused()-
Returns true if accepting is currently paused on all listeners, false otherwise. With multiple listen sockets, all must be paused for this to return true.
request_handler(sub { my $req = shift; ... })-
Sets the global request handler. Any previous handler is replaced.
The handler callback is passed a Feersum::Connection object.
Subject to change: if the request has an entity body then the handler will be called only after receiving the body in its entirety. The body may use Content-Length or chunked Transfer-Encoding. The maximum size defaults to 67108864 bytes and can be changed via
max_body_len(). psgi_request_handler(sub { my $env = shift; ... })-
Like request_handler, but assigns a PSGI handler instead.
read_timeout()read_timeout($duration)-
Get or set the global read timeout. Default is 5 seconds. Must be a positive non-zero value; passing 0 or a negative value will croak. Changes take effect for new connections only; existing connections retain the timeout they were accepted with.
Feersum will wait about this long to receive all headers of a request (within the tolerances provided by libev). If an entity body is part of the request (e.g. POST or PUT) it will wait this long between successful
read()system calls. This timeout also serves as the keepalive idle timeout between requests on persistent connections; there is no separate setting for that.On HTTP/2 it is the only idle watchdog the connection has, and it counts silence in either direction: a client that stops sending WINDOW_UPDATEs mid-download, and so stops sending anything at all, is reaped after this long even though a response is still in flight. Real clients acknowledge data as they consume it, so this reaps a stalled peer rather than a slow one.
header_timeout()header_timeout($seconds)-
Get or set the header completion deadline timeout (Slowloris protection). Default is 10 seconds; pass
0to disable it.When enabled, connections must complete sending all HTTP headers within this many seconds from connection acceptance or receive a 408 Request Timeout response. For TLS connections where the handshake has not yet completed, the connection is silently closed (no HTTP response can be sent before the handshake finishes). This is a hard deadline that does not reset when data arrives, unlike
read_timeoutwhich resets on each successful read.This provides protection against Slowloris-style attacks where malicious clients send headers very slowly to exhaust server connection resources.
It bounds the header phase only. Once the headers are complete the deadline is dropped and the request body is governed by
read_timeout, which resets on every successful read - so a client trickling one body byte perread_timeoutholds its connection open indefinitely. This matches nginx'sclient_body_timeout, but note the interaction withmax_connections: a connection stalled mid-body is never eligible for the idle-keepalive eviction that reclaims capacity, so enough slow-body clients can occupy the whole budget. If you accept requests from untrusted clients, put a reverse proxy in front (which buffers the body) rather than relying onmax_connectionsalone, and consider settingwrite_timeout, which is disabled by default and is the matching deadline for the response phase.Unlike the other per-connection tunables, this one is read live rather than snapshotted at accept: the deadline is re-armed at the start of each request, so lowering it under attack takes effect on connections that are already open, including for the next request on a keepalive connection.
Recommended value for direct internet exposure: 30-60 seconds. When running behind a reverse proxy (nginx, HAProxy), this can typically be left disabled since the proxy handles slow clients.
graceful_shutdown(sub { .... })-
Causes Feersum to initiate a graceful shutdown of all outstanding connections. No new connections will be accepted. All listen socket file descriptors are closed; the Perl socket objects are not freed but the underlying fds are invalid after this call.
graceful_shutdownis intended to be terminal: because the retained Perl socket objects still hold the (now-closed) fds, they will close those fd numbers again when the server object is eventually destroyed. The process should exit once the completion callback fires (asFeersum::Runnerdoes viaPOSIX::_exit) rather than continue running and reuse the freed fd numbers.The sub parameter is a completion callback. It will be called when all connections have been flushed and closed. This allows one to do something like this:
my $cv = AE::cv; my $death = AE::timer 2.5, 0, sub { fail "SHUTDOWN TOOK TOO LONG"; exit 1; }; Feersum->endjinn->graceful_shutdown(sub { pass "all gracefully shut down, supposedly"; undef $death; $cv->send; }); $cv->recv;The
$deathtimer in that example is not decoration. "All connections" includes ones Feersum no longer times out: a socket taken over throughpsgix.ioon HTTP/1.x or TLS, and an established RFC 8441 HTTP/2 tunnel. Those are exempt from the read and write timeouts on purpose, because a websocket may legitimately sit idle for hours, so nothing will ever reap them and the completion callback will not fire while one is open. A caller that shuts down on its own must impose its own deadline, as the example does. Feersum::Runner already does this viagraceful_timeout(default 5 seconds), after which it force-exits. DIED-
Not really a method so much as a static function. Works similar to EV's/AnyEvent's error handler.
The error is passed as
$_[0].$@is not set: the handler is invoked underG_EVAL, which clears it.The default implementation warns the error to STDERR. To install a custom handler:
no strict 'refs'; *{'Feersum::DIED'} = sub { warn "Error: $_[0]" };A handler must not throw - see the note below - so
Carp::confessis not usable here; useCarp::cluckif you want a stack trace.Will get called for any errors that happen before the request handler callback is called, when the request handler callback throws an exception and potentially for other not-in-a-request-context errors.
It will not get called for read timeouts or header deadline timeouts (Slowloris protection) that occur while waiting for a complete header, nor for timeouts while waiting for a request entity body.
Note: Any exceptions thrown by the DIED handler itself are caught, discarded and not reported anywhere (the handler is called with G_EVAL). A handler that dies is therefore silent. The server will still respond with a 500 error to the client.
The 500 can only be sent while the response has not started. A handler (or PSGI streaming callback) that dies mid-stream instead has its response sealed so the client can detect the truncation: HTTP/1.x closes the connection without the terminating chunk, HTTP/2 sends
RST_STREAM. A response explicitly completed with$w->closebefore the die is delivered intact; merely letting the writer go out of scope does not count as completion when the same callback then dies, since that drop is exactly what an exception unwind looks like. set_server_name_and_port($host,$port)-
Override Feersum's notion of what SERVER_NAME and SERVER_PORT should be. Applies to the most recently added listener only; call it after each
use_socket()/accept_on_fd()when running multiple listeners. (use_socket()already calls this implicitly with values derived from the socket.) get_keepalive()set_keepalive($bool)-
Enable or disable keepalive for new connections. Default is disabled. When enabled, HTTP/1.1 connections without an explicit
Connection: closeheader will be kept alive between requests. Changes take effect for new connections only.Returns the current setting (a boolean) when called as
get_keepalive(). get_drain_accept_queue()set_drain_accept_queue($bool)-
When enabled,
graceful_shutdown()accepts whatever connections the kernel has already queued on each TCP listen socket and serves them as part of the drain, before closing the listener. Default is disabled.Enable this when the listen socket is owned by this process alone - a
SO_REUSEPORTsocket, most notably - because such a socket's accept queue dies with it: the queued clients completed their TCP handshake and would be reset having sent nothing wrong. Feersum::Runner enables this automatically for reuseport workers. Leave it off for a listen socket shared with other processes (the pre-fork inheritance model), where draining would steal connections a live sibling is about to serve and lengthen this process's shutdown for no benefit.Accepted connections respect
max_connectionsand count toward the drain thatgraceful_shutdown()waits out. UNIX-domain listeners are never drained (under Feersum::Runner they are shared even in reuseport mode).Enabling this also turns
TCP_DEFER_ACCEPToff on the server's TCP listeners, current and future: a deferred connection - established, but its first data not yet arrived - is invisible toaccept(), so the shutdown drain would otherwise close the listener on top of it.Returns the current setting (a boolean) when called as
get_drain_accept_queue(). set_reverse_proxy($bool)-
Enable or disable reverse proxy mode. Changes take effect for new connections only. When enabled, Feersum trusts
X-Forwarded-ForandX-Forwarded-Protoheaders from upstream proxies to determine the client's real IP address and request scheme.Security note: Feersum uses the leftmost IP from
X-Forwarded-For, which assumes a single-hop reverse proxy that overwrites (not appends to) the header. If your proxy appends to an existingX-Forwarded-For, clients can spoof their IP by sending a forged header. Ensure your reverse proxy strips or replacesX-Forwarded-Forrather than appending.A second, subtler route: the CGI mapping folds dashes and underscores to the same key, so a client-sent
X_Forwarded_Forlands inHTTP_X_FORWARDED_FORbeside the proxy's realX-Forwarded-For, and if it arrives first it is leftmost. This mode is not fooled by that - it reads the exact header name off the wire, soREMOTE_ADDRstays correct - but an application readingHTTP_X_FORWARDED_FORitself can be. The behaviour is not specific to Feersum (Starman and HTTP::Server::PSGI fold it the same way); the fix is at the proxy, which should drop headers containing underscores, as nginx does by default.When this mode is active,
env()setsREMOTE_ADDRandpsgi.url_schemefrom the forwarded headers, and the Feersum::Connection methodsclient_address()andurl_scheme()return the forwarded values. Note thatremote_address()andremote_port()always report the immediate peer (they are not affected by this mode); useclient_address()for the forwarded client address.Combined with the PROXY protocol: if
proxy_protocolis also enabled, the two sources are used for different parts of the same peer.REMOTE_ADDRcomes fromX-Forwarded-For(this mode wins), whileREMOTE_PORTkeeps the value the PROXY header supplied, so the pair does not describe one endpoint. The PROXY header is supplied by the upstream connection itself and cannot be forged by the client, whereasX-Forwarded-Forcan if your proxy appends rather than replaces. If you have a PROXY-protocol upstream, prefer it alone and leavereverse_proxyoff. get_reverse_proxy()-
Returns whether reverse proxy mode is currently enabled (1 or 0).
max_connection_reqs()-
Changes take effect for new connections only: the value is cached per connection at accept time.
max_connection_reqs($count)-
Get or set the maximum number of requests allowed per keep-alive connection. Default is 0 (unlimited). When set to a positive value, the connection will be closed after serving that many requests, even if keep-alive is enabled.
This is useful for preventing any single connection from monopolizing server resources and helps with memory management by periodically recycling connections.
HTTP/1.1 only. An HTTP/2 connection is not closed after this many streams - multiplexing many requests over one connection is the point of the protocol. Use
max_h2_concurrent_streams()to bound concurrency there, andmax_connections()to bound connections overall. read_priority()read_priority($priority)-
Get or set the libev watcher priority for read I/O operations. Priority range is -2 (lowest) to +2 (highest), default is 0; out-of-range values are silently clamped. Higher priority watchers are invoked before lower priority ones. Changes take effect for new connections only.
At -2 read watchers tie with Feersum's own dispatch watcher, so a request is handled one event-loop iteration later than it was read, costing an extra syscall per request. Keep this above -2 unless you need it.
write_priority()write_priority($priority)-
Get or set the libev watcher priority for write I/O operations. Priority range is -2 (lowest) to +2 (highest), default is 0; out-of-range values are silently clamped. Changes take effect for new connections only.
accept_priority()accept_priority($priority)-
Get or set the libev watcher priority for accept operations. Priority range is -2 (lowest) to +2 (highest), default is 0; out-of-range values are silently clamped. Changes take effect for listeners added afterwards; existing listeners keep the priority they were set up with.
set_psgix_io($bool)-
Enable or disable the
psgix.ioPSGI extension (default: enabled). When disabled, Feersum skips setting uppsgix.ioin the PSGI environment hash, avoiding the overhead of creating a raw I/O handle for each request.Disable this if your application never uses
psgix.io(WebSocket upgrades, etc.) for a small performance improvement in the PSGI path.Disabling it is also the fix if some middleware in your stack copies or dumps the whole environment hash: reading the
psgix.iovalue takes over the socket, so a full-hash read leaves the request with no response. See "psgix.io". get_psgix_io()-
Returns whether
psgix.iois currently enabled (1 or 0). set_proxy_protocol($bool)-
Enable or disable PROXY protocol support. When enabled, Feersum expects all new connections to begin with a PROXY protocol header (v1 text or v2 binary format, auto-detected) before any HTTP data.
The PROXY protocol is used by load balancers like HAProxy, AWS ELB/NLB, and nginx to pass the original client IP address to backend servers. When a valid PROXY header is received, REMOTE_ADDR and REMOTE_PORT are updated to reflect the client's real address.
Special cases: - PROXY v1 UNKNOWN: Keeps original address (used for health checks) - PROXY v2 LOCAL: Keeps original address (used for health checks) - PROXY v2 with UNSPEC or AF_UNIX address family: keeps original address
Connections without a valid PROXY header will be rejected with HTTP 400.
Only enable this when ALL connections come from a proxy that sends PROXY headers.
See
eg/proxy-protocol.plfor a runnable example. get_proxy_protocol()-
Returns true if PROXY protocol support is enabled, false otherwise.
max_accept_per_loop()max_accept_per_loop($count)-
Get or set the maximum number of connections to accept per event loop iteration. Default is 64.
Limiting accepts per loop prevents a flood of new connections from starving existing connections of CPU time. Lower values provide more fairness between new and existing connections; higher values improve throughput under heavy connection load.
active_conns()-
Returns the current count of active connection objects being handled by Feersum. For HTTP/2, each concurrent stream counts as a separate unit in addition to the underlying TCP connection, so a single H2 connection with N streams contributes N+1 to this count.
total_requests()-
Returns the total number of requests processed since the server started. Useful for monitoring and statistics. The counter is a native unsigned integer (64-bit on 64-bit Perl builds, 32-bit on 32-bit builds).
access_log()access_log($cb)-
Get or set a callback invoked as each response completes, with
($method, $uri, $elapsed_seconds). Passundefto disable it. Returns the current callback (orundef).Only requests that reached the handler are reported: one the server rejects itself - malformed, over a limit, timed out - is answered without dispatching and produces no line. This is a service-time hook, not a CLF access log.
$elapsedis measured from the moment the request was dispatched to the handler until its response is fully flushed, so it is the request's own service time - not, as aresponse_guard-based logger would report under keepalive, the interval until the following request. An exception from the callback is caught and reported as a warning; it does not affect the response, which has already been sent by the time the callback runs.Feersum::Runner's
access_logoption is implemented with this. max_requests_per_worker()max_requests_per_worker($limit, $cb)max_requests_per_worker($limit, $cb, $retiring_cb)-
Retire this process after it has handled
$limitrequests: once the count is reached Feersum stops accepting, closes its listeners, lets in-flight requests drain, and then calls$cb. Setting$limitto 0 disables it (no callback is needed in that case). Returns the current limit.$retiring_cb, if given, is called instead at the moment the limit is reached, just before the drain begins. The drain has no deadline of its own, and this is the only notification that one has started, so a caller that needs to bound it - as Feersum::Runner does withgraceful_timeout- has to arm its timer here. An exception from it is caught and warned about.The check happens in C where the request counter lives, so the limit is exact and costs nothing per request. Feersum::Runner's
max_requests_per_workeroption is implemented with this; a Perl-side watcher either polled (and overshot a limit of 10 by several thousand under load) or had to run a callback on every event-loop iteration.This is per-process, so under
pre_forkit retires one worker, which the supervisor then replaces. max_connections()max_connections($limit)-
Get or set the maximum number of concurrent connections. Default is 10000.
When the limit is reached, Feersum first tries to close the oldest idle keep-alive connection to make room. If no idle connections are available, the new connection is closed immediately after accept() and accepting is paused on that listener until a connection slot frees up. This provides protection against Slowloris-style DoS attacks that attempt to exhaust server resources by holding many connections open.
A connection whose client vanished mid-stream is neither idle keep-alive nor freeing itself, so it can never be evicted to make that room; see "PSGI interface" for why a streaming application needs a heartbeat before this limit is safe to rely on.
Setting this to 0 disables the limit. In production, consider also running Feersum behind a reverse proxy (nginx, HAProxy) which can provide additional connection limiting and rate limiting.
Note: When HTTP/2 is in use, each H2 stream pseudo-connection counts toward
active_conns()in addition to the TCP connection itself, so open streams consume the budget for accepting further TCP connections. Stream creation itself is not checked against this limit - usemax_h2_concurrent_streams()to bound streams. max_read_buf()-
Changes take effect for new connections only: the value is cached per connection at accept time.
max_read_buf($bytes)-
Get or set the maximum read buffer size per connection (default 64 MiB, 67108864 bytes). This limits how large the read buffer can grow during header parsing and chunked body reception. A request that exceeds the limit while still in its header block receives a 431 (the fields are too large, not the payload); a chunked body that outgrows it receives a 413. Set to 0 to reset to the compile-time default.
The limit is approximate, and where it bites depends on the transport. On a plain connection the buffer is checked before it grows, so a request is rejected slightly below the configured value; on TLS the check happens after a record has been decrypted, so a request may be accepted slightly above it. The difference is a few tens of KiB, which is immaterial at the default but worth knowing if you set a very small value. It is not applied to HTTP/2, whose request size is bounded instead by
max_body_len, bymax_h2_concurrent_streams, and by the SETTINGS_MAX_HEADER_LIST_SIZE that Feersum advertises to the peer. max_body_len()-
Changes take effect for new connections only: the value is cached per connection at accept time.
max_body_len($bytes)-
Get or set the maximum request body size (default 64 MiB, 67108864 bytes). This limits
Content-Lengthvalues and cumulative chunked body sizes. Requests that exceed the limit receive a 413 response on both HTTP/1.1 and HTTP/2. An H2 body of unknown length that outgrows the limit mid-stream is reset withENHANCE_YOUR_CALMinstead, there being no response to attach it to. Set to 0 to reset to the compile-time default. max_uri_len()-
Changes take effect for new connections only: the value is cached per connection at accept time.
max_uri_len($bytes)-
Get or set the maximum request URI length (default 8192 bytes). URIs that exceed the limit receive a 414 response on both HTTP/1.1 and HTTP/2. Set to 0 to reset to the compile-time default.
write_timeout()-
Changes take effect for new connections only: the value is cached per connection at accept time.
write_timeout($seconds)-
Get or set the write/response timeout. Default is 0 (disabled).
When enabled, connections that make no write progress within this many seconds are forcibly closed. The timer resets on each successful write. This protects against slow consumers that stall the server by not reading response data. For HTTP/2, the timeout operates per-stream: a stalled stream receives RST_STREAM rather than closing the entire connection. It covers a response held behind the peer's flow-control window, and separately the connection itself when its outbound buffer backs up; both are reaped, and progress on either refreshes the deadline, so a transfer too large to finish within
write_timeoutis not interrupted while it is still moving. HTTP/2 tunnels (RFC 8441 Extended CONNECT) remain exempt, likepsgix.io.On plain and TLS connections alike (including the HTTP/2 session), "progress" includes the peer draining the kernel send buffer, not merely successful writes: at each deadline Feersum asks the kernel (
SIOCOUTQon Linux,SO_NWRITEon macOS,FIONWRITEon FreeBSD/NetBSD; seehas_outq_probe()) whether queued bytes have left since the previous check, and pushes the deadline back if so. Without that probe a client reading steadily but slower than half the socket send buffer per interval would be cut off mid-transfer, because the OS reports writability - and so permits a deadline-refreshing write - only after half the buffer drains. TLS buffers ciphertext in user space on top of that, but flushing it waits on the same writability signal, so the kernel's drain count is the honest progress measure there too. Because ACKs can arrive in MSS-sized clumps (64KB on loopback), a single drain-free interval is not treated as proof of a stall; a peer that drains nothing across three consecutive checks is closed, so a genuinely stuck client still lasts only a small bounded multiple (at most three intervals) ofwrite_timeout. Where no probe exists (OpenBSD) a slow-but-active reader may still be closed once the send buffer stays backed up for a full interval.The corollary is that
write_timeoutbounds a stalled transfer, not a slow one: a client that keeps draining a trickle holds its connection for as long as it cares to, exactly as a trickling request body does underread_timeout. That is the same bargain nginx strikes, and it costs the peer a byte for every byte it holds, but it means enough deliberately slow readers can occupymax_connections. The advice is the same as for slow bodies: put a reverse proxy in front of untrusted clients. Disabled when the application takes over the socket viaio()orpsgix.io, since the application owns the socket for as long as it holds it. Over TLS the deadline does apply again once the application closes its end while data it wrote is still queued, so a peer that stops reading cannot pin that connection open forever.On HTTP/2 the deadline is not cleared when the buffer drains, so it also bounds the gap between an application's own writes. Once a streaming response has called write at all - a zero-length write counts, since the deadline tracks calls and not bytes - it must write again within this many seconds or the stream is reset, even though the client is healthy and reading; HTTP/1.x stops the timer once the data is gone and imposes no such rule. A long-lived HTTP/2 stream that has not written at all is unaffected, so open-and-wait works, but an SSE or long-poll application that pushes an event and then goes quiet needs
write_timeouteither left at 0 or set above its own heartbeat interval. linger_timeout()linger_timeout($seconds)-
Get or set the lingering-close deadline. Default is 5 seconds; set to 0 to disable lingering entirely.
An empty write buffer means the kernel send buffer accepted the response, not that the client received it - megabytes can still be in flight. A plain
close()at that point orphans the socket, and any byte arriving afterwards (a pipelined next request, a speculative write on a connection the client believes is reusable) makes the kernel answer RST, which discards the queued response at both ends. So, like nginx and Apache, a completed HTTP/1.x close-response instead performs a lingering close:shutdown(SHUT_WR)queues FIN behind the buffered response, and the server keeps reading and discarding until the peer closes, 256 KB have been drained, or this many seconds have passed - whichever comes first. Error responses (including 413 replies to a client still uploading) and idle keepalive reaps linger the same way; TLS connections, HTTP/2 connections, write-timeout teardowns, and connections closed during a graceful shutdown or bymax_connectionseviction close immediately as before.A lingering connection holds its file descriptor and its
max_connectionsslot for at most this long, which is why the default is much shorter than nginx's 30 seconds: with keepalive disabled every response is a close-response, so every connection pays it. wbuf_low_water()-
Changes take effect for new connections only: the value is cached per connection at accept time.
wbuf_low_water($bytes)-
Get or set the write buffer low-water-mark. Default is 0 (callback fires only when the buffer is completely empty).
When using streaming responses with
poll_cb, this setting controls when the write callback is invoked. If set to a positive value, the callback fires when the buffered data drops to or below this threshold, allowing the application to keep the write pipe full for better throughput. Works across all transports (plain, TLS, and HTTP/2). get_multiprocess()set_multiprocess($bool)-
Mark this server instance as running in a multi-process configuration. When true,
psgi.multiprocessin the PSGI env hash will be set to true (per the PSGI spec). Feersum::Runner sets this automatically whenpre_forkis enabled.Returns the current setting (a boolean) when called as
get_multiprocess(). max_h2_concurrent_streams()max_h2_concurrent_streams($n)-
Get or set the maximum number of concurrent HTTP/2 streams per connection (default: 100). Sent in the SETTINGS frame during H2 handshake. Lower values reduce per-connection memory usage for WebSocket-heavy workloads; higher values benefit multiplexed API traffic. Requires H2 compiled in.
Values are clamped to the compile-time ceiling
FEER_H2_MAX_CONCURRENT_STREAMS(default 100); requesting more silently saturates at that ceiling. Values below 1 are clamped to 1. set_tls(cert_file => $path, key_file => $path, [listener => $idx])-
Enable TLS 1.3 on a listener. Requires Feersum to be compiled with TLS support (picotls submodule + OpenSSL; see Alien::OpenSSL).
The cert_file should be a PEM-encoded certificate chain, and key_file the corresponding PEM-encoded private key.
The optional
listenerparameter specifies which listener to configure (0-based index, in order ofuse_socket()/accept_on_fd()calls). Defaults to the last-added listener.Call this after
use_socket()oraccept_on_fd()to apply TLS to that listener. Croaks if no listeners have been configured yet or if thelistenerindex is out of range. Different listeners can have different TLS configurations, or some can be plain HTTP while others use TLS.my $ngn = Feersum->endjinn; $ngn->use_socket($tls_socket); $ngn->set_tls(cert_file => 'default.crt', key_file => 'default.key');For virtual hosting with multiple certificates on a single port, add SNI entries after setting the default certificate:
$ngn->set_tls(sni => 'example.com', cert_file => 'ex.crt', key_file => 'ex.key'); $ngn->set_tls(sni => 'other.com', cert_file => 'ot.crt', key_file => 'ot.key');Clients requesting a hostname that matches an SNI entry get that certificate; all others get the default. Matching is case-insensitive. Up to 32 SNI entries per listener.
When TLS is enabled and Alien::nghttp2 was available at build time, HTTP/2 can be enabled by passing
h2 => 1. Without this flag, onlyhttp/1.1is offered during ALPN negotiation:$ngn->set_tls(cert_file => 'server.crt', key_file => 'server.key', h2 => 1);h2 => 1is a listener-wide setting and cannot be combined withsni =>in the same call (each SNI vhost shares the listener's ALPN configuration). Seth2 => 1on the initial default-certificate call and then addsni =>entries without it.Feersum::Runner also accepts
h2 => 1as a top-level option. has_tls()-
Returns true if Feersum was compiled with TLS support (picotls).
has_h2()-
Returns true if Feersum was compiled with HTTP/2 support (nghttp2).
has_outq_probe()-
Returns true if this build can ask the kernel how much of a socket's send queue is still undelivered (
SIOCOUTQ/SO_NWRITE/FIONWRITE), whichwrite_timeoutuses to spare slow-but-draining clients, plain and TLS alike.
GRITTY DETAILS
Compile Time Options
There are a number of constants defined in feersum_core.h. If you change any of these, be sure to note that in any bug reports.
- MAX_HEADERS
-
Defaults to 64. Controls how many headers can be present in an HTTP request.
If a request exceeds this limit the app handler does not run: HTTP/1.1 gets a 431 response - the fields are too large, as for an over-long header name - and HTTP/2 a stream reset.
- MAX_HEADER_NAME_LEN
-
Defaults to 128. Controls how long the name of each header can be.
If a request exceeds this limit the app handler does not run: HTTP/1.1 gets a 431 response, HTTP/2 a stream reset.
- MAX_TRAILER_HEADERS
-
Defaults to 64. Caps how many trailer headers a chunked request (HTTP/1.1) or an HTTP/2 request may carry. Exceeding it is rejected: 400 for HTTP/1.1, stream reset for HTTP/2.
- MAX_CHUNK_COUNT
-
Defaults to 100000. Caps how many chunks a single chunked request body may consist of; bodies with more chunks are rejected with a 400 response. Guards against CPU exhaustion from streams of tiny chunks.
- MAX_PIPELINE_DEPTH
-
Defaults to 15. Bounds how many pipelined HTTP/1.1 requests are dispatched recursively off one read; deeper pipelines are deferred to the next event loop iteration (they still complete, just without unbounded recursion).
- FEER_MAX_LISTENERS
-
Defaults to 16. Maximum number of listen sockets per server instance;
use_socket()/accept_on_fd()croak beyond this. - FEER_MAX_SNI_ENTRIES
-
Defaults to 32. Maximum number of SNI certificate entries per listener.
- MAX_URI_LEN
-
Defaults to 8192. Controls the maximum length of the request URI (including query string).
If a request exceeds this limit the app handler does not run; both HTTP/1.1 and HTTP/2 get a 414 response.
- MAX_BODY_LEN
-
Compile-time default for
max_body_len()(64 MiB). Controls how large the body of a POST/PUT/etc. can be. Usemax_body_len($bytes)to override at runtime.See also "BUGS".
- READ_BUFSZ
- READ_GROW_FACTOR
-
READ_BUFSZ defaults to 4096, READ_GROW_FACTOR 4.
Together, these tune how data is read for a request.
Read buffers start out at READ_BUFSZ bytes. If another read is needed and the remaining free space in the buffer is under READ_BUFSZ bytes then the buffer grows by READ_GROW_FACTOR * READ_BUFSZ bytes. The trade-off with the grow factor is memory usage vs. system calls.
- READ_TIMEOUT
-
Controls read timeout. Default is 5.0 sec. Also used as the keepalive idle timeout (there is no separate keepalive timeout setting).
- FEERSUM_IOMATRIX_SIZE
-
Controls the size of the main write-buffer structure in Feersum. Making this value lower will use slightly less memory per connection at the cost of speed (and vice-versa for raising the value). The effect is most noticeable when your app is making a lot of sparse writes. The default of 64 generally keeps usage under 4k per connection on full 64-bit platforms when you take into account the other connection and request structures.
NOTE: the struct is always
FEERSUM_IOMATRIX_SIZEentries; if your OS defines a smallerIOV_MAX/UIO_MAXIOV(Solaris is 16; Linux and macOS are 1024), Feersum clamps the number of iovecs passed to eachwritev(2)call at runtime rather than changing the struct size. - FEER_H2_MAX_CONCURRENT_STREAMS
-
Default for
max_h2_concurrent_streams()(100). Controls how many HTTP/2 streams a single connection can have open at once. Override at runtime with$server->max_h2_concurrent_streams($n). - FEER_H2_MAX_HEADER_LIST_SIZE
-
Maximum size of the header list per HTTP/2 request (64 KB).
- FEERSUM_STEAL
-
For non-threaded perls, this defaults to enabled.
When enabled, Feersum will "steal" the contents of temporary lexical scalars used for response bodies. The scalars become
undefas a result, but due to them being temps they likely aren't used again anyway. Stealing saves the time and memory needed to make a copy of that scalar, resulting in a mild to moderate performance boost.This egregious hack only extends to non-magical, string,
PADTMPscalars.If it breaks for your new version of perl, please send stash a note (or a pull request!) on github.
Worth noting is that a similar zero-copy effect can be achieved by using the
psgix.body.scalar_refsfeature.
Environment Variables
FEERSUM_FREELIST_MAX-
Read at module BOOT. Caps the per-process freelist size for recycled
feer_req/iomatrixstructs. Default is 32. Set to0to disable struct caching entirely (every request allocates fresh memory). Lower values reduce idle memory at the cost of allocator overhead under load. FEERSUM_MAX_PRE_FORK-
Read by Feersum::Runner at
use-time. Caps thepre_forkoption; asking for more workers than this value will croak. Default is1000. FEERSUM_GRACEFUL_TIMEOUT-
Read by Feersum::Runner when initiating a graceful shutdown; see "graceful_timeout" in Feersum::Runner.
FEERSUM_DEBUG-
When set, Feersum::Runner keeps STDERR attached to the terminal during daemonization (
daemonize => 1) instead of redirecting it to /dev/null. Useful for debugging daemon startup.
HTTP/2 Support
When Feersum is built with TLS (picotls + Alien::OpenSSL) and HTTP/2 (Alien::nghttp2) support, HTTP/2 can be negotiated via ALPN on TLS connections. HTTP/2 is disabled by default and must be explicitly enabled by passing h2 => 1 to set_tls() or to Feersum::Runner.
TLS-only -- cleartext HTTP/2 (h2c) is not supported. HTTP/2 is negotiated exclusively through the
h2ALPN token during the TLS handshake.Request methods -- all standard methods (GET, POST, PUT, DELETE, etc.) are supported. Request bodies are fully buffered before the handler is called, same as HTTP/1.x. Note: unlike HTTP/1.1 where Feersum rejects non-standard methods (TRACE, PROPFIND, etc.) with 405, HTTP/2 passes all methods through to the request handler.
Streaming responses -- the
psgi.streaming/start_streaming()interface works over HTTP/2, with eachwrite()producing DATA frames.Multiple concurrent streams -- the server processes many streams in parallel on a single connection, up to
FEER_H2_MAX_CONCURRENT_STREAMS(default 100).Rapid-reset protection (CVE-2023-44487) -- a connection that opens and resets more than
FEER_H2_RST_FLOOD_THRESHOLD(200) streams withinFEER_H2_RST_FLOOD_WINDOW(10) seconds is closed. Server-initiated resets (timeouts, internal errors) are not counted against the client.Not supported -- server push, server-sent trailers, streaming (incremental) request bodies, and
sendfile. For HTTP/2 responses, usewrite()instead ofsendfile.IO::Handle response bodies -- a PSGI response whose body is a filehandle or
getline-style object is read to completion when the handler returns, with bytes flushed to the client incrementally as they are read. Unlike the HTTP/1.x path this drain is not event-loop-paced: it does not read the socket while running, so a very large or slow body occupies the worker until it is fully read (a per-streamRST_STREAMthat keeps the connection open is not noticed until the drain finishes). For such responses prefer the streaming writer API (start_streaming+write), which is driven by the event loop.PSGI environment --
psgi.url_schemeishttpsfor HTTP/2 streams.SERVER_PROTOCOLisHTTP/2.Extended CONNECT / WebSocket tunnels (RFC 8441) -- Feersum advertises
SETTINGS_ENABLE_CONNECT_PROTOCOL=1so HTTP/2 clients can open WebSocket tunnels via Extended CONNECT. Feersum translates the H2 Extended CONNECT into H1-equivalent PSGI env variables (matching HAProxy/nghttpx behaviour), so existing PSGI WebSocket middleware works transparently:REQUEST_METHOD => 'GET' # translated from CONNECT HTTP_UPGRADE => 'websocket' # synthesised from :protocol HTTP_CONNECTION => 'Upgrade' # synthesised psgix.h2.protocol => 'websocket' # raw :protocol value psgix.h2.extended_connect => 1The native
$req->method()likewise returnsGETfor these streams.The handler code is identical to HTTP/1.1 upgrades: write an
HTTP/1.1 101response line followed byUpgrade:/Connection:headers viapsgix.io(or$req->io()). Under H2, Feersum automatically sends 200 HEADERS to accept the tunnel and silently swallows the HTTP/1.1 101 response written by the app, relaying only the subsequent data as H2 DATA frames. This means the same PSGI handler works for both H1 and H2 without any protocol branching.psgix.io(or$req->io()) returns a bidirectional handle backed by a Unix socketpair; Feersum bridges bytes between that handle and the HTTP/2 DATA frames in both directions.
See eg/h2-server.pl for a runnable server that serves h2 and HTTP/1.1 on the same listener via ALPN.
PERFORMANCE
Benchmark results on a typical Linux server (single process, single thread, loopback, wrk -t4 -c100 -d30, "Hello World" response):
Feersum native: ~210K req/s
Feersum PSGI: ~132K req/s
Gazelle: ~44K req/s
Starlet: ~23K req/s
Twiggy: ~14K req/s
Mojolicious: ~2.8K req/s
Read the table as orientation, not a controlled comparison: Feersum runs with keepalive on, Gazelle has no keepalive support at all, and Starlet serves one request per connection unless raised, so those two pay a TCP setup per request.
The native request_handler avoids PSGI env hash construction and is roughly 50% faster. TLS 1.3 via vendored picotls costs about 18% on the native interface and under 10% on PSGI, where per-request work dominates; expect more without AES-NI. In PSGI mode Feersum is 3-10x faster than other popular Perl PSGI servers.
Run bash bench/compare.sh to get numbers on your own hardware. See also bench/run.sh (plain HTTP and prefork), bench/run_tls.sh (TLS), bench/run_unix.sh (unix sockets), and bench/pipeline.pl (HTTP/1.1 pipelining).
DEPLOYMENT
Systemd
# /etc/systemd/system/feersum.socket
[Socket]
ListenStream=80
[Install]
WantedBy=sockets.target
# /etc/systemd/system/feersum.service
[Service]
ExecStart=/usr/bin/perl /path/to/app.pl
NonBlocking=true
User=www-data
Group=www-data
See eg/systemd-socket.pl for socket activation code.
Docker
HEALTHCHECK --interval=5s CMD curl -sf http://localhost:5000/health
See eg/healthcheck.pl for a health check endpoint pattern.
Reverse Proxy
Feersum works behind nginx, HAProxy, Caddy, or Envoy. See eg/nginx.conf, eg/haproxy.cfg, eg/Caddyfile, eg/envoy.yaml for example configs. Enable reverse_proxy or proxy_protocol as appropriate.
Zero-Downtime Restart
For a PSGI app, use Plack::Handler::Feersum - it is the handler that installs the app with psgi_request_handler:
plackup -s Feersum --app-file=app.psgi --hot-restart=1 --pre-fork=4 \
--listen 0.0.0.0:5000 app.psgi
kill -HUP <master-pid> # zero-downtime reload of app code
--app-file is required: hot restart re-reads the app from disk in each new generation, and plackup's positional argument only supplies an already-compiled app. Without it the server croaks with hot_restart requires app_file.
Feersum::Runner installs a native handler, so use it directly only with a native app file:
perl -MFeersum::Runner -e '
Feersum::Runner->new(
listen => ["0.0.0.0:5000"],
app_file => "app.feersum",
hot_restart => 1,
pre_fork => 4,
)->run;
'
See eg/hot-reload.pl for a complete example.
UPGRADING
Upgrading from 1.505 is a large jump. Most changes are additive, but the following can affect a working application without any code change on your part. Nothing here needs action if your app is a plain PSGI handler served over HTTP/1.1 with default settings.
Requirements
Perl 5.14 or newer is now required (was 5.8.7).
New and lowered default limits
These are new DoS protections. Each is configurable; a request exceeding one is rejected by the server before your handler runs, so the app sees nothing.
setting 1.505 now
----------------- ---------------- ------------------
max_body_len 2 GiB 64 MiB
max_uri_len (no limit) 8192 -> 414
max_read_buf (no limit) 64 MiB -> 431/413
max_connections (no limit) 10000
header_timeout (none) 10 seconds
write_timeout (none) 0 (off)
Raise any of them with the corresponding method if your application accepts large uploads or long query strings; see "METHODS".
Privilege drop clears supplementary groups
Feersum::Runner's user option previously called only setuid: without a matching group, the process kept its original GID and every supplementary group, so a daemon started as root and "dropped" to an unprivileged user retained root's group memberships (on a typical Linux box that includes shadow and docker). user now also derives the group from that user's passwd entry and clears supplementary groups, and the drop is verified.
A configuration that cannot complete the group drop now croaks instead of continuing with the groups intact.
Stricter request parsing
Requests that 1.505 accepted are now rejected:
An HTTP/1.1 request with no
Host:header is answered with 400 (RFC 9112 requires it). Hand-written health checks and some load-balancer probes send one without. HTTP/1.0 requests withoutHost:are still accepted.Both
Content-LengthandTransfer-Encodingpresent: 400.Any
Transfer-Encodingon an HTTP/1.0 request: 400. 1.505 ignored the header entirely on 1.0, soContent-Lengthframed the message. That is the dangerous half of a TE.CL desync - a front end honouringTransfer-Encodingand Feersum honouringContent-Lengthdisagree on where the request ends (RFC 9112 section 6.1), and nginx's defaultproxy_http_versionis 1.0.Transfer-Encodingis not defined for HTTP/1.0, so it is now rejected rather than silently ignored.A duplicate
Host:header: 400.Obsolete line folding (a continuation line) in a request header: 400. 1.505 silently dropped the continuation.
Transfer-Encodingon a method that takes no body: 400. An unsupported transfer coding: 501.An
Expect:value other than100-continue: 417. 1.505 ignoredExpect:entirely.A header name longer than 128 bytes: 431. 1.505 documented this limit but never enforced it.
A
Content-Lengthwith a leading sign, such as+5: 400. RFC 9110 section 8.6 defines it as1*DIGIT, and an intermediary that rejects or reinterprets the value while Feersum accepts it is a smuggling desync. Surrounding whitespace is still accepted - it is legal OWS.A PROXY protocol v2 frame whose TLV block ends in a 1 or 2 byte remainder: rejected. Such a tail is too short to be a TLV header, so it is the same malformation as a TLV whose declared length overruns the block, which was already rejected; the two now agree. The protocol pads with
PP2_TYPE_NOOP, never with a bare remainder.
An absolute-form request target (GET http://host/path HTTP/1.1, which RFC 9112 section 3.2.2 requires a server to accept) now yields the path in PATH_INFO. 1.505 put the whole http://host/path there, so no route or ACL matched it. REQUEST_URI still carries the raw target, unchanged.
TLS
A client that offers ALPN but shares no protocol with the server now gets a fatal no_application_protocol alert, as RFC 7301 section 3.2 requires. 1.505 silently fell back to HTTP/1.1, so a client offering only an unknown protocol token used to get a working HTTP/1.1 connection and now gets a handshake failure. Clients that send no ALPN extension at all are unaffected.
Response behaviour
A response to
HEADno longer carries a body (RFC 9110 section 9.3.2). Handlers may keep returning one; Feersum measures it forContent-Lengthand transmits only the headers.1xx and 204 responses no longer pass through an application-supplied
Content-Length, and 205 is now treated as a no-body status.A no-body status returned with a body now sends none of it, whatever form the body takes - an array, a filehandle, or
sendfile. Such a response carries no framing, so those bytes previously arrived undelimited and the next response on a keepalive connection was read as their continuation. A handler that answers 304 without discarding an already-open filehandle is the usual way to hit this.A streaming response now honours an application-supplied
Content-Lengthinstead of discarding it. If yours is wrong the response will be truncated or over-long; either correct it or omit it and let Feersum use chunked framing.A response header name or value containing CR or LF is rejected with 500 (CWE-113 response splitting). This breaks the old trick of packing two
Set-Cookievalues into one string.A UTF8-flagged response string (body, header value or status message) whose characters all fit in a byte is now sent as its downgraded bytes, the way
syswrite-based servers such as Starman and Twiggy send it. Such strings used to go out in Perl's internal UTF-8 encoding - consistently framed, but a byte-level difference from every other PSGI server. Strings holding characters above 255 (illegal in a PSGI response) still go out in the internal encoding with consistent framing.Duplicate request headers are now joined (with
", ", or"; "forCookie) rather than only the first being reported.
PSGI environment
psgi.inputis always a reader object, even for a request with no body (PSGI 1.1 requires this). Code usingif ($env->{'psgi.input'})as a has-a-body test must checkCONTENT_LENGTHinstead.psgix.input.bufferedis no longer advertised. The handle was always forward-only (seekcannot rewind), while the flag tells consumers like Plack::Request thatseek(0,0)restores the whole body - so->contentafter->body_parameterssilently returned''. With the flag absent, such consumers buffer the body themselves and every ordering works. Reads onpsgi.inputstill never block. An app that tested the flag before reading incrementally will now take its own buffered path; the input handle additionally supportsgetline/getlinesand<$fh>since 1.507.psgi.multiprocessis true when running underpre_fork.return_from_psgix_io()(and the nativereturn_from_io()) now croak unless the handle handed back is the one that wraps this connection's socket. Passing a different filehandle used to be accepted, and it spliced that handle's buffered bytes into this connection's next request while leaving the real handle able to close a descriptor Feersum had reclaimed.Handing the socket back now takes a private duplicate of the descriptor, so the returned handle no longer shares one with Feersum. Code written to the old rule (keep a reference alive for the life of the process, because there was no safe moment to drop it) still works and can now simply release the handle when it is done.
Error reporting
The default Feersum::DIED now warns the exception to STDERR. It previously called Carp::confess, whose exception was in turn swallowed by the G_EVAL the handler is invoked under - so an unhandled application exception produced a 500 and no output at all. Expect to start seeing those errors in your logs. Install your own handler (see "DIED") to change the format or silence them.
Runner, plackup and the CLI
plackup -s Feersum -Dnow really daemonizes. 1.505 ignored the flag, so a-Dthat was harmless for years will now detach the process and redirect STDERR to /dev/null. Remove it under a supervisor.SIGTERMis now handled and triggers a graceful shutdown, so the process exits 0 rather than reporting death by signal, and shutdown may take up tograceful_timeout.--listenin native mode accumulates instead of last-one-wins. A script that appended--listento override an earlier value will now bind both addresses.bin/feersum --nativerejects unknown options instead of ignoring them. Leftover flags meant for another server will now be a fatal error.read_timeout => 0is now rejected; it was silently ignored.The Feersum object is no longer a hash reference, so the documented 1.505 accessor
Feersum->endjinn->{socket}no longer works. Use the public API.Per-connection tunables are snapshotted when the connection is accepted, so changing them at runtime no longer affects live connections.
header_timeoutis the exception: it is re-read when each request starts, so a change does reach connections that are already open.
BUGS
Please report bugs using https://github.com/stash/Feersum/issues
Request bodies are capped at MAX_BODY_LEN (64 MiB by default). For untrusted clients it is still recommended to run Feersum behind a reverse proxy that enforces tighter entity-size limits.
Although not explicitly a bug, the following may cause undesirable behavior. Feersum will have set SIGPIPE to be ignored by the time your handler gets called. If your handler needs to detect SIGPIPE, be sure to do a local $SIG{PIPE} = ... (perlipc) to make it active just during the necessary scope.
Feersum is not thread-safe and must not be used with Perl ithreads. It uses global/static data structures (free lists, lookup tables) that are not protected by locks. Running Feersum in a multi-threaded environment will cause race conditions and memory corruption. Use pre-fork instead of threads for parallelism.
SEE ALSO
Companion modules in this distribution: Feersum::Runner, Plack::Handler::Feersum, Feersum::Connection, Feersum::Connection::Handle.
Inspiration: https://en.wikipedia.org/wiki/Feersum_Endjinn
picohttpparser: https://github.com/h2o/picohttpparser
picotls: https://github.com/h2o/picotls
AUTHORS
Jeremy Stashewsky, stash@cpan.org
vividsnow - multi-instance, TLS 1.3 (picotls), HTTP/2 (nghttp2), PROXY protocol v1/v2, security hardening
THANKS
Tatsuhiko Miyagawa for PSGI and Plack.
Marc Lehmann for EV and AnyEvent (not to mention JSON::XS and Coro).
Kazuho Oku for picohttpparser.
Luke Closs (lukec), Scott McWhirter (konobi), socialtexters and van.pm for initial feedback and ideas. Audrey Tang and Graham Termarsch for XS advice.
Hans Dieter Pearcey (confound) for docs and packaging guidance.
For bug reports: Chia-liang Kao (clkao), Lee Aylward (leedo)
Audrey Tang (au) for flash socket policy support.
COPYRIGHT AND LICENSE
Copyright (C) 2011 by Jeremy Stashewsky
Portions Copyright (C) 2010 Socialtext Inc.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.14 or, at your option, any later version of Perl 5 you may have available.
picohttpparser is Copyright 2009-2014 Kazuho Oku, Tokuhiro Matsuno, Daisuke Murase, and Shigeo Mitsunari. It is released under the same terms as Perl itself (or, at your option, the MIT license).
picotls (bundled for TLS support) is Copyright (C) 2016-2025 DeNA Co., Ltd., Kazuho Oku, Fastly, and Christian Huitema, released under the MIT license (the bundled PEM/base64 component is under the ISC license). See the per-file headers under picotls-git/ for the authoritative notices.