Request data
A handler reads the URL, selected headers, and body from Request. Query,
form, and route extraction return a boolean; check it before using their
buffers. Capacities include the terminating NUL byte.
Capture headers per request
aWOT stores only the application headers you ask it to capture. Declare a
contiguous Request::Header array and pass it to process(client, headers),
or assign it to ProcessOptions.headers. Arrays infer capacities and counts.
Names are compared without regard to case.
req.get("X-Label") returns null when that header is absent or was not
captured. A present header with an empty value returns a non-null empty string.
Buffers and presence state are cleared before parsing and after processing.
Read or copy the value inside a handler; it does not survive process().
Query values
req.path() is percent-decoded; after successful parsing, req.query() returns
the raw query without the ?, or an empty string if there was no query. Named
req.query(name, buffer, capacity)
decodes the name and value and treats + as a space. A literal + stays literal
in a path. Invalid percent escapes and decoded NUL anywhere in the request
target are rejected with 400 before routing.
A false result can mean a missing query value, invalid input, or insufficient space. An overflow leaves a NUL-terminated partial result, which the example rejects. Invalid pointers and nonpositive capacities fail without touching the buffer.
Form values
req.form(name, nameCapacity, value, valueCapacity) consumes the next
application/x-www-form-urlencoded field from the body and decodes its name
and value. It also treats + as a space. aWOT does not select this parser based
on Content-Type; your handler chooses when to call it. It is a sequential
parser, rather than a lookup by field name.
A false result can mean invalid input or insufficient space. An overflow leaves
a NUL-terminated partial result, which the example rejects, and consumes through
the raw & delimiter or end of body so a later call can read the next field.
Invalid pointers and nonpositive capacities fail without touching the buffers.
Headers, query, form, context, and custom storage
This complete ESP32 sketch demonstrates three routes. GET /inspect?name=Ada
reads a query and captured header; POST /form reads form fields; PUT /echo
reads and returns a bounded body. Its per-request context measures processing
time, and custom buffers replace the default URL and output storage.
The form and echo routes require a known, nonzero Content-Length of at most
128 bytes; they reject chunked requests because the total length is unknown.
#include <WiFi.h>
#include <aWOT.h>
#include <string.h>
WiFiServer server(80);
App app;
struct RequestState {
unsigned long started;
int status;
};
void inspect(Request& req, Response& res) {
char name[32];
if (!req.query("name", name, sizeof(name))) {
res.sendStatus(400);
return;
}
const char* label = req.get("X-Label");
res.print("Name: ");
res.println(name);
res.print("Label: ");
res.println(label ? label : "(absent)");
}
void readForm(Request& req, Response& res) {
const char* type = req.get("Content-Type");
if (!type || strcmp(type, "application/x-www-form-urlencoded") != 0) {
res.sendStatus(415);
return;
}
if (req.left() <= 0 || req.left() > 128) {
res.sendStatus(400);
return;
}
// Validate every field before starting the success response.
unsigned int fields = 0;
while (req.left() > 0) {
char name[16];
char value[32];
if (!req.form(name, sizeof(name), value, sizeof(value))) {
res.sendStatus(400);
return;
}
++fields;
}
res.print("Fields received: ");
res.print(fields);
}
void echo(Request& req, Response& res) {
if (req.left() <= 0 || req.left() > 128) {
res.sendStatus(400);
return;
}
char body[128];
size_t length = req.left();
if (req.readBytes(body, length) != length || req.left() != 0) {
res.sendStatus(400);
return;
}
res.set("Content-Type", "application/octet-stream");
res.write((const uint8_t*)body, length);
}
void finish(Request& req, Response& res) {
RequestState* state = static_cast<RequestState*>(req.context);
state->status = res.statusSent();
Serial.print("HTTP status: ");
Serial.print(state->status);
Serial.print("; elapsed ms: ");
Serial.println(millis() - state->started);
}
void setup() {
Serial.begin(115200);
WiFi.begin("YOUR_SSID", "YOUR_PASSWORD");
while (WiFi.status() != WL_CONNECTED) delay(500);
Serial.println(WiFi.localIP());
app.get("/inspect", inspect);
app.post("/form", readForm);
app.put("/echo", echo);
app.finally(finish);
server.begin();
}
void loop() {
WiFiClient client = server.available();
if (client.connected()) {
char label[32];
char contentType[48];
Request::Header headers[] = {
{"X-Label", label}, {"Content-Type", contentType}
};
char url[64];
uint8_t output[32];
RequestState state = {millis(), 0};
App::ProcessOptions options;
options.headers = headers;
options.urlBuffer = url;
options.writeBuffer = output;
options.context = &state;
app.process(&client, options);
client.stop();
}
}
Set your credentials and use the printed address:
curl --header 'X-Label: example' 'http://192.168.1.140/inspect?name=Ada+Lovelace'
curl --data 'name=Ada&city=Helsinki' http://192.168.1.140/form
curl --request PUT --data-binary 'hello' http://192.168.1.140/echo
Body lengths and reads
For Content-Length bodies, req.left() reports unread body bytes, including
pushed-back bytes. For chunked bodies, it returns -1 until the zero chunk and
final empty trailer line have been consumed, even if pushback contains bytes.
It then returns the pushback count or zero. Use req.left() != 0 when a loop
must also handle chunked bodies, and handle failed reads so incomplete input
does not leave the loop spinning.
Ordinary read() is nonblocking and returns -1 when no byte is available;
the buffer overload can read across chunks. Inherited readBytes() waits up
to the request’s Stream timeout and returns the number actually read. Both
decode chunked payload without allocating a body buffer. Check returned counts
and body completion. available() reports ready payload from the current
chunk or remaining Content-Length, plus pushback, capped at INT_MAX; it does
not prove a complete body has arrived. Both left() and available() can
consume ready chunk framing without waiting for input. bytesRead() counts
transport bytes, including the request line, headers, chunk framing, and
trailers, once; it is not the decoded body length.
Content-Length is decimal, bounded by INT_MAX, and allows surrounding spaces
or tabs. Repeated lengths must agree. HTTP/1.1 accepts a single
Transfer-Encoding: chunked field. Each chunk can declare up to UINT32_MAX
bytes, and the complete body can span multiple chunks. Extensions are ignored;
trailers are consumed and discarded without changing captured headers. Comma
lists or conflicting Content-Length values, Content-Length combined with
Transfer-Encoding, repeated or unsupported transfer codings, and transfer
coding on HTTP/1.0 are rejected with 400 and closure. Without either framing
header, the body length is zero.
Malformed chunk framing closes the connection. If no response status has been
sent when routing finishes, aWOT invokes onError with 400 and supplies its
usual fallback response; it does not replace an already-started response.
Check short or timed-out reads yourself: inherited Stream helpers do not
guarantee an onError call for incomplete input.
Read or discard the complete body before retaining an HTTP connection. An incomplete chunked body prevents both keep-alive reuse and an open-response handoff. On rejected requests, this example closes the connection. See connections and JSON for structured body parsing.
Borrowed storage and context
ProcessOptions uses ArrayView: it borrows existing arrays without allocating
or copying them. All names, header buffers, URL/output buffers, and context
must remain alive throughout process(). Concurrent calls need separate
storage. The caller owns context; handlers receive the same pointer through
req.context. Copy any data needed afterward into your own storage.
Arrays infer their sizes. This partial equivalent shows the explicit sizes needed when your variables are raw pointers; supply their actual capacities and header count:
// Inside a caller that owns these pointers and their storage.
Request::Header capture("X-Label", labelPointer, labelCapacity);
App::ProcessOptions options;
options.urlBuffer = {urlPointer, urlCapacity};
options.writeBuffer = {outputPointer, outputCapacity};
options.headers = {&capture, 1};
options.context = contextPointer;
app.process(&client, options);
Null URL or output buffer pointers choose default storage. A custom URL buffer
needs at least two bytes; output capacity must exceed CHUNK_OVERHEAD.
Active header entries need a non-null name and buffer with positive capacity.
Invalid options stop the client before any route, error, or final callback.
req.client() returns the supplied Client*. If you call process(Stream*),
it points to a temporary StreamClient adapter that expires on return. Never
retain that pointer, or a Request/Response pointer, for later use.