The Verdict: Filtering Query Parameters in ESPAsyncWebServer

The most robust way to filter query parameters in an ESP32 async web server is to use the request->hasParam("key") guard before calling getParam(), paired with the modern mathieucarbou/ESPAsyncWebServer fork to prevent heap fragmentation on ESP32 Core 3.x. Never chain request->getParam("key")->value() directly without checking for existence first; a missing parameter returns a null pointer and will instantly trigger a watchdog reset.

Use this decision tree to determine the exact method for your routing logic:

Scenario Required Method Concrete Pick / Action
Checking if a parameter exists in the URL request->hasParam("key") Always use this as your primary if() gate.
Extracting POST body form data instead of URL GET request->getParam("key", true) Pass true as the second argument to target the POST body.
Parsing numeric values safely param->value().toInt() Wrap in bounds checking: if (val >= 0 && val <= 100).
Handling mixed GET and POST on the same route Iterate request->params() Use a for loop with request->getParam(i) to catch all.
Choosing the underlying library for ESP32 Core 3.x Original vs. Maintained Fork DEFAULT PICK: Install mathieucarbou/ESPAsyncWebServer via PlatformIO/Arduino IDE.

Hardware & Environment Spec Sheet

This build targets the standard 30-pin ESP32 DevKit V1. We are pairing the web server with an I2C environmental sensor to demonstrate filtering data requests based on URL parameters (e.g., /api?metric=temp&unit=f).

Difficulty Rating: Intermediate (Requires understanding of HTTP methods and I2C addressing)
Estimated Build Time: 45 minutes

Parts List

  • Microcontroller: ESP32-WROOM-32 DevKit V1 (30-pin, 4MB Flash)
  • Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652) or generic equivalent
  • Wiring: 22 AWG silicone jumper wires
  • Power: 5V 2A USB-C power supply (do not rely on laptop USB ports for WiFi TX spikes)

Pin Mapping Table

Component Pin Label ESP32 GPIO Notes
BME280 VIN / VCC 3V3 Do not use 5V; the BME280 is strictly 3.3V logic.
BME280 GND GND Common ground required.
BME280 SDA GPIO 21 Default I2C SDA for ESP32 DevKit V1.
BME280 SCL GPIO 22 Default I2C SCL for ESP32 DevKit V1.

Complete Compilable Code: Async Parameter Filtering

The following code is compiled against ESP32 Core 3.x. It sets up an asynchronous server, validates incoming query parameters, handles missing keys gracefully, and serves JSON data from the BME280. Pin definitions and error handling are explicitly included.

#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <Wire.h>
#include <Adafruit_BME280.h>

// --- PIN & HARDWARE DEFINITIONS ---
#define PIN_I2C_SDA 21
#define PIN_I2C_SCL 22
#define BME_I2C_ADDR 0x76 // Change to 0x77 if your breakout uses the alternate address

// --- NETWORK CREDENTIALS ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// --- GLOBAL OBJECTS ---
AsyncWebServer server(80);
Adafruit_BME280 bme;

// Helper: Convert Celsius to Fahrenheit
float cToF(float c) { return (c * 9.0 / 5.0) + 32.0; }

void setup() {
  Serial.begin(115200);
  while(!Serial) { delay(10); }

  // Initialize I2C with explicit pins
  Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
  
  if (!bme.begin(BME_I2C_ADDR, &Wire)) {
    Serial.println("[FATAL] Could not find a valid BME280 sensor, check wiring & I2C address!");
    while (1) { delay(100); } // Halt execution
  }
  Serial.println("[OK] BME280 initialized.");

  // Connect to WiFi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\n[OK] Connected. IP: %s\n", WiFi.localIP().toString().c_str());

  // --- ROUTE DEFINITION & PARAMETER FILTERING ---
  server.on("/api", HTTP_GET, [](AsyncWebServerRequest *request){
    
    // 1. Filter: Check if the required 'metric' parameter exists
    if (!request->hasParam("metric")) {
      request->send(400, "application/json", "{\"error\": \"Missing required parameter: metric\"}");
      return;
    }

    // 2. Safely extract the parameter value
    String metric = request->getParam("metric")->value();
    
    // 3. Filter: Check optional 'unit' parameter (default to Celsius)
    String unit = "c";
    if (request->hasParam("unit")) {
      unit = request->getParam("unit")->value();
      unit.toLowerCase(); // Normalize input
    }

    // 4. Route logic based on filtered parameters
    if (metric == "temp") {
      float temp = bme.readTemperature();
      if (unit == "f") temp = cToF(temp);
      request->send(200, "application/json", "{\"temperature\": " + String(temp, 2) + ", \"unit\": \"" + unit + "\"}");
    } 
    else if (metric == "humidity") {
      float hum = bme.readHumidity();
      request->send(200, "application/json", "{\"humidity\": " + String(hum, 1) + ", \"unit\": \"%\"}");
    } 
    else {
      // Handle unrecognized metric values
      request->send(422, "application/json", "{\"error\": \"Unsupported metric. Use 'temp' or 'humidity'.\"}");
    }
  });

  server.begin();
}

void loop() {
  // AsyncWebServer handles requests in the background via interrupts/FreeRTOS tasks.
  // Keep the main loop empty or use it for non-blocking sensor polling.
  delay(1000);
}

Debugging: "Guru Meditation Error" & Null Param Crashes

When filtering parameters, the most common catastrophic failure is attempting to read a value from a parameter that wasn't sent in the HTTP request. This results in a null pointer dereference inside the AsyncTCP stack.

Exact Error String:
Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.
Often accompanied by: PC: 0x400... AsyncWebServerRequest::getParam in the backtrace.

Ranked Causes

  1. Missing hasParam() Guard: You called request->getParam("key")->value() directly. If "key" is absent, getParam returns NULL, and calling ->value() on NULL triggers the LoadProhibited panic.
  2. GET vs POST Mismatch: You sent data via a POST form body, but your code is searching the URL query string (the default behavior of getParam).
  3. Heap Fragmentation: Using the abandoned me-no-dev library on ESP32 Core 3.x causes memory leaks during parameter parsing, eventually leading to allocation failures and subsequent panics.

The First 3 Things to Check When It Fails

  1. Verify the Null Guard: Search your code for ->value(). Ensure every single instance is wrapped in an if (request->hasParam("...")) block.
  2. Check the HTTP Method Flag: If you are parsing POST data, ensure you are calling request->getParam("key", true) (note the true boolean flag to search the POST body instead of the URL).
  3. Inspect URL Encoding: If your parameter contains spaces or special characters (e.g., ?name=John Doe), the browser might drop or mangle it. Ensure your client is URL-encoding the string (e.g., ?name=John%20Doe) before transmission.

Extending and Simplifying the Build

Depending on your project phase, you may need to scale this architecture up for production or strip it down for a quick bench test.

How to Extend

  • Add LittleFS for Frontend Hosting: Use server.serveStatic("/", LittleFS, "/").setDefaultFile("index.html") to host a React or Vanilla JS frontend that uses fetch() to query your /api endpoint asynchronously.
  • Implement MQTT Fallback: If the WiFi stack drops, buffer your BME280 readings in a FreeRTOS queue and publish them via MQTT when the connection restores, ensuring no data is lost during HTTP timeouts.
  • Add Parameter Rate Limiting: Track the client IP via request->client()->remoteIP() and use a simple hash map to drop requests exceeding 10 per second to prevent ESP32 lockups from DDoS-style browser refresh spam.

How to Simplify

If you are just testing the async routing logic and don't have a BME280 on hand, strip the I2C code entirely. Replace the sensor reads with millis() to return a constantly changing timestamp. This isolates the network stack from hardware faults, allowing you to verify that your query parameter filtering logic correctly handles malformed URLs without waiting for I2C timeouts.