API referenceEdit this page ↗
On this page

aWOT 4 API

This reference targets aWOT 4.0.0. Install it through Arduino Library Manager. For existing projects, read Migrating to v4. The 3.x reference remains available for older installations.

App

App registers routes and synchronously processes one HTTP request at a time. Include <aWOT.h>. The examples below use its default namespace imports; when ENABLE_AWOT_NAMESPACE is defined, qualify types with awot::.

App app;

void home(Request& req, Response& res) {
  res.print("Hello World!");
}

// Register once in setup(): app.get("/", home);
// With a connected Client in loop():
// app.process(&client);
// client.stop();

Construction and ownership

App();
~App();

Apps cannot be copied or assigned: their owned default Router is noncopyable. Destroying an App releases its route registrations, but does not destroy mounted routers, callback instances, or network clients. Keep registered path strings, mounted routers, and bound callback instances alive while the App uses them. Registration methods return void; they provide no allocation-failure result.

App::get

void get(const char* path, Router::MIDDLEWARE_PARAM middleware);
void get(Router::MIDDLEWARE_PARAM middleware);

Register a GET handler. The overload without a path matches every GET path. Handlers run in registration order. Call req.next() to continue; returning without it stops dispatch. See Router::Middleware for callback configuration.

App::head

void head(const char* path, Router::MIDDLEWARE_PARAM middleware);
void head(Router::MIDDLEWARE_PARAM middleware);

Register a HEAD handler. HEAD needs its own route; a GET registration does not handle HEAD. Status and headers are sent, while response payload and chunk terminators are suppressed. The pathless overload matches every HEAD path.

App::post

void post(const char* path, Router::MIDDLEWARE_PARAM middleware);
void post(Router::MIDDLEWARE_PARAM middleware);

Register a POST handler, optionally restricted to a route pattern.

App::put

void put(const char* path, Router::MIDDLEWARE_PARAM middleware);
void put(Router::MIDDLEWARE_PARAM middleware);

Register a PUT handler, optionally restricted to a route pattern.

App::del

void del(const char* path, Router::MIDDLEWARE_PARAM middleware);
void del(Router::MIDDLEWARE_PARAM middleware);

Register a DELETE handler. The C++ method name is del.

App::patch

void patch(const char* path, Router::MIDDLEWARE_PARAM middleware);
void patch(Router::MIDDLEWARE_PARAM middleware);

Register a PATCH handler, optionally restricted to a route pattern.

App::options

void options(const char* path, Router::MIDDLEWARE_PARAM middleware);
void options(Router::MIDDLEWARE_PARAM middleware);

Register an OPTIONS handler. aWOT does not generate an OPTIONS or CORS response automatically.

App::use

void use(const char* path, Router* router);
void use(Router* router);
void use(const char* path, Router::MIDDLEWARE_PARAM middleware);
void use(Router::MIDDLEWARE_PARAM middleware);

Callback overloads match every supported HTTP method. Their optional path is a route pattern, so app.use("/api", handler) matches /api and /api/, rather than every descendant path. A pathless callback matches every path.

Router overloads mount a borrowed Router at a path prefix with a segment boundary. app.use("/api", &router) mounts routes under /api and does not match /api2. Mounting without a path preserves the whole path. See Router::use for trailing slashes and nested mounts.

Member callbacks

With STD_FUNCTION_MIDDLEWARE, each method-specific registration and use also provides these exact template overloads:

template <typename T>
void get(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void get(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void head(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void head(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void post(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void post(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void put(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void put(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void del(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void del(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void patch(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void patch(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void options(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void options(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void use(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void use(T* instance, void (T::*method)(Request&, Response&));

These bind a non-const member function to the supplied instance. The App stores the binding, and the instance must outlive its registrations. Capturing lambdas are also available with this callback configuration. Function-pointer builds support free functions and noncapturing lambdas. See Router::Middleware.

App::notFound

void notFound(Router::MIDDLEWARE_PARAM middleware);
#ifdef STD_FUNCTION_MIDDLEWARE
template <typename T>
void notFound(T* instance, void (T::*method)(Request&, Response&));
#endif

Set the single fallback callback, replacing any earlier one. It runs after successful parsing if routing leaves both statusSent() and ended() false, even if a matched handler returned without writing a response. aWOT sends status 404 before calling it. Supply the headers and body in the callback.

With no custom fallback, aWOT invokes onError with 404 and sends its built-in 404 response unless the error callback ends the response. A custom notFound callback takes precedence over that 404 error path.

App::finally

void finally(Router::MIDDLEWARE_PARAM middleware);
#ifdef STD_FUNCTION_MIDDLEWARE
template <typename T>
void finally(T* instance, void (T::*method)(Request&, Response&));
#endif

Set the single final callback, replacing any earlier one. It runs once after routing, fallback, or a parser error and before buffered output and the final chunk are finalized. It runs even after res.end(). On parser errors, request fields may be incomplete. Invalid processing options and null transports bypass all callbacks, including finally.

App::onError and ErrorHandler

typedef void (*ErrorHandler)(Request&, Response&, int statusCode);
void onError(ErrorHandler handler);

Set the single error callback; nullptr removes it. This is always a function pointer, including builds using std::function for ordinary middleware. It receives parser errors or the default 404 fallback: 400 for malformed input or unsupported request transfer encoding, 408 for parser read timeout, 414 for an invalid or oversized request target, 431 for captured-header overflow or certain header read failures, and 505 for an unsupported HTTP version.

To supply a custom response, call res.end() after writing it. Otherwise aWOT continues with its built-in sendStatus(statusCode) response. Parser errors close the client after finalization and never allow connection reuse. Malformed chunk framing discovered during body reads also closes the client. If no response status has been sent when routing finishes, aWOT invokes onError with 400; an already-started response is not replaced.

void errorResponse(Request& req, Response& res, int code) {
  res.status(code);
  res.set("Content-Type", "application/json");
  res.print("{\"error\":true}");
  res.end();
}
// Register once: app.onError(errorResponse);

App::setTimeout

void setTimeout(unsigned long timeoutMillis);

Set the read timeout in milliseconds for future Requests. The default is 1000. It controls waits for individual input bytes during parsing and initializes the Request’s inherited Stream timeout. It is neither a whole-request deadline nor a keep-alive idle timeout. Changing a current body’s timeout uses req.setTimeout().

App::ProcessOptions

struct ProcessOptions {
  ArrayView<char> urlBuffer;
  ArrayView<uint8_t> writeBuffer;
  ArrayView<Request::Header> headers;
  void* context = nullptr;
};

All storage is borrowed through process(). Arrays infer capacities or entry counts; raw pointers need {pointer, size}. A null URL or write-buffer pointer selects default storage, ignoring that view’s size. A supplied URL buffer needs at least two bytes, including the terminator. A supplied output buffer needs more than CHUNK_OVERHEAD bytes. Output capacity is additionally limited by the reserved chunk-size header. Only missing buffers allocate default local arrays.

Header count must be nonnegative; a positive count needs a non-null array, and each entry needs a non-null name and buffer with positive capacity. Zero count ignores the header pointer. Invalid options stop the Client and return a result with both flags false, without invoking handlers. See Request::Header and ArrayView.

char url[64];
uint8_t output[32];
char authorization[96];
Request::Header headers[] = { {"Authorization", authorization} };
int requestId = 7;

App::ProcessOptions options;
options.urlBuffer = url;
options.writeBuffer = output;
options.headers = headers;
options.context = &requestId;
// For raw pointers: options.urlBuffer = {urlPointer, urlCapacity};
// App::ProcessResult result = app.process(&client, options);

Keep the options’ arrays, header names and value buffers, and context object alive until process() returns. Concurrent requests need separate storage. Captured header buffers are cleared before and after processing.

App::ProcessResult

struct ProcessResult {
  bool responseOpen = false;
  bool keepAlive = false;
};

responseOpen means the parsed request successfully opened a response with res.keepOpen(), and its initial writes succeeded. Retain the underlying client for the stream or protocol upgrade. A chunked request body must also be complete, including its final chunk and trailers.

keepAlive means parsing succeeded, the request body was completely consumed, writes succeeded, the response remains ordinary HTTP, and the response selected Connection: keep-alive. HTTP/1.1 alone does not enable reuse. The two flags cannot both be true. A false flag pair means the caller should close the client. These flags describe processing, rather than guaranteeing that a peer remains connected; the caller owns later disconnect handling and idle timeouts. Neither flag retains the client or extends its lifetime. On ESP32, destroying the last WiFiClient handle closes the socket, even if a flag was true. Keep the handle in persistent storage, or copy it into an owner such as a channel before the local handle leaves scope.

App::process

ProcessResult process(Client* client);
ProcessResult process(Client* client, const ProcessOptions& options);
ProcessResult process(Stream* stream);
ProcessResult process(Stream* stream, const ProcessOptions& options);
template <size_t N>
ProcessResult process(Client* client, Request::Header (&headers)[N]);
template <size_t N>
ProcessResult process(Stream* stream, Request::Header (&headers)[N]);

Process one request synchronously, invoke matching handlers, run the final callback, and finalize buffered output. The header-array overloads infer the entry count and otherwise use default options. A null transport returns both flags false. The ordinary success path leaves transport closure to the caller; invalid options, parser errors, and failed response writes stop a Client.

// client is a connected handle stored outside loop(), or in a persistent pool.
App::ProcessResult result = app.process(&client);
if (!result.responseOpen && !result.keepAlive) {
  client.stop();
}
// For keepAlive, retain this client and process its next request when ready.
// For responseOpen, let the stream/channel own subsequent I/O and closure.

This fragment assumes that client remains alive after the enclosing function returns. Declaring a fresh local WiFiClient inside loop() and omitting stop() does not preserve an ESP32 connection. The complete connection reuse example shows the persistent declaration, acceptance, subsequent requests, and idle timeout.

The Stream overload wraps the borrowed Stream in a temporary StreamClient. req.client() refers to that adapter only during processing; do not retain it or pass it to a long-lived channel. The adapter’s stop() is a no-op and it reports itself connected, so the caller must manage the actual Stream lifecycle. Use a real concrete Client for WebSockets and SSE.

App::strcmpi and App::strcmpiP

static int strcmpi(const char* s1, const char* s2);
static int strcmpiP(const char* s1, const unsigned char* s2);

Compare two NUL-terminated strings ignoring case. Return 0 for equality, -1 when the first sorts earlier, or 1 when it sorts later. strcmpiP reads its second argument from program memory. Supply valid non-null pointers.

Router

Router groups handlers for mounting in an App or another Router. Registration and matching follow the same rules as App routes. Include <aWOT.h>.

Construction and ownership

Router();
Router(const Router&) = delete;
Router& operator=(const Router&) = delete;
~Router();

Construct an empty Router. It owns its registration nodes and releases them on destruction. It borrows mounted routers, path strings, and member-callback instances; those objects must remain alive while registrations can run. A Router cannot be copied or assigned. Mount with app.use("/prefix", &router); the old path-taking constructor is unavailable. Registration methods return void and provide no allocation-failure result. Register routes once, usually in setup().

Router::Middleware

The callback type depends on the configuration in every translation unit:

#ifdef STD_FUNCTION_MIDDLEWARE
typedef std::function<void(Request& request, Response& response)> Middleware;
#define MIDDLEWARE_PARAM Middleware
#else
typedef void Middleware(Request& request, Response& response);
#define MIDDLEWARE_PARAM Middleware*
#endif

The headers use MIDDLEWARE_PARAM for the callback parameters shown below. Selection is automatic on ESP8266, ESP32, or platforms with <functional>; STD_FUNCTION_MIDDLEWARE also enables it explicitly. std::function supports capturing lambdas and the member overloads. Function-pointer builds support free functions and noncapturing lambdas.

For a function-pointer build, apply AWOT_NO_STD_FUNCTION_MIDDLEWARE to every library and sketch translation unit. Defining it only inside a sketch cannot change compiled library configuration. Defining it together with STD_FUNCTION_MIDDLEWARE is an error. See the configuration guide.

A matched callback starts with continuation disabled. req.next() enables the next matching registration; returning alone stops dispatch. res.end() stops dispatch even if req.next() was called. Router dispatch never reads or drains the request body for the callback.

void logRequest(Request& req, Response& res) {
  Serial.println(req.path());
  req.next();
}

void showItem(Request& req, Response& res) {
  char id[16];
  if (!req.route("id", id, sizeof(id))) {
    res.sendStatus(400);
    return;
  }
  res.print(id);
}

Router api;
// Register once:
// api.use(logRequest);
// api.get("/items/:id", showItem);
// app.use("/api", &api);

Router::get

void get(const char* path, MIDDLEWARE_PARAM middleware);
void get(MIDDLEWARE_PARAM middleware);

Register GET middleware with a route pattern, or for every GET path when the path is omitted.

Router::head

void head(const char* path, MIDDLEWARE_PARAM middleware);
void head(MIDDLEWARE_PARAM middleware);

Register HEAD middleware. HEAD never falls back to GET. Response bodies and chunk terminators are suppressed for HEAD, while status and headers are sent.

Router::post

void post(const char* path, MIDDLEWARE_PARAM middleware);
void post(MIDDLEWARE_PARAM middleware);

Register POST middleware, optionally restricted to a route pattern.

Router::put

void put(const char* path, MIDDLEWARE_PARAM middleware);
void put(MIDDLEWARE_PARAM middleware);

Register PUT middleware, optionally restricted to a route pattern.

Router::del

void del(const char* path, MIDDLEWARE_PARAM middleware);
void del(MIDDLEWARE_PARAM middleware);

Register DELETE middleware. The C++ method is named del.

Router::patch

void patch(const char* path, MIDDLEWARE_PARAM middleware);
void patch(MIDDLEWARE_PARAM middleware);

Register PATCH middleware, optionally restricted to a route pattern.

Router::options

void options(const char* path, MIDDLEWARE_PARAM middleware);
void options(MIDDLEWARE_PARAM middleware);

Register OPTIONS middleware. Generate any required CORS headers in the handler.

Router::use

void use(const char* path, Router* router);
void use(Router* router);
void use(const char* path, MIDDLEWARE_PARAM middleware);
void use(MIDDLEWARE_PARAM middleware);

Callback overloads match every supported HTTP method. A provided path matches the full route; without a path, the callback matches every path.

The Router overload mounts at a segment boundary. Trailing slashes on the mount prefix are ignored, so /api, /api/, and /api/// mount equivalently. /api matches /api, /api/, and /api/items, but excludes /api2/items. The mounted router matches its paths against the suffix after that prefix. Its "/" or "" root route matches either /api or /api/. Mounting at / or without a path leaves the full path available.

Nested mounts accumulate their prefixes. For example, a child /items/:id route mounted under /v1, inside a Router mounted under /api, handles /api/v1/items/42. req.path() still returns the full decoded path, while req.route() refers to the current handler’s suffix and pattern. Prefixes are literal strings and do not define route parameters.

Mounted router dispatch shares continuation state with its parent. If a child callback returns without req.next(), parent dispatch stops too. If no child route matches, or the last child callback calls req.next(), subsequent parent registrations can run. The mount borrows the router rather than copying it.

Member callbacks

These overloads exist only with STD_FUNCTION_MIDDLEWARE:

template <typename T>
void get(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void get(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void head(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void head(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void post(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void post(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void put(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void put(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void del(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void del(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void patch(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void patch(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void options(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void options(T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void use(const char* path, T* instance, void (T::*method)(Request&, Response&));
template <typename T>
void use(T* instance, void (T::*method)(Request&, Response&));

Pass an instance pointer and a non-const member function. The binding retains the pointer and the instance must outlive dispatch. For example:

class Controller {
 public:
  void index(Request& req, Response& res) { res.print("Controller"); }
};

Controller controller;
Router routes;
// Register once: routes.get("/", &controller, &Controller::index);

Request

Request extends Arduino Stream to read a bounded HTTP body and expose the parsed request. aWOT constructs it for each App::process() call and passes it to callbacks; its constructor is private. Do not retain a Request, its path or query pointers, or captured header pointers after processing returns.

Request::Header

struct Header {
  Header(const char* name, ArrayView<char> buffer);
  Header(const char* name, char* buffer, int bufferLength);
  const char* name;
  ArrayView<char> buffer;
};

Describe one header to capture in caller-owned storage. Arrays implicitly convert to ArrayView and infer their capacity. Raw buffers require an explicit capacity, including the NUL terminator. Put descriptors in one contiguous array:

char authorization[96];
char origin[96];
Request::Header headers[] = {
  {"Authorization", authorization},
  {"Origin", origin}
};
// Process a connected client: app.process(&client, headers);

ProcessOptions.headers = headers also infers the array’s entry count. For raw descriptor pointers, use {headerPointer, headerCount}. Names and buffers must remain valid throughout process(); each active descriptor needs a non-null name and buffer with positive capacity. Buffer contents and presence state are cleared before parsing and after processing. Use separate storage for concurrent requests.

Header names match without case sensitivity. Repeated captured fields append their values with a comma and need capacity for that comma. Leading spaces and tabs in each value are skipped. Captured-value overflow fails parsing with 431. The parser handles Content-Length and Transfer-Encoding internally rather than populating captures for them. Use req.left() to inspect the remaining body length or completion state. Trailer fields are discarded and do not populate or change captured headers.

Request::context

void* context;

The pointer supplied by ProcessOptions.context, defaulting to nullptr. aWOT neither owns nor copies the referenced object. Keep it alive through processing, use the correct type when casting, and check for null when the context is optional.

struct RequestContext { unsigned long started; };

void elapsed(Request& req, Response& res) {
  RequestContext* context = static_cast<RequestContext*>(req.context);
  if (context) res.print(millis() - context->started);
  else res.sendStatus(500);
}

Request::client

Client* client();

Return the borrowed underlying Arduino Client. Reading it directly bypasses the Request’s body bounds, chunk decoding, pushback, and counters. Use req for an ordinary body; direct Client I/O is useful after a protocol upgrade or when continuing a response opened with res.keepOpen().

For process(Stream*), the pointer identifies a temporary StreamClient adapter that expires when process() returns. Do not retain it. Channels require the correct concrete Client type, rather than that temporary adapter.

Request::MethodType

enum MethodType {
  UNKNOWN, GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS, ALL
};

UNKNOWN is the initial value; the seven supported request verbs set their corresponding value. ALL is the internal registration selector used by use. It is not an incoming HTTP verb. Unsupported verbs fail parsing with 400.

Request::method

MethodType method();

Return the parsed HTTP method. Parsing-error callbacks may see UNKNOWN.

Request::minorVersion

int minorVersion();

Return 0 for HTTP/1.0, 1 for HTTP/1.1, or -1 before a valid version was parsed. Other versions fail with 505. The version alone does not select keep-alive.

Request::next

void next();

Enable continuation to the next matching registration after this callback returns. The dispatcher disables continuation before invoking each handler. Returning without next() stops routing. res.end() also stops routing and takes precedence over continuation. Calling next() does not recursively run the next handler or end the response.

Request::get

char* get(const char* name);

Look up a captured header by a valid, NUL-terminated name without case sensitivity. Return nullptr for an absent or uncaptured header. A present empty value returns a non-null pointer to "". The pointer refers to borrowed capture storage available only during processing.

void authenticate(Request& req, Response& res) {
  const char* token = req.get("Authorization");
  if (!token || strcmp(token, "Bearer example") != 0) {
    res.sendStatus(401);
    res.end();
    return;
  }
  req.next();
}

Capture Authorization with a Request::Header before using this handler; registering middleware alone does not capture headers.

Request::path

char* path();

Return the NUL-terminated, percent-decoded path without its query. A literal + remains +. The parser finds the raw ? separator before decoding, so an encoded %3F remains part of the decoded path. Invalid percent escapes or decoded NUL anywhere in the target, including its query, fail parsing with 400. Mounts do not change this full path. Storage belongs to the active processing call; use the returned pointer as a read-only value.

Request::query

char* query();
bool query(const char* name, char* buffer, int bufferLength);

The no-argument overload returns the raw encoded query without ?. After successful target parsing, a target with no query yields an empty string. The pointer uses the same borrowed URL storage as path().

The named overload matches decoded names and copies the first matching decoded value. Both name and value decoding treat + as a space. Encoded delimiters do not split fields. A matching name= succeeds with an empty value; a bare name without = is not a value. Capacity includes the NUL terminator.

Return false for a missing name, invalid input or escape, decoded NUL, or insufficient capacity. Invalid pointers and nonpositive capacity fail without touching the buffer. Otherwise the buffer is initialized to empty; overflow leaves a terminated partial result. Always check the returned boolean.

void search(Request& req, Response& res) {
  char term[32];
  if (!req.query("q", term, sizeof(term))) {
    res.sendStatus(400);
    return;
  }
  res.print(term);
}

Request::route

bool route(const char* name, char* buffer, int bufferLength);
bool route(int number, char* buffer, int bufferLength);

Copy a segment from the current handler’s matched route. The named overload looks up :name in the current pattern. The indexed overload counts all path segments from zero, including literal segments. For /items/:id, index 0 is items and index 1 is the ID. In mounted routers, the route excludes accumulated mount prefixes, while path() still includes them.

Return true when the requested segment fits, including its NUL terminator. Return false for a missing segment/name, invalid buffer, nonpositive capacity, or overflow. Do not use the output on failure. The indexed overload clears the valid destination first and can leave a terminated partial value on overflow. The value is already path-decoded; + stays literal.

void item(Request& req, Response& res) {
  char id[16];
  if (!req.route("id", id, sizeof(id))) {
    res.sendStatus(400);
    return;
  }
  res.print(id);
}
// Register once: app.get("/items/:id", item);

Request::form

bool form(char* name, int nameLength, char* value, int valueLength);

Consume the next URL-encoded body field and copy its decoded name and value. This is sequential reading, rather than a lookup by name. Raw & separates fields and the first raw = separates name and value. Percent escapes decode and + becomes space. A field without = has an empty value. aWOT does not check Content-Type before decoding; capture and validate it when required.

Both capacities include a NUL terminator. Return true for a complete, valid field that fits; false for no field, invalid escapes, decoded NUL, timeout, or overflow. Invalid pointers and nonpositive capacity leave output untouched. Overflow leaves terminated partial output and consumes through the field’s raw & or end of body, allowing the next call to read a later field. Check left() to distinguish end of body from unread input after a failure.

void saveForm(Request& req, Response& res) {
  char name[16];
  char value[32];
  while (req.left() != 0) {
    if (!req.form(name, sizeof(name), value, sizeof(value))) {
      res.sendStatus(400);
      return;
    }
    // Validate and apply this field here.
  }
  res.sendStatus(204);
}

Request::left

int left();

For Content-Length bodies, return the unread declared bytes plus pushed-back bytes. For chunked bodies, return -1 until the zero chunk and final empty trailer line have been consumed, even when pushback contains bytes. After that, return the pushback count or zero. This call can consume already-available chunk framing without waiting for input; it does not consume payload. A malformed or incomplete chunked body continues to return -1.

aWOT accepts nonnegative decimal Content-Length up to INT_MAX, with surrounding spaces/tabs. Repeated values must agree. HTTP/1.1 also accepts a single Transfer-Encoding: chunked field, with each chunk size bounded by UINT32_MAX; the complete body can span multiple chunks. Chunk extensions are ignored and trailer fields are consumed and discarded. Comma lists or conflicting Content-Length values, Content-Length combined with Transfer-Encoding, repeated or unsupported Transfer-Encoding values, and transfer coding on HTTP/1.0 produce 400 and close the connection. Without either framing header, the body length is zero.

An unread or truncated body prevents keep-alive reuse. An incomplete chunked body also prevents an open-response handoff. aWOT does not automatically drain unread payload.

Request::available

int available();

Return immediately available payload bytes, capped at the remaining Content-Length or the current chunk’s remaining payload, plus pushback bytes. The result saturates at INT_MAX. This call can consume already-available chunk framing without waiting for input; it does not count framing as payload or look ahead through later chunks. Zero can mean the body is complete or that its next bytes have not arrived; use left() to distinguish them.

Request::read

int read();
int read(uint8_t* buf, size_t size);

The byte overload reads pushback first, then the Client. Return 0–255 for a byte or -1 when no byte is available or the body is exhausted. It does not wait for more input. Reaching the body’s end through this overload sets the inherited Stream timeout to zero.

The buffer overload returns the number read, which can be zero at body end or while chunk framing awaits input. An underlying Client read can return -1 when no byte could be returned. It can read fewer bytes than requested; use left() to distinguish completion from incomplete input. Both overloads decode chunked bodies without allocating a body buffer, and the buffer overload can cross chunk boundaries. They stop at the body boundary and leave a following HTTP request in the transport. Supply valid storage for the requested buffer size. For a timed bounded read, use readBytes() and check its count.

Request::peek

int peek();

Read one byte and push it back, returning that byte or -1. The next Request read returns it again. A first peek can increase bytesRead() because it initially reads the underlying Client. It preserves the unread payload count for a Content-Length body. For chunked bodies, it can advance framing; left() keeps its -1 result until the final chunk and trailers have been consumed.

Request::push

void push(uint8_t ch);

Push a byte onto the Request’s LIFO pushback stack. Reading it again does not increase bytesRead(). available() includes pushed bytes; left() includes them once it can report a known remaining length, retaining -1 while chunked framing is incomplete. The capacity is SERVER_PUSHBACK_BUFFER_SIZE; when full, a push replaces the previous top byte with the new one and has no failure return.

Request::bytesRead

int bytesRead();

Return bytes read from the underlying Client by this Request, including the request line, headers, body payload, chunk framing, and trailers, saturating at INT_MAX. Pushback rereads are not counted again. Calls such as left(), available(), and peek() can advance chunk framing and increase this count.

Request::availableForWrite

int availableForWrite();

Forward to Response::availableForWrite().

Request::write

size_t write(uint8_t data);
size_t write(const uint8_t* buffer, size_t bufferLength);

Forward bytes to the associated Response and return its accepted-byte count. These methods write the HTTP response; they do not modify the request body. Use res for output so its headers and write-error state are explicit. The Request’s own inherited Print::getWriteError() does not report the Response’s write errors; inspect res.getWriteError().

Request::flush

void flush();

Forward to Response::flush(). It flushes output and does not discard or consume the incoming request body.

Inherited Arduino interfaces

The board core supplies the inherited Arduino Stream API and Print API. Follow its available overloads. Stream helpers read the bounded body; Print helpers write the associated response.

Request::setTimeout

Inherited Stream::setTimeout(unsigned long) sets the current request’s timed-read timeout in milliseconds. getTimeout() reads it; App::setTimeout() initializes future Requests.

Request::find

Inherited Stream::find searches the body for a target.

Request::findUntil

Inherited Stream::findUntil adds a terminator condition.

Request::parseInt

Inherited Stream::parseInt reads an integer; validate format when zero and timeout must be distinguished.

Request::parseFloat

Inherited Stream::parseFloat reads a floating-point value.

Request::readBytes

Inherited Stream::readBytes reads with a timeout. Check its count; append any required NUL yourself.

Request::readBytesUntil

Inherited Stream::readBytesUntil adds a delimiter condition; check its count.

Request::readString

Inherited Stream::readString allocates an Arduino String.

Request::readStringUntil

Inherited Stream::readStringUntil adds a delimiter condition.

Request::print

Inherited Print::print formats values into the HTTP response. Prefer res.print() in handlers.

Request::println

Inherited Print::println formats values into the response and appends CRLF. Prefer res.println().

Migrating Request stream access

The old Request::stream() API is retired. Use Request::client(), which returns Client*. An adapter created by process(Stream*) exists only during that call. See the migration guide.

Response

Response extends Arduino Print and writes the HTTP response associated with one Request. aWOT constructs it privately and lends it to callbacks during App::process(). Do not retain it after processing. Use the retained Client for later stream or protocol I/O when ProcessResult.responseOpen is true.

By default, the first body write sends status 200 and ordinary headers. Responses default to Content-Type: text/plain when that status permits it and to a close-delimited body with Connection: close. Selecting Connection: keep-alive without Content-Length enables automatic chunked framing; an explicit Transfer-Encoding: chunked uses the same framing. Default headers do not consume the caller’s SERVER_MAX_HEADERS slots.

Response::status

void status(int code);

Emit an HTTP/1.1 status line. Set it before the first body write. A final status can be sent only once; later calls have no effect. This method does not end dispatch or emit ordinary headers by itself.

status(100), status(102), and status(103) emit and flush an interim response, then reset status and header state so a final response can follow. Headers queued before the call belong to that interim response and are cleared afterward. Status 101 is final for a protocol upgrade and retains its explicitly queued headers; call keepOpen() for a continuing upgraded connection.

HEAD and statuses below 200, 204, 205, and 304 suppress body output and final chunk terminators. Status 204 omits Content-Length and Transfer-Encoding; 205 uses Content-Length: 0. Unknown status codes use their numeric code as the reason phrase; low-memory builds include a smaller reason-phrase table.

Response::statusSent

int statusSent();

Return the current final status code, or zero before one is selected and after an interim response resets state. It describes response state, rather than proof of transport delivery.

Response::sendStatus

void sendStatus(int code);

Send a status line, ordinary headers, and the reason phrase as the response body when a body is allowed. For example, sendStatus(404) sends Not Found on platforms whose reason-phrase table contains it. HEAD and bodyless statuses suppress that payload. Codes below 200 emit only the status response.

This method does not call end() and does not enable middleware continuation. Call res.end() when a custom error callback must suppress the built-in error response, or when dispatch must stop despite req.next().

Response::set

void set(const char* name, const char* value);

Queue a header before headers are emitted. Names and values are borrowed NUL-terminated strings, so keep them alive until emission. Use valid non-null strings without CR or LF. The method stores pointers and does not validate or copy them. It appends a slot rather than replacing an existing header; avoid duplicate framing or connection headers.

The call returns no result and is ignored after headers are sent, after a write error, or when SERVER_MAX_HEADERS slots are occupied. Set required headers before body output or beginHeaders(). Content-Type, Content-Length, Transfer-Encoding: chunked, and Connection: keep-alive affect automatic defaults and framing, with case-insensitive comparisons. A supplied Content-Length must equal your actual payload length; aWOT does not verify it. Explicit chunked encoding takes precedence over a queued Content-Length.

void json(Request& req, Response& res) {
  res.status(200);
  res.set("Content-Type", "application/json");
  res.set("Content-Length", "11");
  res.print("{\"ok\":true}");
}

Response::get

const char* get(const char* name);

Return the first queued header value matching a valid name without case sensitivity, or nullptr if no caller-supplied header matches. Automatically generated defaults are not stored in these slots. This queries queued values, rather than parsing the emitted header block; keepOpen() can suppress queued framing headers during emission.

Response::setDefaults

void setDefaults();

A compatibility no-op in v4. Defaults and framing are derived by beginHeaders() from the eventual status and connection mode. Calling it does not emit or reserve headers.

Response::beginHeaders

void beginHeaders();

Start the header block, choosing status 200 if no final status exists. It writes queued and default headers but leaves the final blank line open so custom header lines can be written with Print methods. Call endHeaders() afterward. Calls are ignored when headers are already sent, headers are already being emitted, or a write error occurred. Usually set() and ordinary body writes suffice.

void manualHeader(Request& req, Response& res) {
  res.beginHeaders();
  res.print("X-Device: demo\r\n");
  res.endHeaders();
  res.print("Hello");
}

Response::endHeaders

void endHeaders();

Write the blank line ending the header block and flush buffered headers. If the block has not started, call beginHeaders() first. Repeated calls after header completion have no effect. Body writes perform both operations automatically unless a manual header block is active.

Response::headersSent

bool headersSent();

Return whether the header block has completed. Inspect getWriteError() as well when successful transport output matters.

Response::end

void end();

Mark dispatch as ended. Routers stop invoking further handlers, and App error handling suppresses its built-in fallback response. This method does not flush, close the Client, or emit a terminal chunk immediately. finally still runs, and process() performs output finalization afterward. It does not prohibit later writes by that callback or the final callback.

Response::ended

bool ended();

Return the dispatch-ended flag. A write failure also sets it. It does not indicate that the Client has been closed or the final chunk delivered.

Response::keepOpen

void keepOpen();

Request continued Client I/O after process() returns. Call before emitting ordinary HTTP headers. For status 200 or above, the initial response becomes close-delimited: it emits Connection: close and suppresses queued Content-Length, Transfer-Encoding, and Connection headers. Later output goes directly through the retained Client and ends when the caller closes it.

For status 101, upgrade headers and Connection: Upgrade are retained. Neither open HTTP streams nor protocol upgrades emit a terminal HTTP chunk. process() returns responseOpen only after successful parsing and writes. Calling this after ordinary headers were sent cannot repair their framing. Use the Server Sent Events or WebSocket helpers for their protocol framing.

Response::write

size_t write(uint8_t data);
size_t write(const uint8_t* buffer, size_t bufferLength);

Accept binary bytes into the response. The first body write emits pending status and headers. Return the count accepted, which can be less than requested on a transport failure. For a bodyless response, payload is suppressed while the return value still reports the requested payload accepted. Supply valid storage for a nonzero buffer length. For text, use the inherited print(); the derived write declarations expose the two signatures above.

Small writes may be buffered, so an accepted count does not guarantee delivery. Large writes flush the buffer and may go directly to the Client; chunked output splits writes into representable chunks. Positive short Client writes are retried from the accepted offset. A zero write stops immediately, sets the Response’s inherited Print write error, ends dispatch, closes the Client, and disables keepAlive and responseOpen. Later flush or finalization can reveal a failure after a buffered write had reported success.

void bytes(Request& req, Response& res) {
  const uint8_t body[] = {0, 1, 2, 3};
  res.set("Content-Type", "application/octet-stream");
  res.set("Content-Length", "4");
  size_t accepted = res.write(body, sizeof(body));
  res.flush();
  if (accepted != sizeof(body) || res.getWriteError()) {
    // The failed transport has been stopped by aWOT.
    return;
  }
}

Response::availableForWrite

int availableForWrite();

Return free output-buffer capacity, or zero after a write error. Capacity is the smaller of storage minus CHUNK_OVERHEAD and the size representable in the reserved hexadecimal chunk header. This describes local buffering rather than the transport’s available send capacity.

Response::flush

void flush();

Write pending buffered bytes and call the Client’s flush(). Check getWriteError() afterward. It does not end routing, close a successful client, or emit the final zero chunk. process() performs finalization once callbacks finish; it flushes pending bytes and terminates ordinary chunked responses.

Response::bytesSent

int bytesSent();

Return the accepted-byte counter, including HTTP status and header bytes as well as accepted body bytes, saturating at INT_MAX. It includes buffered bytes that might fail during a later flush. Suppressed body bytes and automatic chunk framing bytes are not counted. This is not a delivered-body-byte counter.

Response::printP

void printP(const unsigned char* string);
void printP(const char* string);

Print a NUL-terminated string from program memory. It follows ordinary response header and buffering rules, stops on write failure, and returns no byte count. Use getWriteError() to inspect failure.

void flashText(Request& req, Response& res) {
  P(message) = "Hello from program memory";
  res.printP(message);
}

Response::writeP

void writeP(const unsigned char* data, size_t length);

Write exactly length bytes from program memory, including binary zeros. It uses ordinary framing and buffering, stops on write failure, and returns no count. Supply valid program-memory storage for the requested length.

Response::print

Inherit all supported formatting overloads from the board core’s Print::print, including C strings, Arduino String, numbers, flash strings, and Printable. Return values follow accepted-write counts; later buffered failures remain possible.

Response::println

Inherit the board core’s Print::println overloads, which append CRLF to the formatted output.

Inherited write errors

Print::getWriteError() and Print::clearWriteError() are inherited. getWriteError() becomes nonzero on failed Client writes. Clearing that flag does not reconnect a closed client or restore dispatch and connection flags. Let the caller close and discard a failed response transport.

WsChannel

Include <WsChannel.h> explicitly, after <aWOT.h>. A channel holds a fixed number of copied transport handles and receives frames when you call poll(). See the complete WebSocket guide.

Constructor and capacities

template <typename TClient, int N, int MAX_ROOMS = 8>
class WsChannel;
WsChannel();

TClient must be the exact concrete client type supplied to App::process(); upgrade() casts req.client() to that type. It must support default construction and copying a handle to the same underlying connection. For example, use WsChannel<WiFiClient, 4, 1> for requests delivered as WiFiClient. The stored copy must keep the connection usable after the original handle is destroyed. ESP32 WiFiClient copies share socket ownership, so destroying the local accepted handle after a successful upgrade leaves the channel’s owner alive; explicitly stopping any copy closes the shared socket. The bundled native MacOSClient, LinuxClient, and WindowsClient adapters support the same ownership model. Their copies also share unread receive bytes, so a frame buffered during the handshake remains available to the channel. Do not pass the temporary adapter from process(Stream*) to such a channel. Keep the channel alive while it owns connections; close active sockets before discarding it. A channel is intended for single-threaded use.

N must be positive. MAX_ROOMS must be in 1..32; each socket has a 32-bit room-membership bitmap. Each of the N slots allocates a WS_RX_BUFFER_SIZE-byte message buffer and a separate 125-byte control buffer, even when idle. The default message capacity is 256 bytes per slot, including all fragments. See configuration.

upgrade

bool upgrade(Request& req, Response& res);

Returns true after sending the 101 handshake, marking the response open, copying the client into a free slot, and calling onConnect. Capture Upgrade, Connection, Sec-WebSocket-Key, and Sec-WebSocket-Version in a Request::Header array passed to process(). The request must be GET over HTTP/1.1 with no unread body; the upgrade value must be websocket, Connection must contain a complete upgrade token, the version must be 13, and the key must be a valid base64 encoding of 16 bytes. Value matching is case-insensitive where appropriate. All four captured buffers must fit their values plus NUL.

Returns false for an invalid handshake, missing capture, no available slot, or failed handshake write. It can reclaim a disconnected slot, calling onDisconnect. A write failure may already have closed the transport, so a replacement response cannot be guaranteed. The caller must retain the connection only when App::ProcessResult.responseOpen is true, and stop other clients.

void websocketRoute(Request& req, Response& res) {
  if (!sockets.upgrade(req, res)) res.sendStatus(400);
}

poll and frame handling

void poll();

Call frequently from loop(). It reads available bytes incrementally without waiting for a whole frame, dispatches completed text/binary messages, replies to ping and close frames, and checks disconnections and heartbeat timeouts. Positive short writes are retried; a zero or invalid write deactivates the slot, stops its client, and calls onDisconnect.

Incoming frames must be masked. Text and binary messages can span several frames: the first carries the message type, later frames use the continuation opcode, and the final frame sets FIN = 1. The channel assembles the complete message in its receive buffer and calls onMessage once. Ping, pong, and close frames can arrive between fragments without overwriting the message.

Protocol errors, including an unexpected continuation or a new data message before the previous one finishes, close with 1002. Messages larger than WS_RX_BUFFER_SIZE across all fragments close with 1009. Text is checked for valid UTF-8 after the final fragment; a character may span fragments. Invalid text or close reasons close with 1007. Control frames must be unfragmented and contain at most 125 payload bytes, independently of WS_RX_BUFFER_SIZE. Disconnecting or reusing a slot clears any incomplete message.

Compression extensions and TLS are not supplied by the channel; the example uses plain WebSockets. Outgoing messages use a single unmasked frame and are not limited by the receive buffer size. Keep outgoing lengths within the 32-bit frame-length implementation.

The default heartbeat sends an empty ping after 5000 ms and waits for a pong for up to 10000 ms. There is at most one outstanding heartbeat ping. Timeout stops the connection without sending a close frame. Long gaps between poll() calls delay all frame processing and heartbeat checks.

Rooms

int defineRoom(const char* name);
int findRoom(const char* name) const;
const char* roomName(int roomIdx) const;
void join(int socketId, int roomIdx);
void join(int socketId, const char* name);
void leave(int socketId, int roomIdx);
void leave(int socketId, const char* name);
bool inRoom(int socketId, int roomIdx) const;
bool inRoom(int socketId, const char* name) const;

defineRoom() returns the existing index for an equal name, even when the room table is full. A new name gets the next index, starting at zero, or -1 when MAX_ROOMS distinct names have been registered. Null names return -1 without consuming capacity; an empty name is valid. Name comparisons are case-sensitive. The channel borrows each registered name; use a string literal or storage that remains alive and unchanged for the channel’s lifetime. findRoom() returns a registered name’s index, or -1 for a null or unknown name. It never creates a room. roomName() returns the registered pointer, or null for an unnamed or out-of-range index.

join(), leave(), and inRoom() accept either a registered name or an index. Numeric operations accept any room index in 0..MAX_ROOMS-1, even before it is named. Invalid socket or room indices and null or unknown names make join() and leave() no-ops and make inRoom() return false. These membership methods do not check whether a socket is active; new upgrades and disconnections clear membership. Room indices and socket slot IDs are local to this channel. Retain the index from defineRoom() or findRoom() to avoid looking up a name on each call.

int chatRoom = sockets.defineRoom("chat");
if (chatRoom >= 0) {
  sockets.join(0, "chat");
  sockets.to("chat", "Welcome!");
  // sockets.join(0, chatRoom) uses the same room without a name lookup.
}

Sending

void send(int socketId, const char* text);
void send(int socketId, const uint8_t* data, size_t n, bool binary = false);
void to(int roomIdx, const char* text);
void to(int roomIdx, const uint8_t* data, size_t n, bool binary = false);
void to(const char* name, const char* text);
void to(const char* name, const uint8_t* data, size_t n, bool binary = false);
void broadcast(const char* text);
void broadcast(const uint8_t* data, size_t n, bool binary = false);

send() targets one active socket, to() targets active members of a room, and broadcast() targets every active socket. They return no delivery result; failed writes deactivate the affected connection and call onDisconnect. Invalid indices, null or unknown room names, and inactive send() targets are ignored. to() never creates a room. String overloads require a non-null NUL-terminated string and send a text frame. Buffer overloads send exactly n bytes; pass true for binary. Supply valid storage until the method returns. Outgoing text must be valid UTF-8; these methods do not validate it. A received payload can contain NUL bytes, so use its pointer and length rather than the string overload.

void echo(WsMessage& msg) {
  sockets.send(msg.socketId(), msg.data(), msg.length(), msg.isBinary());
}

Connection state and closure

int count() const;
bool connected(int socketId) const;
void close(int socketId, uint16_t code = 1000);

count() counts active slots. connected() reports whether an in-range slot is active; neither method probes the transport. Call poll() regularly to discover disconnects. IDs can be reused after a disconnect, so do not treat them as durable client identities.

close() sends a close frame, then immediately stops and deactivates an active socket and calls onDisconnect. Invalid/inactive IDs are ignored. Accepted codes are 1000..1014 except 1004, 1005, and 1006, or 3000..4999; an invalid code is replaced with 1002. The method does not wait for the peer’s close reply.

Callbacks

void onConnect(ConnectCb cb);
void onDisconnect(DisconnectCb cb);
void onMessage(MessageCb cb);

With automatic std::function support, the public callback types are:

typedef std::function<void(int socketId)> ConnectCb;
typedef std::function<void(int socketId)> DisconnectCb;
typedef std::function<void(WsMessage& msg)> MessageCb;

With function-pointer callbacks they are:

typedef void (*ConnectCb)(int socketId);
typedef void (*DisconnectCb)(int socketId);
typedef void (*MessageCb)(WsMessage& msg);

Each setter replaces the previous callback; nullptr clears it. onConnect runs during a successful upgrade, after the slot becomes active. onDisconnect runs after deactivation and transport closure. onMessage runs synchronously inside poll() once per completed text or binary message, after its final fragment. The WsMessage object and its data must not be retained after that callback. Captured objects in callbacks must outlive their use.

WsMessage

WsMessage is a read-only Arduino Stream over a completed WebSocket message. <WsChannel.h> includes it, or include <WsMessage.h> explicitly. A channel creates a temporary message for onMessage; copy bytes during the callback if you need them later. The payload is not NUL-terminated.

Constructor and opcodes

enum Opcode { TEXT = 0x1, BINARY = 0x2 };
WsMessage(uint8_t* buffer, size_t length, uint8_t opcode, int socketId);

The constructor borrows buffer, starts the read position at zero, and records the supplied metadata. It does not validate the pointer or opcode. The buffer must contain at least length readable bytes and outlive this message. Channel callbacks receive only complete, validated text or binary messages.

Reading and Stream methods

int available();
int read();
int peek();
size_t write(uint8_t);
void flush();

available() returns unread payload bytes. read() returns and consumes the next unsigned byte as an int, or -1 at the end. peek() returns it without advancing, or -1. write() always returns zero and flush() does nothing. Keep manually constructed payload sizes representable as int when using available().

The inherited Arduino Stream interface supplies helpers such as readBytes, readBytesUntil, readString, find, parseInt, parseFloat, and timeout configuration. They use the same read position and may wait for their configured timeout after exhausting a payload; prefer a known length() for bounded binary reads. Inherited Print formatting methods cannot send data through this read-only wrapper; send with the channel. The board core defines the complete inherited Print interface, including formatting helpers and getWriteError() / clearWriteError().

void receive(WsMessage& msg) {
  while (msg.available() > 0) Serial.write((uint8_t)msg.read());
}

Metadata and direct payload access

int socketId() const;
bool isText() const;
bool isBinary() const;
const uint8_t* data() const;
size_t length() const;

socketId() identifies the channel slot that sent the message. isText() and isBinary() compare the opcode to TEXT and BINARY. data() points to the entire original payload; length() is its total byte count. Both remain unchanged as you consume bytes with read(). Use explicit lengths for binary data and text that may contain NUL; do not pass data() to strlen().

SseChannel

Include <SseChannel.h> explicitly after <aWOT.h>. This channel writes server-sent events to copied, long-lived connection handles. See the Server Sent Events guide.

Constructor and capacities

template <typename TClient, int N, int MAX_ROOMS = 8>
class SseChannel;
SseChannel();

N must be positive and reserves N client handles plus active flags and 32-bit room-membership bitmaps. MAX_ROOMS must be in 1..32 and defaults to eight. The channel uses a fixed room table and does not allocate receive buffers. TClient must support default construction, copying a handle to the same connection, and the usual Client write, connected, and stop methods. Match it to the concrete accepted client type, such as SseChannel<WiFiClient, 4, 1>. The stored copy must keep the connection usable after the original handle is destroyed. ESP32 WiFiClient and the bundled native MacOSClient, LinuxClient, and WindowsClient adapters support this shared ownership: add() retains a copy before the local accepted handle leaves scope. Explicitly stopping any copy closes the shared connection. Keep the channel alive for the lifetime of its connections. It has no explicit close-all operation; arrange transport closure before discarding it. Use the channel from a single thread, or supply your own synchronization.

open

void open(Response& res);

Sets status 200, Content-Type: text/event-stream, and Cache-Control: no-cache, then calls res.keepOpen(). Call before emitting HTTP headers. The response is close-delimited, without Content-Length or chunk framing. open() does not add the client. After app.process() succeeds with responseOpen == true, pass the concrete client to add(); stop it otherwise. Do not retain a Response or the temporary client adapter made by process(Stream*).

add

int add(TClient client);

Copies the client into the first inactive or disconnected slot, stops any disconnected handle being replaced, clears that slot’s room membership, and invokes onConnect(clientId). Returns the selected slot index, or -1 after stopping the supplied client when all N slots are connected. Stopping any alias of a copied handle can also stop the stored connection; do not stop the original handle after adding it.

Rooms

int defineRoom(const char* name);
int findRoom(const char* name) const;
const char* roomName(int roomIdx) const;
void join(int clientId, int roomIdx);
void join(int clientId, const char* name);
void leave(int clientId, int roomIdx);
void leave(int clientId, const char* name);
bool inRoom(int clientId, int roomIdx) const;
bool inRoom(int clientId, const char* name) const;

Rooms use the same rules as WebSocket rooms. defineRoom() returns an existing index for equal, case-sensitive name contents, even at capacity; otherwise it assigns the next index or returns -1 when the table is full. Null names return -1 without consuming capacity, and empty names are valid. Names are borrowed and must remain alive and unchanged while the channel exists. findRoom() returns an index or -1 without defining a room. roomName() returns the registered pointer or null for an unnamed or invalid index.

Membership methods accept either a registered name or a numeric index in 0..MAX_ROOMS-1; numeric indices need not have names. Invalid client or room indices and null or unknown names make join() and leave() no-ops and make inRoom() return false. Membership methods do not check whether a slot is active. Adding a client, closing it, or deactivating its slot clears membership. One client can join several rooms. Client IDs and room indices are local to each channel; an SSE room and a WebSocket room have independent memberships.

Sending

void send(int clientId, const char* data);
void send(int clientId, const char* event, const char* data);
void to(int roomIdx, const char* data);
void to(int roomIdx, const char* event, const char* data);
void to(const char* name, const char* data);
void to(const char* name, const char* event, const char* data);
void broadcast(const char* data);
void broadcast(const char* event, const char* data);

send() targets one active client, to() targets active members of a room, and broadcast() targets all active clients. Invalid client or room indices, inactive target slots, and null or unknown room names are ignored. Named operations never create rooms; define them first.

Overloads without an event name send an unnamed event received through EventSource.onmessage. Supplying an event name prefixes event: <event>; browsers receive it through addEventListener(event, ...). Use an event name without CR or LF and a non-null NUL-terminated data string. A null event name selects an unnamed event. Each payload line is prefixed with data: ; CR, LF, and CRLF are normalized to LF, preserving empty and trailing lines. A final blank line ends the event. Strings are read during the call and need not remain alive afterward. Binary data and embedded NUL are not supported by these overloads.

All return void. Positive short writes are retried. Disconnected clients, zero writes, or invalid write counts stop and deactivate the affected slot. Slow transport writes can delay the loop; there is no outgoing event queue.

events.defineRoom("updates");
events.join(0, "updates");
events.send(0, "ready");
events.to("updates", "sample", "first line\nsecond line");
events.broadcast("all clients");

forEach and onConnect

void forEach(Callback fn);
void onConnect(ConnectCb callback);

With STD_FUNCTION_MIDDLEWARE, ConnectCb is std::function<void(int clientId)> and Callback is std::function<void(Print&)>. Otherwise they are function pointers with those same arguments. onConnect() replaces the callback; nullptr clears it. It runs synchronously during add(), after the slot becomes active, and receives that slot’s client ID. Use it to join rooms or send an initial event to that client. Captured objects must outlive their callbacks.

forEach() removes disconnected clients as it visits their slots and calls the supplied non-null Callback with each remaining client’s Print interface. Do not retain that reference. It is useful for comments, custom id: or retry: fields, and periodic cleanup. Writes through this Print reference bypass send()’s short-write retries and failure detection; inspect return counts and design custom writes accordingly.

void initialEvent(int clientId) {
  events.join(clientId, "updates");
  events.send(clientId, "ready", "connected");
}
// During setup(): events.defineRoom("updates"); events.onConnect(initialEvent);

Connection state and closure

int count() const;
bool connected(int clientId) const;
void close(int clientId);

count() counts active slots and connected() reports whether an in-range slot is active. Neither probes the transport. Sending methods clean up disconnected or failed target slots, forEach() checks every active slot, and add() can reclaim a disconnected slot. IDs are reused and are not durable client identities. close() immediately stops and deactivates an active client and clears its room membership; invalid or inactive IDs are ignored.

There is no poll(), disconnect callback, automatic heartbeat, event history, or replay logic. Browsers may reconnect, but the application must implement event IDs and replay if needed. Periodically call forEach() or broadcast an application event to discover disconnected clients in otherwise idle channels.

Storage and StreamClient

ArrayView

template <typename T> class ArrayView;
ArrayView();
ArrayView(T* data, int size);
template <size_t N> ArrayView(T (&data)[N]);
T* data() const;
int size() const;
T& operator[](int index) const;

Defined in <ArrayView.h> and available through <aWOT.h>. The default view has a null pointer and size zero. The pointer constructor records the pointer and explicit size; the array constructor infers the capacity and rejects arrays larger than INT_MAX at compile time. data() and size() return the recorded values. Indexing returns a reference without bounds checks, including on a const view. A view neither owns nor copies its storage and does not validate pointers or sizes. Its elements must remain alive whenever accessed.

App::ProcessOptions uses views for URL storage, output storage, and the contiguous Request::Header array. Arrays infer sizes; raw pointers require an explicit {pointer, size} pair. Request headers borrow their names and value buffers as well. Keep all of this storage, and the context, alive until process() returns. Separate concurrent requests need separate capture buffers.

char url[64];
uint8_t output[32];
char authorization[64];
Request::Header headers[] = { {"Authorization", authorization} };
App::ProcessOptions options;
options.urlBuffer = url;
options.writeBuffer = output;
options.headers = headers;
// Pointer alternatives need explicit capacities:
char* urlPointer = url;
options.urlBuffer = {urlPointer, sizeof(url)};

Null URL or output pointers select the default buffer; their supplied size is ignored. A custom URL buffer needs at least two bytes; custom output capacity must exceed CHUNK_OVERHEAD. Active header entries need non-null names and buffers and positive capacities. Invalid options close the client without running handlers or finally. Captured values are cleared before and after processing. A header view with size zero ignores its pointer and entries; negative counts and positive counts with a null array pointer are invalid. See ProcessOptions and request header capture.

StreamClient

StreamClient(Stream* stream);
int connect(IPAddress, uint16_t);
int connect(const char*, uint16_t);
size_t write(uint8_t byte);
size_t write(const uint8_t* buffer, size_t length);
int available();
int read();
int read(uint8_t* buffer, size_t length);
int peek();
void flush();
void stop();
uint8_t connected();
operator bool();

Defined in <StreamClient.h> and included by <aWOT.h>. This Arduino Client adapter borrows a non-null Stream* that must outlive it. Single-byte reads, peek, availability, writes, and flush delegate to that stream. The bulk read loops until it has read length bytes or encounters -1, returning the count read. Both connect() overloads return 1 without creating a network connection; connected() always returns 1 and boolean conversion returns true. stop() does nothing. The adapter cannot detect stream disconnection or close a serial port.

App::process(Stream*) creates a temporary adapter; req.client() points to it only during processing. Never retain that pointer or put it in a channel. For a persistent adapter, create one in persistent storage and pass its Client pointer explicitly, keeping its wrapped stream alive. Its always-connected semantics still require application-managed termination.

The inherited Arduino Stream methods and Print formatting and write-error methods are available according to your board core. The adapter’s delegated writes do not translate the wrapped stream’s write-error flag into its own Print flag.

Configuration

The configuration guide shows a complete Uno sketch and build-wide overrides. Prefer per-request storage when choosing different URL/output capacities for individual requests.

Headers and namespace

All library types are defined in namespace awot. <aWOT.h> includes App, Router, Request, Response, StreamClient, and common configuration. It adds using namespace awot; unless ENABLE_AWOT_NAMESPACE is defined. That macro controls the convenience using-directive; it does not change where types are declared. With it defined, write awot::App, awot::Request, and so on, or use your own selective using-declarations. ArrayView arrives through Request. Include <WsChannel.h>, <WsMessage.h>, or <SseChannel.h> explicitly for those APIs.

Callback selection

aWOT_common.h selects STD_FUNCTION_MIDDLEWARE automatically on ESP8266, ESP32, or compilers whose __has_include finds <functional>, unless AWOT_NO_STD_FUNCTION_MIDDLEWARE is defined. Explicitly selecting STD_FUNCTION_MIDDLEWARE also requires <functional>. In that mode, router middleware and channel callbacks use std::function: free functions, lambdas with captures, and supported member bindings are accepted. Captured references and bound instances must outlive their registrations. App::ErrorHandler always remains a function pointer.

Without std::function, middleware and channel callbacks are function pointers. Use free/static functions or convertible lambdas without captures. The member binding overloads are unavailable. To force this configuration, pass -DAWOT_NO_STD_FUNCTION_MIDDLEWARE to every library and sketch C++ translation unit. A define at the top of an .ino cannot change the compiled library ABI. Defining both selection macros is a compile-time error.

HTTP storage macros

MacroLow-memory defaultOther defaultMeaning
SERVER_URL_BUFFER_SIZE64256Default combined URL storage, including NUL.
SERVER_PUSHBACK_BUFFER_SIZE32128Bytes reserved in every Request’s pushback stack.
SERVER_OUTPUT_BUFFER_SIZE321024Default response output buffer, including chunk overhead.
SERVER_MAX_HEADERS1010Caller-supplied response header slots.

LOW_MEMORY_MCU is selected for the ATmega328P, ATmega32U4, ATmega16U4, and the _AVR_ATmega328__ target macro listed in aWOT_common.h. Those defaults apply to Uno and Leonardo; they are not measurements of total sketch memory. Default content type, connection, and framing headers do not use caller header slots. Response::set() silently ignores additional headers once SERVER_MAX_HEADERS is reached. Request header capture capacity is supplied separately by the caller.

HTTP overrides affect object layout and/or implementation. Apply them consistently to every library and sketch translation unit and rebuild the library. Use positive pushback/header capacities and a default URL capacity of at least two bytes. Output storage must exceed CHUNK_OVERHEAD.

CHUNK_HEADER_SIZE is derived from SERVER_OUTPUT_BUFFER_SIZE: it is 3 for capacities through 0xF, 4 through 0xFF, 5 through 0xFFF, 6 through 0xFFFF, and 10 above that. CHUNK_TRAILER_SIZE is 2; CHUNK_OVERHEAD is their sum; CHUNK_DATA_SIZE subtracts the overhead from the default output capacity. These are derived macros, not separate override points. The chunk-header reservation also limits custom output buffers. CRLF is the "\r\n" protocol string.

P and flash strings

P(message) = "Hello from flash!";
// Inside a route handler:
res.printP(message);

P(name) declares a static const unsigned char array in program memory. On AVR it uses a named .progmem section; elsewhere it uses PROGMEM. It is a declaration macro; supply the initializer and semicolon. Use printP() for a NUL-terminated string and writeP(data, length) for exact bytes. Flash storage does not change the pointer lifetime requirements for request or response headers; their ordinary C strings must be readable in the platform’s normal address space.

Channel macros

MacroDefaultMeaning
WS_RX_BUFFER_SIZE256Complete incoming message capacity in bytes, including all fragments; allocated in every WebSocket slot.
WS_PING_INTERVAL_MS5000ULTime from the previous heartbeat ping to sending the next when no pong is pending.
WS_PING_TIMEOUT_MS10000ULTime to wait for the outstanding heartbeat pong before stopping the connection.

Define these before including <WsChannel.h>. Use positive capacities and intervals appropriate for your application. Setting a heartbeat interval to zero does not disable heartbeat; it makes a ping due immediately. Channel definitions must agree across translation units that instantiate the same channel type. Both WsChannel<TClient, N, MAX_ROOMS> and SseChannel<TClient, N, MAX_ROOMS> use template parameters for slot and room capacities; these are not macros. SSE has no automatic heartbeat configuration. Each WebSocket slot also reserves a separate 125-byte control buffer. Large capacities can exhaust small boards; the Uno SerialBridge example uses one slot with a 64-byte message buffer plus the control buffer.