On this page

Migrating to aWOT 4

Install aWOT 4.0.0 through Arduino Library Manager and update sketches using the changes below. The 3.x API remains available for existing applications; the current reference documents v4. The snippets on this page are partial before-and-after examples. Replace the indicated declarations, handlers, or call sites in your existing sketch.

Application becomes App

Keep including <aWOT.h>, but rename the application class:

// 3.x
Application app;
// 4.x
App app;

If a helper function takes an app or router by value, change it to a reference: for example, void addRoutes(App& app) or void addRoutes(Router& router). aWOT 4 rejects copies of these objects because each instance owns its registered routes. Sketches that register routes directly on a global app are unaffected.

The v4 library has multiple source and header files; install the whole library instead of copying the old aWOT.h and aWOT.cpp pair into a sketch.

Middleware continues explicitly

In 3.x, returning from a handler allowed the next matching registration to run unless the response was ended. In v4, returning stops dispatch; call req.next() when you want to continue:

// 3.x logger: returning continued dispatch.
void logRequest(Request& req, Response& res) {
  Serial.println(req.path());
}
// 4.x logger
void logRequest(Request& req, Response& res) {
  Serial.println(req.path());
  req.next();
}

Use this change in authentication middleware, multiple registrations for one route, and handlers inside mounted routers. Keep terminal error paths without req.next(). res.end() still marks a response as ended and prevents further route dispatch. The final callback still runs before finalization.

Header capture belongs to process()

Replace application-wide header registration:

// 3.x: declaration and setup registration
char authorization[64];
// Inside setup():
app.header("Authorization", authorization, sizeof(authorization));

with caller-owned capture storage for each processing call:

// 4.x: inside the connected-client branch of loop()
char authorization[64];
Request::Header headers[] = { {"Authorization", authorization} };
app.process(&client, headers);
client.stop();

The array is contiguous; capacities and entry counts are inferred. Alternatively assign it to ProcessOptions.headers. Raw buffers need Request::Header{"Authorization", pointer, capacity}. Names, capture entries, and buffers must live through process() and be separate for concurrent calls. Values are cleared before and after the call.

In a handler, req.get(name) now returns null for an absent or uncaptured header. A present empty header returns a non-null empty string. Check for null before comparing or dereferencing it. Copy values that you need after processing.

Context and buffers use ProcessOptions

Replace positional context and buffer arguments with options. This example assumes client and application context state already exist in the caller:

// 3.x
char url[64];
uint8_t output[32];
app.process(&client, url, sizeof(url), output, sizeof(output), &state);
// 4.x
char url[64];
uint8_t output[32];
App::ProcessOptions options;
options.urlBuffer = url;
options.writeBuffer = output;
options.context = &state;
app.process(&client, options);
client.stop();

req.context still exposes the supplied pointer. The new ArrayView fields borrow storage: arrays infer sizes; raw pointers require {pointer, size}. There are no separate option length/count fields or linked HeaderNode entries. Null URL/output pointers select defaults. Supplied URL storage needs at least two bytes; output storage must exceed CHUNK_OVERHEAD. Invalid options stop the client without invoking routes, onError, or finally.

Request::stream() becomes Request::client()

In v3, req.stream() returned a Stream* for the underlying connection. In v4, this method is replaced by req.client(), which returns a Client*. If your handler calls req.stream(), update it as follows:

// 3.x: inside a handler
Stream* transport = req.stream();
// 4.x: inside a handler
Client* transport = req.client();

Router mount matching

Router mounts now match complete path segments: a router mounted at /api receives /api and /api/users, but no longer /apiculture. Trailing slashes on mount paths are ignored, so /api/ and /api now behave the same. Check any mounts that relied on partial names or trailing slashes. See routing for examples.

Check the processing result

process() now returns App::ProcessResult instead of void. For the common close-after-response case, keep an explicit client.stop() after processing. For an application already managing retained clients, use the result:

// client is stored outside loop(), or in a persistent client pool.
App::ProcessResult result = app.process(&client);
if (!result.keepAlive && !result.responseOpen) {
  client.stop();
}

keepAlive requires an explicit Connection: keep-alive response header, a consumed request body, successful parsing and writes, and no open response. HTTP/1.1 alone does not enable reuse. The caller implements idle timeouts and closes rejected or expired clients.

The result does not retain the transport. ESP32 closes the socket when the last WiFiClient owner is destroyed, so a local client inside loop() still closes at scope exit even when keepAlive is true. Keep the handle at file scope or in persistent storage, as shown in the complete connection reuse sketch.

res.keepOpen() leaves an HTTP stream or protocol upgrade open after processing. Call it before emitting ordinary HTTP headers, then retain the client only if responseOpen is true. Ordinary open responses are close-delimited; aWOT removes length/chunk framing and sets Connection to close. A 101 upgrade retains its upgrade headers. Follow the complete connection examples, WebSocket guide, and Server Sent Events guide for ownership and cleanup.

Parsing, responses, and errors

Recheck applications that depended on permissive parsing. path() is decoded and query() is raw; named query/form decoding treats + as a space, while path decoding preserves +. Invalid target escapes or decoded NUL produce 400. Query/form capacities include NUL, and extraction can fail; always check the returned boolean. HTTP/1.1 requests support a single Transfer-Encoding: chunked field. Repeated or unsupported transfer codings, transfer coding on HTTP/1.0, and Content-Length combined with Transfer-Encoding are rejected with 400. Repeated Content-Length values must agree and fit INT_MAX. Chunked reads decode payload directly; extensions are ignored and trailers are discarded. Each chunk size can be up to UINT32_MAX.

For chunked requests, req.left() returns -1 until the final chunk and trailers have been consumed, then the remaining pushback count or zero. Change generic body loops from req.left() > 0 to req.left() != 0, and handle failed reads. Code that requires a known total length can continue rejecting negative left() values. An incomplete chunked body allows neither connection reuse nor an open-response handoff. Malformed chunk framing closes the connection and, if no response status was sent when routing finishes, invokes onError with 400; an existing response is not replaced.

app.onError(handler) adds a shared parser and missing-route error callback. A custom error response must call res.end() to suppress the built-in fallback. finally runs before response finalization, including after parser errors; invalid process options bypass it.

Positive short writes are retried. A zero transport write closes the client and sets getWriteError(); buffered acceptance is not proof of delivery. bytesSent() counts accepted response bytes, including headers. HEAD requires an explicit route and suppresses its payload; bodyless statuses also suppress output. Interim responses reset state for a final response. setDefaults() is retained as a compatibility no-op; defaults are chosen when headers are emitted.

Callback and configuration changes

Supported platforms now select std::function automatically. They accept capturing lambdas and member callbacks such as app.get("/count", &counter, &Counter::handle). Keep the instance alive while registered. Function-pointer builds accept ordinary functions and noncapturing lambdas. Apply AWOT_NO_STD_FUNCTION_MIDDLEWARE and HTTP layout overrides to every library and sketch translation unit, rather than defining them only in a sketch. See configuration.

Earlier pre-release v4 SSE sketches

The following changes apply to sketches using the earlier pre-release v4 SSE helper. Sending now follows the same recipient selection as WebSockets:

Earlier v4 SSE callUpdated call
events.send("data")events.broadcast("data")
events.send("sample", "data")events.broadcast("sample", "data")

Use events.send(clientId, data) or events.send(clientId, event, data) to send to one active client. events.to(room, data) and events.to(room, event, data) target room members; room can be an integer index or a registered name. Broadcast calls still send to every active client.

Change connection callbacks from void callback(Print&) to void callback(int clientId). The callback can join a room or send an initial event with events.join(clientId, "chat") and events.send(clientId, "ready", "connected"). Register the room with events.defineRoom("chat") before accepting clients. forEach callbacks continue to receive Print& for comments or custom fields.

add(client) now returns the assigned client slot ID, or -1 when the channel is full and stops the supplied client. count() and connected(clientId) report active slots; close(clientId) stops one client. IDs are reused, and new clients start with no room memberships. Sending and forEach() detect disconnected clients and clean up their slots. SSE has no poll() or onDisconnect callback.

SseChannel<TClient, N, MAX_ROOMS> defaults to 8 rooms, so existing declarations with only a client type and capacity remain valid. Both channels support defineRoom(), findRoom(), roomName(), join(), leave(), and inRoom() with the same room behavior: case-sensitive names, duplicate registration returning the existing index, and at most 32 rooms. Names are borrowed and must remain valid and unchanged for the channel’s lifetime. See the SSE guide for the complete connection flow.