Skip to content

RaftWebServerSysMod

Rob Dobson edited this page Oct 25, 2026 · 4 revisions

WebServer SysMod

Built-in SysMod that hosts an HTTP server, exposes the REST API over HTTP, optionally serves static files, and registers WebSocket channels with the Comms Channels layer.

Source: RaftWebServer/components/WebServer/

What it provides

  • HTTP server on a configurable port with a configurable REST API path prefix.
  • REST API bridge — every endpoint registered via RestAPIEndpointManager::addEndpoint() is reachable as GET/POST/PUT/DELETE <prefix>/<endpoint>....
  • Deferred responses — an endpoint may defer its reply. A GET endpoint is called from the responder's loop; a POST (or other method) endpoint once the whole request body has been received. The response (and its headers, which include the length) is sent when the endpoint completes it, and the connection isn't timed out as idle meanwhile (the deferral's own timeout, at most 60 s, and the 5-minute connection limit still apply). If the client disconnects first, the response is discarded.
  • File uploads off the main loop — a fileupload POST's chunks are written on the file system worker, not in the web server's loop. The web server stops reading the request body while 16 KB waits to be written (TCP flow control then slows the client), and the reply is deferred until the file is written (see ProtocolExchange).
  • Static file serving from any mounted file system (LittleFS / SPIFFS / SD). Each chunk of a file is read on the file system worker, the next one read ahead while the current one is sent (or read in place if the worker can't take the job).
  • WebSocket channels — each entry in the websockets array becomes one or more comms channels using a chosen protocol codec.
  • webcerts REST endpoint for uploading TLS certificates.

SysType configuration

Place a WebServer block in the SysType SysMods array. Typical values:

{
    "name": "WebServer",
    "enable": 1,
    "webServerPort": 80,
    "apiPrefix": "api/",
    "fileServer": 1,
    "numConnSlots": 6,
    "stdRespHeaders": [
        "Access-Control-Allow-Origin: *"
    ],
    "staticFilePaths": "/=/local,/files/sd=/sd",
    "websockets": [
        {
            "pfix": "ws",
            "pcol": "RICSerial",
            "maxConn": 4,
            "txQueueMax": 20,
            "pingMs": 30000
        }
    ],
    "taskCore": 0,
    "taskPriority": 5,
    "taskStack": 5000,
    "sendMax": 5000,
    "clearPendingMs": 0
}
Key Default Meaning
enable false Master switch
webServerPort 80 TCP listen port
apiPrefix "/api" Path prefix for REST endpoints — /api/subscription etc.
fileServer true Enable static file handler
numConnSlots 12 Max concurrent HTTP connections
stdRespHeaders [] Header lines added to every response
staticFilePaths "/=/<defaultFS>,/files/local=/local,/files/sd=/sd" Comma-separated urlPath=fsPath pairs
websockets [] Array of websocket configs (see below)
taskCore, taskPriority, taskStack 0, 9, 5000 Web server task
sendMax 20000 Max unsent outbound data held per connection (bytes); a send that would exceed it fails the connection - see Memory use and WebSocket behaviour
clearPendingMs 0 If > 0, drop pending tx after this many ms

Memory use

Each connection can queue up to sendMax bytes of unsent data when the link drains slowly (e.g. weak WiFi). These buffers use PSRAM when the board has it and it is enabled (CONFIG_SPIRAM), and otherwise internal RAM, which WiFi and lwIP also need. With the defaults (12 slots × 20000 bytes) a board without PSRAM can run out of internal RAM under concurrent downloads.

On boards without PSRAM:

  • Consider lowering numConnSlots and sendMax.
  • In RaftWebServer versions after 1.16.1, a send buffer that can't be grown, or a file chunk that can't be allocated, fails that connection instead of crashing.
  • File downloads are read and sent in chunks of at most 4096 bytes (RAFT_WEB_FILE_CHUNK_MAX_BYTES, which can be defined in the build to change it), rather than up to sendMax.

Connection limits and lwIP settings

Each HTTP or WebSocket connection uses an lwIP socket and an active TCP connection, as do the listeners, mDNS, SNTP, NetBIOS, DNS and the application. The ESP-IDF defaults are too small for 12 connection slots, and when they run out new connections are reset by lwIP before the web server sees them. Recommended settings for the SysType's sdkconfig.defaults (or the project's systypes/Common/sdkconfig.defaults):

CONFIG_LWIP_MAX_SOCKETS=24
CONFIG_LWIP_TCP_ACCEPTMBOX_SIZE=16
CONFIG_LWIP_MAX_ACTIVE_TCP=32

In RaftWebServer versions after 1.16.1:

  • Startup warning: a warning is logged if numConnSlots plus the sockets other things typically need exceeds CONFIG_LWIP_MAX_SOCKETS.
  • Busy response: a connection which can't be given a slot gets 503 Service Unavailable with Retry-After: 1, rather than being closed (which the client sees as a reset).
  • Bursts: several waiting connections are accepted per main loop pass, so a burst of connections is handled.

With these settings a UM FeatherS3 served bursts of up to 24 simultaneous requests with no failures.

On boards with PSRAM, enable it in the SysType's sdkconfig.defaults (CONFIG_SPIRAM=y and the board's mode, e.g. CONFIG_SPIRAM_MODE_QUAD=y or CONFIG_SPIRAM_MODE_OCT=y). Note that in RaftCore 1.58.1 and earlier the PSRAM-aware buffers didn't use PSRAM under ESP-IDF 5 and later, even when it was enabled. This is fixed in later versions.

Websocket entry

Key Default Meaning
pfix "ws" URL path prefix and channel interface name
pcol "RICSerial" Protocol codec to attach (RICSerial, RICFrame, RICJSON)
maxConn 5 Max simultaneous websocket connections
pktMaxBytes 100000 Longest message received (longer ones are dropped)
inboundBlockLen 24000 with PSRAM (capped at pktMaxBytes), otherwise the channel default (1200) The comms channel's inboundBlockLen. RICREST file uploads use blocks of up to 2/3 of it, so up to 16000 bytes with PSRAM, 800 without
pingMs 30000 Ping interval (0 for none)
closeIfNoPong false Close the connection if no pong arrives within 2 × pingMs + 2 s
content "binary" binary or text messages
txQueueMax 20 Outbound queue depth - only used if WEBSOCKET_SEND_USE_TX_QUEUE is defined (it isn't by default)

Each connection slot is registered with CommsChannelManager as a channel named <pfix>_<idx>, so connection 0 to pfix:"ws" becomes channel ws_0. See Comms Channels.

WebSocket behaviour

  • Receiving: fragmented messages (a first frame and continuation frames) are put back together, each fragment unmasked with its own mask key, and a ping or pong between fragments doesn't disturb them. A message longer than pktMaxBytes is dropped (only its own frames are skipped), and so is one whose receive buffer can't be grown with an 8 KB margin left (SpiramAwareAllocator::max_allocatable()). A frame whose header gives a length over pktMaxBytes closes the link with status 1009 (message too big). Receive buffers use PSRAM where available and grow geometrically.
  • Sending: each frame is written once - the header, then the data, appended to reserved space - with no zero-filled buffer. Sends happen on the main task (the channel callbacks must only be called from it). By default (WEBSOCKET_SEND_USE_TX_QUEUE not defined) a frame is sent at once; data the socket can't take now is held in the connection's send buffer (up to sendMax) and sent as the socket drains.
  • Back-pressure: before a publish is sent on a WebSocket channel the web server checks the connection can send now (canSendBufferOnChannel(): no unsent data held, and the socket writable), so high-rate publishers drop messages rather than overflow the send buffer and close the connection. Other messages only check that the channel is connected. See RaftCore devdocs/websocket-backpressure-analysis.md.

REST endpoints exposed

Endpoint Method Purpose
webcerts/set POST Upload TLS certificates (body = JSON document)

Every other endpoint visible over HTTP comes from other SysMods registering with RestAPIEndpointManager. The web server simply forwards.

Programmatic helpers

void serveStaticFiles(const char* servePaths, const char* cacheControl = nullptr);
void enableServerSideEvents(const String& eventsURL);
void sendServerSideEvent(const char* eventContent, const char* eventGroup);

These are typically called from your application's setup() after RaftCoreApp::setup().

Related

Clone this wiki locally