Repository: hoeken/PsychicHttp Branch: master Commit: 1b63cf4db65f Files: 158 Total size: 862.7 KB Directory structure: gitextract_tbu1jugx/ ├── .clang-format ├── .github/ │ └── workflows/ │ ├── arduino.yml │ ├── esp-idf.yml │ ├── platformio.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── RELEASE.md ├── benchmark/ │ ├── arduinomongoose/ │ │ ├── .gitignore │ │ ├── include/ │ │ │ └── README │ │ ├── lib/ │ │ │ └── README │ │ ├── platformio.ini │ │ ├── src/ │ │ │ └── main.cpp │ │ └── test/ │ │ └── README │ ├── comparison.ods │ ├── espasyncwebserver/ │ │ ├── .gitignore │ │ ├── include/ │ │ │ └── README │ │ ├── lib/ │ │ │ └── README │ │ ├── platformio.ini │ │ ├── src/ │ │ │ ├── main.cpp │ │ │ └── secret.h │ │ └── test/ │ │ └── README │ ├── eventsource-client-test.js │ ├── http-client-test.js │ ├── loadtest-http.sh │ ├── loadtest-websocket.sh │ ├── package.json │ ├── parse-http-test.js │ ├── parse-websocket-test.js │ ├── psychichttp/ │ │ ├── .gitignore │ │ ├── include/ │ │ │ └── README │ │ ├── lib/ │ │ │ └── README │ │ ├── platformio.ini │ │ ├── src/ │ │ │ ├── main.cpp │ │ │ └── secret.h │ │ └── test/ │ │ └── README │ ├── psychichttps/ │ │ ├── .gitignore │ │ ├── data/ │ │ │ ├── server.crt │ │ │ └── server.key │ │ ├── include/ │ │ │ └── README │ │ ├── lib/ │ │ │ └── README │ │ ├── platformio.ini │ │ ├── src/ │ │ │ ├── main.cpp │ │ │ └── secret.h │ │ └── test/ │ │ └── README │ ├── results/ │ │ ├── arduinomongoose-http-loadtest.log │ │ ├── arduinomongoose-websocket-loadtest.log │ │ ├── espasync-http-loadtest.log │ │ ├── espasync-websocket-loadtest.log │ │ ├── psychic-http-loadtest.log │ │ ├── psychic-ssl-http-loadtest.log │ │ ├── psychic-ssl-websocket-loadtest.log │ │ ├── psychic-v1.1-http-loadtest.log │ │ ├── psychic-v1.1-websocket-loadtest.log │ │ └── psychic-websocket-loadtest.log │ └── websocket-client-test.js ├── component.mk ├── examples/ │ ├── esp-idf/ │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── README.md │ │ ├── data/ │ │ │ ├── custom.txt │ │ │ ├── server.crt │ │ │ ├── server.key │ │ │ ├── www/ │ │ │ │ ├── index.html │ │ │ │ └── text.txt │ │ │ └── www-ap/ │ │ │ └── index.html │ │ ├── include/ │ │ │ └── README │ │ ├── lib/ │ │ │ └── README │ │ ├── main/ │ │ │ ├── CMakeLists.txt │ │ │ ├── idf_component.yml │ │ │ ├── main.cpp │ │ │ └── secret.h │ │ ├── partitions_custom.csv │ │ └── sdkconfig.defaults │ ├── platformio/ │ │ ├── .gitignore │ │ ├── data/ │ │ │ ├── custom.txt │ │ │ ├── server.crt │ │ │ ├── server.key │ │ │ ├── www/ │ │ │ │ ├── index.html │ │ │ │ ├── text.txt │ │ │ │ └── websocket-test.html │ │ │ └── www-ap/ │ │ │ └── index.html │ │ ├── platformio.ini │ │ └── src/ │ │ ├── main.cpp │ │ └── secret.h │ └── websockets/ │ ├── .gitignore │ ├── data/ │ │ └── www/ │ │ └── index.html │ ├── include/ │ │ └── README │ ├── lib/ │ │ └── README │ ├── platformio.ini │ ├── src/ │ │ ├── main.cpp │ │ └── secret.h │ └── test/ │ └── README ├── idf_component.yml ├── library.json ├── library.properties ├── middleware.md ├── partitions-4MB.csv ├── platformio.ini ├── request flow.drawio └── src/ ├── ChunkPrinter.cpp ├── ChunkPrinter.h ├── MultipartProcessor.cpp ├── MultipartProcessor.h ├── PsychicClient.cpp ├── PsychicClient.h ├── PsychicCore.h ├── PsychicEndpoint.cpp ├── PsychicEndpoint.h ├── PsychicEventSource.cpp ├── PsychicEventSource.h ├── PsychicFileResponse.cpp ├── PsychicFileResponse.h ├── PsychicHandler.cpp ├── PsychicHandler.h ├── PsychicHttp.h ├── PsychicHttpServer.cpp ├── PsychicHttpServer.h ├── PsychicHttpsServer.cpp ├── PsychicHttpsServer.h ├── PsychicJson.cpp ├── PsychicJson.h ├── PsychicMiddleware.cpp ├── PsychicMiddleware.h ├── PsychicMiddlewareChain.cpp ├── PsychicMiddlewareChain.h ├── PsychicMiddlewares.cpp ├── PsychicMiddlewares.h ├── PsychicRequest.cpp ├── PsychicRequest.h ├── PsychicResponse.cpp ├── PsychicResponse.h ├── PsychicRewrite.cpp ├── PsychicRewrite.h ├── PsychicStaticFileHander.cpp ├── PsychicStaticFileHandler.h ├── PsychicStreamResponse.cpp ├── PsychicStreamResponse.h ├── PsychicUploadHandler.cpp ├── PsychicUploadHandler.h ├── PsychicVersion.h ├── PsychicWebHandler.cpp ├── PsychicWebHandler.h ├── PsychicWebParameter.h ├── PsychicWebSocket.cpp ├── PsychicWebSocket.h ├── TemplatePrinter.cpp ├── TemplatePrinter.h ├── UrlEncode.cpp ├── UrlEncode.h ├── async_worker.cpp ├── async_worker.h ├── http_status.cpp └── http_status.h ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ Language: Cpp BasedOnStyle: LLVM AccessModifierOffset: -2 AlignConsecutiveMacros: true AllowAllArgumentsOnNextLine: false AllowAllParametersOfDeclarationOnNextLine: false AllowShortIfStatementsOnASingleLine: false BinPackArguments: false ColumnLimit: 0 ContinuationIndentWidth: 2 FixNamespaceComments: false IndentAccessModifiers: true IndentCaseLabels: true IndentPPDirectives: BeforeHash IndentWidth: 2 NamespaceIndentation: All PointerAlignment: Left ReferenceAlignment: Left TabWidth: 2 UseTab: Never BreakBeforeBraces: Linux AllowShortLambdasOnASingleLine: All AlignAfterOpenBracket: DontAlign ================================================ FILE: .github/workflows/arduino.yml ================================================ name: Arduino Lint on: push: branches: [] pull_request: branches: [] schedule: - cron: "0 1 * * 6" # Every Saturday at 1AM concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: arduino/arduino-lint-action@v1 with: library-manager: update project-type: library compliance: strict ================================================ FILE: .github/workflows/esp-idf.yml ================================================ name: ESP-IDF on: push: branches: [] pull_request: branches: [] schedule: - cron: "0 1 * * 6" # Every Saturday at 1AM concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: name: "ESP-IDF ${{ matrix.idf_ver }}" runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: idf_ver: ["v4.4.8", "release-v5.5"] # "release-v6.0" removed until arduino-esp32 v4.0 ships with ESP-IDF v6.0 support idf_target: ["esp32", "esp32s3"] steps: - uses: actions/checkout@v4 with: path: ${{ github.workspace }}/app - name: Prepare Secret run: cp ${{ github.workspace }}/app/examples/esp-idf/main/secret.h ${{ github.workspace }}/app/examples/esp-idf/main/_secret.h - name: Compile uses: espressif/esp-idf-ci-action@v1 with: esp_idf_version: ${{ matrix.idf_ver }} target: ${{ matrix.idf_target }} path: app/examples/esp-idf command: apt-get update && apt-get install -y python3-venv && idf.py build ================================================ FILE: .github/workflows/platformio.yml ================================================ name: Platform IO on: push: branches: [] pull_request: branches: [] schedule: - cron: "0 1 * * 6" # Every Saturday at 1AM concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: build: name: "pio:${{ matrix.board }}:${{ matrix.platform }}:${{ matrix.flags }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: flags: ["-DPSY_ENABLE_SSL", "-DPSY_ENABLE_REGEX"] platform: [ "stable" ] board: [ "esp32dev", "esp32-s2-saola-1", "esp32-s3-devkitc-1", "esp32-c3-devkitc-02", "esp32-c6-devkitc-1", ] steps: - uses: actions/checkout@v4 - name: Cache PlatformIO uses: actions/cache@v4 with: key: ${{ runner.os }}-pio path: | ~/.cache/pip ~/.platformio - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.13" - name: Install Platform IO run: | python -m pip install --upgrade pip pip install --upgrade platformio - name: Install Checked out PsychicHttp run: pio lib -g install -f $GITHUB_WORKSPACE - name: Prepare Secret run: | cd examples/platformio cp src/secret.h src/_secret.h - run: PIO_BOARD=${{ matrix.board }} PIO_PLATFORM="https://github.com/pioarduino/platform-espressif32/releases/download/${{ matrix.platform }}/platform-espressif32.zip" PLATFORMIO_BUILD_FLAGS="${{ matrix.flags }}" pio run -e ci ================================================ FILE: .github/workflows/release.yml ================================================ --- name: Release to Platform IO and Arduino on: release: types: - released jobs: release_number: name: Release Number Validation runs-on: ubuntu-latest steps: ### Check the version number in the code matches the tag number - uses: actions/checkout@v4 - name: Retrieve the version number(s) run: | TAG_VERSION=$(sed "s/^v//" <<< $GITHUB_REF_NAME) PLATFORMIO_VERSION=$(jq ".version" library.json -r) ARDUINO_VERSION=$(awk -F= '/version/{gsub(/"/, "", $2); print $2}' library.properties) echo TAG_VERSION=$TAG_VERSION >> $GITHUB_ENV echo PLATFORMIO_VERSION=$PLATFORMIO_VERSION >> $GITHUB_ENV echo ARDUINO_VERSION=$ARDUINO_VERSION >> $GITHUB_ENV - name: Check the version number is semver compliant run: | if ! [[ $TAG_VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-z]*[0-9]+)?$ ]]; then echo "ERROR: The version number is not semver compliant" exit 1 fi - name: Check the Platformio version matches the tag number run: | if [ "$TAG_VERSION" != "$PLATFORMIO_VERSION" ]; then echo "ERROR: The version number in library.json ($PLATFORMIO_VERSION) does not match the tag number ($TAG_VERSION)" exit 1 fi - name: Check the Arduino version matches the tag number run: | if [ "$TAG_VERSION" != "$ARDUINO_VERSION" ]; then echo "ERROR: The version number in library.properties ($ARDUINO_VERSION) does not match the tag number ($TAG_VERSION)" exit 1 fi release_to_platformio: name: Release to PlatformIO Registry runs-on: ubuntu-latest needs: release_number steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 - name: Install Platform IO run: | pip install -U platformio platformio update - name: Register new library version run: | PLATFORMIO_AUTH_TOKEN=${{ secrets.PLATFORMIO_AUTH_TOKEN }} pio pkg publish --type library --no-interactive --notify ================================================ FILE: .gitignore ================================================ _secret.h .$request flow.drawio.bkp .$request flow.drawio.dtmp .clang_complete .gcc-flags.json .pio .pioenvs .pioenvs .piolibdeps .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/ipch .vscode/launch.json .vscode/settings.json *.a *.app *.dll *.dylib *.exe *.gch *.la *.lai *.lib *.lo *.mod *.o *.obj *.opensdf *.out *.pch *.sdf *.slo *.so *.suo **.DS_Store **.pio **.vscode /build /psychic-http-loadtest.log /psychic-websocket-loadtest.log benchmark/_psychic-http-loadtest.json benchmark/.~lock.comparison.ods# benchmark/http-loadtest-results.csv benchmark/node_modules benchmark/package-lock.json benchmark/psychic-http-loadtest.log benchmark/psychic-websocket-loadtest.json benchmark/psychic-websocket-loadtest.log benchmark/websocket-loadtest-results.csv examples/arduino/ examples/platformio/lib/PsychicHttp examples/websockets/lib/PsychicHttp src/cookie.txt src/secret.h Visual\ Micro ================================================ FILE: .gitmodules ================================================ [submodule "examples/esp-idf/components/ArduinoJson"] path = examples/esp-idf/components/ArduinoJson url = https://github.com/bblanchon/ArduinoJson branch = 7.x [submodule "examples/esp-idf/components/arduino-esp32"] path = examples/esp-idf/components/arduino-esp32 url = https://github.com/espressif/arduino-esp32 branch = idf-release/v4.4 [submodule "examples/esp-idf/components/esp_littlefs"] path = examples/esp-idf/components/esp_littlefs url = https://github.com/joltwallet/esp_littlefs.git ================================================ FILE: CHANGELOG.md ================================================ ## 2.2.0 - fix: memory leaks — add PsychicEndpoint destructor to delete handler; removeEndpoint/removeHandler/removeRewrite now delete removed objects; reset() deletes middleware chain - fix: stale endpoint state — removeEndpoint now cleans up _esp_idf_endpoints so WebSocket entries are properly unregistered across server restarts - fix: HTTPS server now syncs max_uri_handlers and stack_size from config before starting (both were silently ignored before) - fix: path traversal attack blocked in static file handler (#security) - fix: redirect response code was initialized wrong, breaking redirects (#239) - fix: correct integer comparisons in indexOf() checks, char overflow in auth buffer, and format specifiers for size_t - fix: urlDecode bounds-checks before reading past a trailing % - fix: regex URI matching now catches invalid patterns instead of crashing - fix: closeCallback guards against null handler before calling checkForClosedClient - fix: improper delegation call in PsychicJSONResponse - feat: removed urlencode external dependency — pulled into repository as src/UrlEncode.cpp - feat: replaced WiFi.h/ETH.h dependencies with generic esp_netif API; isConnected(), ON_STA_FILTER, and ON_AP_FILTER now work on all interface types including ESP32-P4 - feat: esp_netif compatibility — use esp_netif_next loop for older ESP-IDF versions (#235) - feat: added warning when registering WebSocket handler after start() call (#233) - feat: ESP-IDF v5.5 support added; v6.0 removed pending Arduino support - fix: NTP/DHCP handling for ESP-IDF - examples: increased stack size to fix multipart file upload issues; added start() call to HTTPS redirect server example ## 2.1.3 - fix: Added getParams() to access all parameters from issue #236 ## 2.1.2 - fix: close _file before PsychicFileResponse to prevent LittleFS remove() failure ## 2.1.1 - Re-added deleted MAX function per #230 ## 2.1.0 (since 2.0.0) - send to all clients, not bail on the first one. - Fix issue whereby H2 encoding ignores method and defaults to HTTP_GET. (#202) - now using the stable version of pioarduino. - V2 dev rollup: update PsychicFileResponse (set status and content type before chunked responses), fix getCookie, and add pong reply to ping. (#228, #207, #209, #222) - Update async_worker.cpp to fix compatibility with Arduino ESP32 3.3.0. (#225) - fixed a mistake from the pull merge. - Moved setting content type and response code into sendHeaders(). (PR #220) - Check if content size is 0 before sending a response. (#218) - Fix crash with Event Source and update CI / IDF examples. (#221) - fixed EventSource error with missing headers (content type, cache-control, keep-alive). - fixed the CI to use the latest stable versions. - ugh. CI so annoying. - bump to v2.1.0. # v2.0 I apologize for sitting on this release for so long. Its been almost a year and life just sort of got away from me. I'd like to get this release out and then start working through the backlog of issues. v2.0 has been very stable for me, so it's more than time to release it. * Huge amount of work was done to add MiddleWare and some more under the hood updates * Modified the request handling to bring initial url matching and filtering into PsychicHttpServer itself. * Fixed a bug with filter() where endpoint is matched, but filter fails and it doesn't continue matching further endpoints on same uri (checks were in different codebases) * HTTP_ANY support * unlimited endpoints (no more need to manually set config.max_uri_handlers) * much more flexibility for future * Endpoint Matching Updates * Endpoint matching functions can be set on server level (```server.setURIMatchFunction()```) or endpoint level (```endpoint.setURIMatchFunction()```) * Added convenience macros MATCH_SIMPLE, MATCH_WILDCARD, and MATCH_REGEX * Added regex matching of URIs, enable it with define PSY_ENABLE_REGEX * On regex matched requests, you can get match data with request->getRegexMatches() * Ported URL rewrite functionality from ESPAsyncWS ## Changes required from v1.x to v2.0: * add a ```server.begin()``` or ```server.start()``` after all your ```server.on()``` calls * remove any calls to ```config.max_uri_handlers``` * if you are using a custom ```server.config.uri_match_fn``` to match uris, change it to ```server.setURIMatchFunction()``` # v1.2.1 * Fix bug with missing include preventing the HTTPS server from compiling. # v1.2 * Added TemplatePrinter from https://github.com/Chris--A/PsychicHttp/tree/templatePrint * Support using as ESP IDF component * Optional using https server in ESP IDF * Fixed bug with headers * Add ESP IDF example + CI script * Added Arduino Captive Portal example and OTAUpdate from @06GitHub * HTTPS fix for ESP-IDF v5.0.2+ from @06GitHub * lots of bugfixes from @mathieucarbou Thanks to @Chris--A, @06GitHub, and @dzungpv for your contributions. # v1.1 * Changed the internal structure to support request handlers on endpoints and generic requests that do not match an endpoint * websockets, uploads, etc should now create an appropriate handler and attach to an endpoint with the server.on() syntax * Added PsychicClient to abstract away some of the internals of ESP-IDF sockets + add convenience * onOpen and onClose callbacks have changed as a result * Added support for EventSource / SSE * Added support for multipart file uploads * changed getParam() to return a PsychicWebParameter in line with ESPAsyncWebserver * Renamed various classes / files: * PsychicHttpFileResponse -> PsychicFileResponse * PsychicHttpServerEndpoint -> PsychicEndpoint * PsychicHttpServerRequest -> PsychicRequest * PsychicHttpServerResponse -> PsychicResponse * PsychicHttpWebsocket.h -> PsychicWebSocket.h * Websocket => WebSocket * Quite a few bugfixes from the community. Thank you @glennsky, @gb88, @KastanEr, @kstam, and @zekageri ================================================ FILE: CMakeLists.txt ================================================ set(COMPONENT_SRCDIRS "src" ) set(COMPONENT_ADD_INCLUDEDIRS "src" ) set(COMPONENT_REQUIRES "arduino-esp32" "esp_https_server" "arduinojson" ) register_component() target_compile_definitions(${COMPONENT_TARGET} PUBLIC -DESP32) target_compile_options(${COMPONENT_TARGET} PRIVATE -fno-rtti) ================================================ FILE: LICENSE ================================================ Copyright (c) 2024 Jeremy Poulter, Zachary Smith, and Mathieu Carbou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # PsychicHttp - HTTP on your ESP 🧙🔮 PsychicHttp is a webserver library for ESP32 + Arduino framework which uses the [ESP-IDF HTTP Server](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/esp_http_server.html) library under the hood. It is written in a similar style to the [Arduino WebServer](https://github.com/espressif/arduino-esp32/tree/master/libraries/WebServer), [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer), and [ArduinoMongoose](https://github.com/jeremypoulter/ArduinoMongoose) libraries to make writing code simple and porting from those other libraries straightforward. **Discord**: [https://discord.gg/TAQrTR3f9C](https://discord.gg/TAQrTR3f9C) # Features * Asynchronous approach (server runs in its own FreeRTOS thread) * Handles all HTTP methods with lots of convenience functions: * GET/POST parameters * get/set headers * get/set cookies * basic key/value session data storage * authentication (basic and digest mode) * HTTPS / SSL support * Static fileserving (SPIFFS, LittleFS, etc.) * Chunked response serving for large files * File uploads (Basic + Multipart) * Websocket support with onOpen, onFrame, and onClose callbacks * EventSource / SSE support with onOpen, and onClose callbacks * Request filters, including Client vs AP mode (ON_STA_FILTER / ON_AP_FILTER) * Middleware system (logging, authentication, CORS, and custom) * URL rewriting * Regex URI matching * JSON request/response support (via ArduinoJson) * TemplatePrinter class for dynamic variables at runtime ## Differences from ESPAsyncWebserver * No templating system (anyone actually use this?) # Usage ## Installation ### Platformio [PlatformIO](http://platformio.org) is an open source ecosystem for IoT development. Add "PsychicHttp" to project using [Project Configuration File `platformio.ini`](http://docs.platformio.org/page/projectconf.html) and [lib_deps](http://docs.platformio.org/page/projectconf/section_env_library.html#lib-deps) option: ```ini [env:myboard] platform = espressif... board = ... framework = arduino # using the latest stable version lib_deps = hoeken/PsychicHttp # or using GIT Url (the latest development version) lib_deps = https://github.com/hoeken/PsychicHttp ``` ### Installation - Arduino Open *Tools -> Manage Libraries...* and search for PsychicHttp. # Principles of Operation ## Things to Note * PsychicHttp is a fully asynchronous server and as such does not run on the loop thread. * You should not use yield or delay or any function that uses them inside the callbacks. * The server is smart enough to know when to close the connection and free resources. * You can not send more than one response to a single request. ## PsychicHttp * Listens for connections. * Wraps the incoming request into PsychicRequest. * Keeps track of clients + calls optional callbacks on client open and close. * Find the appropriate handler (if any) for a request and pass it on. ## Request Life Cycle * TCP connection is received by the server. * HTTP request is wrapped inside ```PsychicRequest``` object + TCP Connection wrapped inside PsychicConnection object. * When the request head is received, the server goes through all ```PsychicEndpoints``` and finds one that matches the url + method. * ```handler->filter()``` and ```handler->canHandle()``` are called on the handler to verify the handler should process the request. * Any middleware attached to the server or endpoint is run in order. * ```handler->handleRequest()``` is called to actually process the HTTP request. * If the handler cannot process the request, the server will loop through any global handlers and call that handler if it passes filter(), canHandle(), and middleware. * If no global handlers are called, the server.onNotFound handler will be called. * Each handler is responsible for processing the request and sending a response. * When the response is sent, the client is closed and freed from the memory. * Unless its a special handler like websockets or eventsource. ![Flowchart of Request Lifecycle](/assets/request-flow.svg) ### Handlers * ```PsychicHandler``` is used for processing and responding to specific HTTP requests. * ```PsychicHandler``` instances can be attached to any endpoint or as global handlers. * Setting a ```Filter``` to the ```PsychicHandler``` controls when to apply the handler, decision can be based on request method, url, request host/port/target host, the request client's localIP or remoteIP. * Two filter callbacks are provided: ```ON_AP_FILTER``` to execute the rewrite when request is made to the AP interface, ```ON_STA_FILTER``` to execute the rewrite when request is made to the STA interface. * The ```canHandle``` method is used for handler specific control on whether the requests can be handled. Decision can be based on request method, request url, request host/port/target host. * Depending on how the handler is implemented, it may provide callbacks for adding your own custom processing code to the handler. * Global ```Handlers``` are evaluated in the order they are attached to the server. The ```canHandle``` is called only if the ```Filter``` that was set to the ```Handler``` return true. * The first global ```Handler``` that can handle the request is selected, no further processing of handlers is called. ![Flowchart of Request Lifecycle](/assets/handler-callbacks.svg) ### Responses and how do they work * The ```PsychicResponse``` objects are used to send the response data back to the client. * Typically the response should be fully generated and sent from the callback. * It may be possible to generate the response outside the callback, but it will be difficult. * The exceptions are websockets + eventsource where the response is sent, but the connection is maintained and new data can be sent/received outside the handler. # Porting From ESPAsyncWebserver If you have existing code using ESPAsyncWebserver, you will feel right at home with PsychicHttp. Even if internally it is much different, the external interface is very similar. Some things are mostly cosmetic, like different class names and callback definitions. A few things might require a bit more in-depth approach. If you're porting your code and run into issues that aren't covered here, please post an issue. ## Globals Stuff * Change your #include to ```#include ``` * Change your server instance: ```PsychicHttpServer server;``` * Define websocket handler if you have one: ```PsychicWebSocketHandler websocketHandler;``` * Define eventsource if you have one: ```PsychicEventSource eventSource;``` ## setup() Stuff * add your handlers and call `server.start()` at the end * server now supports unlimited endpoints — no need to set `config.max_uri_handlers` * check your callback function definitions: * All request callbacks now take two parameters: `PsychicRequest *request` and `PsychicResponse *response` * `AsyncWebServerRequest` -> `PsychicRequest` * no more onBody() event * for small bodies (up to `MAX_REQUEST_BODY_SIZE`, default 16k) it will be automatically loaded and accessed by `request->body()` * for large bodies, use an upload handler and `onUpload()` * websocket callbacks are much different (and simpler!) * websocket / eventsource handlers get attached to url in `server.on("/url", &handler)` instead of passing url to handler constructor * eventsource callbacks are onOpen and onClose now ## Requests / Responses * `request->send` is now `response->send()` * if you create a response, call `response->send()` directly, not `request->send(reply)` * `request->headers()` is not supported by ESP-IDF, you have to just check for the header you need * No `request->beginResponse()`. Instantiate a `PsychicResponse` instead: ```PsychicResponse response(request);``` * No PROGMEM support (it's not relevant to ESP32: https://esp32.com/viewtopic.php?t=20595) * No Stream response support just yet # Usage ## Create the Server Here is an example of the typical server setup: ```cpp #include PsychicHttpServer server; void setup() { //connect to wifi //optional: set default headers on every response DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); //call server methods to attach endpoints and handlers server.on(...); server.serveStatic(...); //call start at the end. you can add/remove handlers after, except for websockets. server.start(); } ``` ## Add Handlers One major difference from ESPAsyncWebserver is that handlers can be attached to a specific url (endpoint) or as a global handler. The reason for this, is that attaching to a specific URL is more efficient and makes for cleaner code. ### Endpoint Handlers An endpoint is basically just the URL path (eg. /path/to/file) without any query string. The ```server.on(...)``` function is a convenience function for creating endpoints and attaching a handler to them. There are two main styles: attaching a basic ```WebRequest``` handler and attaching an external handler. ```cpp //creates a basic PsychicWebHandler that calls the request_callback callback server.on("/url", HTTP_GET, request_callback); //same as above, but defaults to HTTP_GET server.on("/url", request_callback); //attaches a websocket handler to /ws PsychicWebSocketHandler websocketHandler; server.on("/ws", &websocketHandler); //handle any HTTP method on a URL server.on("/any", HTTP_ANY, request_callback); ``` The ```server.on(...)``` returns a pointer to the endpoint, which can be used to call various functions like ```setHandler()```, ```addFilter()```, ```addMiddleware()```, and ```setURIMatchFunction()```. ```cpp //respond to /url only from requests to the AP server.on("/url", HTTP_GET, request_callback)->addFilter(ON_AP_FILTER); //add middleware to a specific endpoint server.on("/secure", HTTP_GET, request_callback)->addMiddleware(&authMiddleware); //set the URI matching function on a specific endpoint server.on("/simple", HTTP_GET, request_callback)->setURIMatchFunction(MATCH_SIMPLE); //attach websocket handler to /ws PsychicWebSocketHandler websocketHandler; server.on("/ws")->setHandler(&websocketHandler); ``` ### Basic Requests The ```PsychicWebHandler``` class is for handling standard web requests. It provides a single callback: ```onRequest()```. This callback is called when the handler receives a valid HTTP request. One major difference from ESPAsyncWebserver is that this callback needs to return an esp_err_t variable to let the server know the result of processing the request. The ```response->send()``` function will return this. It is a good habit to return the result of these functions as sending the response will close the connection. The function definition for the onRequest callback is: ```cpp esp_err_t function_name(PsychicRequest *request, PsychicResponse *response); ``` Here is a simple example that sends back the client's IP on the URL /ip ```cpp server.on("/ip", [](PsychicRequest *request, PsychicResponse *response) { String output = "Your IP is: " + request->client()->remoteIP().toString(); return response->send(output.c_str()); }); ``` `PsychicWebHandler` also has `onOpen()` and `onClose()` callbacks to track connections to a specific endpoint: ```cpp PsychicWebHandler *handler = new PsychicWebHandler(); handler->onRequest([](PsychicRequest *request, PsychicResponse *response) { return response->send("OK"); }); handler->onOpen([](PsychicClient *client) { Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); handler->onClose([](PsychicClient *client) { Serial.printf("[handler] connection #%u closed\n", client->socket()); }); server.on("/handler", handler); ``` ### Uploads The ```PsychicUploadHandler``` class is for handling uploads, both large POST bodies and multipart encoded forms. It provides two callbacks: ```onUpload()``` and ```onRequest()```. ```onUpload(...)``` is called when there is new data. This function may be called multiple times so that you can process the data in chunks. The function definition for the onUpload callback is: ```cpp esp_err_t function_name(PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool final); ``` * request is a pointer to the Request object * filename is the name of the uploaded file * index is the overall byte position of the current data * data is a pointer to the data buffer * len is the length of the data buffer * final is a flag to tell if its the last chunk of data ```onRequest(...)``` is called after the successful handling of the upload. Its definition and usage is the same as the basic request example as above. #### Basic Upload (file is the entire POST body) It's worth noting that there is no standard way of passing in a filename for this method, so the handler attempts to guess the filename with the following methods: * Checking the Content-Disposition header * Checking the _filename query parameter (eg. /upload?filename=filename.txt becomes filename.txt) * Checking the url and taking the last part as filename (eg. /upload/filename.txt becomes filename.txt). You must set a wildcard url for this to work as in the example below. ```cpp //handle a very basic upload as post body PsychicUploadHandler *uploadHandler = new PsychicUploadHandler(); uploadHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { File file; String path = "/www/" + filename; Serial.printf("Writing %d/%d bytes to: %s\n", (int)index+(int)len, request->contentLength(), path.c_str()); if (last) Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index+(uint64_t)len); //our first call? if (!index) file = LittleFS.open(path, FILE_WRITE); else file = LittleFS.open(path, FILE_APPEND); if(!file) { Serial.println("Failed to open file"); return ESP_FAIL; } if(!file.write(data, len)) { Serial.println("Write failed"); return ESP_FAIL; } return ESP_OK; }); //gets called after upload has been handled uploadHandler->onRequest([](PsychicRequest *request, PsychicResponse *response) { String url = "/" + request->getFilename(); String output = "" + url + ""; return response->send(output.c_str()); }); //wildcard basic file upload - POST to /upload/filename.ext server.on("/upload/*", HTTP_POST, uploadHandler); ``` #### Multipart Upload Very similar to the basic upload, with 2 key differences: * multipart requests don't know the total size of the file until after it has been fully processed. You can get a rough idea with request->contentLength(), but that is the length of the entire multipart encoded request. * you can access form variables, including multipart file info (name + size) in the onRequest handler using request->getParam() ```cpp //a little bit more complicated multipart form PsychicUploadHandler *multipartHandler = new PsychicUploadHandler(); multipartHandler->onUpload([](PsychicRequest *request, const String& filename, uint64_t index, uint8_t *data, size_t len, bool last) { File file; String path = "/www/" + filename; //some progress over serial. Serial.printf("Writing %d bytes to: %s\n", (int)len, path.c_str()); if (last) Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index+(uint64_t)len); //our first call? if (!index) file = LittleFS.open(path, FILE_WRITE); else file = LittleFS.open(path, FILE_APPEND); if(!file) { Serial.println("Failed to open file"); return ESP_FAIL; } if(!file.write(data, len)) { Serial.println("Write failed"); return ESP_FAIL; } return ESP_OK; }); //gets called after upload has been handled multipartHandler->onRequest([](PsychicRequest *request, PsychicResponse *response) { PsychicWebParameter *file = request->getParam("file_upload"); String url = "/" + file->value(); String output; output += "" + url + "
\n"; output += "Bytes: " + String(file->size()) + "
\n"; output += "Param 1: " + request->getParam("param1")->value() + "
\n"; output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); //upload to /multipart url server.on("/multipart", HTTP_POST, multipartHandler); ``` ### Static File Serving The ```PsychicStaticFileHandler``` is a special handler that does not provide any callbacks. It is used to serve a file or files from a specific directory in a filesystem to a directory on the webserver. The syntax is exactly the same as ESPAsyncWebserver. Anything that is derived from the ```FS``` class should work (eg. SPIFFS, LittleFS, SD, etc) A couple important notes: * If it finds a file with an extra .gz extension, it will serve it as gzip encoded (eg: /targetfile.ext -> {targetfile.ext}.gz) * If the file is larger than FILE_CHUNK_SIZE (default 8kb) then it will send it as a chunked response. * It will detect most basic filetypes and automatically set the appropriate Content-Type The ```server.serveStatic()``` function handles creating the handler and assigning it to the server: ```cpp //serve static files from LittleFS/www on / only to clients on same wifi network //this is where our /index.html file lives server.serveStatic("/", LittleFS, "/www/")->addFilter(ON_STA_FILTER); //serve static files from LittleFS/www-ap on / only to clients on SoftAP //this is where our /index.html file lives server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER); //serve static files from LittleFS/img on /img //it's more efficient to serve everything from a single www directory, but this is also possible. server.serveStatic("/img", LittleFS, "/img/"); //you can also serve single files server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); //set cache control headers server.serveStatic("/", LittleFS, "/www/")->setCacheControl("max-age=60"); ``` You could also theoretically use the file response directly: ```cpp server.on("/ip", [](PsychicRequest *request, PsychicResponse *response) { String filename = "/path/to/file"; PsychicFileResponse fileResponse(request, LittleFS, filename); return fileResponse.send(); }); ``` ### Websockets The ```PsychicWebSocketHandler``` class is for handling WebSocket connections. It provides 3 callbacks: ```onOpen(...)``` is called when a new WebSocket client connects. ```onFrame(...)``` is called when a new WebSocket frame has arrived. ```onClose(...)``` is called when a new WebSocket client disconnects. Here are the callback definitions: ```cpp void open_function(PsychicWebSocketClient *client); esp_err_t frame_function(PsychicWebSocketRequest *request, httpd_ws_frame *frame); void close_function(PsychicWebSocketClient *client); ``` WebSockets were the main reason for starting PsychicHttp, so they are well tested. They are also much simplified from the ESPAsyncWebserver style. You do not need to worry about error handling, partial frame assembly, PONG messages, etc. The onFrame() function is called when a complete frame has been received, and can handle frames up to the entire available heap size. Here is a basic example of using WebSockets: ```cpp //create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed. PsychicWebSocketHandler websocketHandler; websocketHandler.onOpen([](PsychicWebSocketClient *client) { Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->sendMessage("Hello!"); }); websocketHandler.onFrame([](PsychicWebSocketRequest *request, httpd_ws_frame *frame) { Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); return request->reply(frame); }); websocketHandler.onClose([](PsychicWebSocketClient *client) { Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); //attach the handler to /ws. You can then connect to ws://ip.address/ws //NOTE: the handler must be registered before server.start() is called, otherwise it will not work. server.on("/ws", &websocketHandler); ``` The onFrame() callback has 2 parameters: * ```PsychicWebSocketRequest *request``` a special request with helper functions for replying in websocket format. * ```httpd_ws_frame *frame``` ESP-IDF websocket struct. The important struct members we care about are: * ```uint8_t *payload; /*!< Pre-allocated data buffer */``` * ```size_t len; /*!< Length of the WebSocket data */``` For sending data on the websocket connection, there are 3 methods: * ```request->reply()``` - only available in the onFrame() callback context. * ```webSocketHandler.sendAll()``` - can be used anywhere to send websocket messages to all connected clients. * ```client->sendMessage()``` - can be used anywhere* to send a websocket message to a specific client All of the above functions either accept a simple ```char *``` string or you can pass a constructed ```httpd_ws_frame```. *Special Note:* Do not hold on to the ```PsychicWebSocketClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects. Instead, store the ```int client->socket()```. Then when you want to send a message, use this code: ```cpp //make sure our client is still connected. PsychicWebSocketClient *client = websocketHandler.getClient(socket); if (client != NULL) client->sendMessage("Your Message"); ``` ### EventSource / SSE The ```PsychicEventSource``` class is for handling EventSource / SSE connections. It provides 2 callbacks: ```onOpen(...)``` is called when a new EventSource client connects. ```onClose(...)``` is called when a new EventSource client disconnects. Here are the callback definitions: ```cpp void open_function(PsychicEventSourceClient *client); void close_function(PsychicEventSourceClient *client); ``` Here is a basic example of using PsychicEventSource: ```cpp //create our handler... note this should be located as a global or somewhere it wont go out of scope and be destroyed. PsychicEventSource eventSource; eventSource.onOpen([](PsychicEventSourceClient *client) { Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->send("Hello user!", NULL, millis(), 1000); }); eventSource.onClose([](PsychicEventSourceClient *client) { Serial.printf("[eventsource] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); //attach the handler to /events server.on("/events", &eventSource); ``` For sending data on the EventSource connection, there are 2 methods: * ```eventSource.send()``` - can be used anywhere to send events to all connected clients. * ```client->send()``` - can be used anywhere* to send events to a specific client All of the above functions accept a simple ```char *``` message, and optionally: ```char *``` event name, id, and reconnect time. *Special Note:* Do not hold on to the ```PsychicEventSourceClient``` for sending messages to clients outside the callbacks. That pointer is destroyed when a client disconnects. Instead, store the ```int client->socket()```. Then when you want to send a message, use this code: ```cpp //make sure our client is still connected. PsychicEventSourceClient *client = eventSource.getClient(socket); if (client != NULL) client->send("Your Event"); ``` ### JSON PsychicHttp has built-in support for JSON via ArduinoJson. There are two ways to use it: #### JSON Request Callback The simplest approach is to use the `PsychicJsonRequestCallback` signature directly with `server.on()`. The JSON body is parsed automatically and passed as a `JsonVariant`: ```cpp server.on("/api", HTTP_POST, [](PsychicRequest *request, PsychicResponse *resp, JsonVariant &json) { JsonObject input = json.as(); PsychicJsonResponse response(resp); JsonObject output = response.getRoot(); output["status"] = "success"; output["millis"] = millis(); if (input.containsKey("foo")) output["foo"] = input["foo"]; return response.send(); }); ``` #### JSON Response Use `PsychicJsonResponse` to build and send a JSON response from any handler: ```cpp server.on("/json", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response) { PsychicJsonResponse jsonResponse(response); JsonObject root = jsonResponse.getRoot(); root["status"] = "ok"; root["millis"] = millis(); return jsonResponse.send(); }); ``` ### Middleware PsychicHttp has a middleware system that allows you to run code before and after a request is handled. Middleware can be attached to the server (runs on every request) or to a specific endpoint. The `PsychicMiddlewareNext` function must be called to pass the request to the next middleware or handler: ```cpp esp_err_t run(PsychicRequest *request, PsychicResponse *response, PsychicMiddlewareNext next); ``` #### Built-in Middleware Three built-in middleware classes are included: **LoggingMiddleware** - logs requests in a curl-like format: ```cpp LoggingMiddleware loggingMiddleware; loggingMiddleware.setOutput(Serial); server.addMiddleware(&loggingMiddleware); ``` **AuthenticationMiddleware** - handles HTTP Basic or Digest authentication: ```cpp AuthenticationMiddleware auth; auth.setUsername("admin"); auth.setPassword("admin"); auth.setRealm("My App"); auth.setAuthMethod(HTTPAuthMethod::BASIC_AUTH); // or DIGEST_AUTH auth.setAuthFailureMessage("You must log in."); //attach to the whole server server.addMiddleware(&auth); //or attach to a specific endpoint server.on("/secure", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response) { return response->send("Authenticated!"); })->addMiddleware(&auth); ``` **CorsMiddleware** - adds CORS headers for cross-origin requests: ```cpp CorsMiddleware corsMiddleware; //optional customization (defaults shown) corsMiddleware.setOrigin("*"); corsMiddleware.setMethods("*"); corsMiddleware.setHeaders("*"); corsMiddleware.setAllowCredentials(true); corsMiddleware.setMaxAge(86400); server.addMiddleware(&corsMiddleware); ``` #### Custom Middleware You can write custom middleware by either subclassing `PsychicMiddleware` or by using a lambda: ```cpp server.addMiddleware([](PsychicRequest *request, PsychicResponse *response, PsychicMiddlewareNext next) { Serial.printf("Before: %s\n", request->path().c_str()); esp_err_t ret = next(request, response); Serial.printf("After: %d\n", response->getCode()); return ret; }); ``` ### URL Rewriting PsychicHttp supports URL rewriting, which allows you to map one URL to another before the request is matched to a handler: ```cpp //rewrite /rewrite to /api?foo=rewrite server.rewrite("/rewrite", "/api?foo=rewrite"); //rewrites can also have filters server.rewrite("/mobile", "/mobile-index.html")->setFilter(ON_AP_FILTER); ``` ### URI Matching PsychicHttp supports three URI matching modes that can be set at the server level or per-endpoint: * `MATCH_SIMPLE` - exact string matching * `MATCH_WILDCARD` - ESP-IDF wildcard matching (e.g., `/files/*`) * `MATCH_REGEX` - regular expression matching (requires `PSY_ENABLE_REGEX` define) ```cpp //set default for all endpoints server.setURIMatchFunction(MATCH_WILDCARD); //or set per-endpoint server.on("/files/*", HTTP_GET, handler)->setURIMatchFunction(MATCH_WILDCARD); ``` #### Regex URI Matching To use regex matching, add `#define PSY_ENABLE_REGEX` before including PsychicHttp, or define it in your build flags. Then use `MATCH_REGEX` as the URI match function and capture groups via `request->getRegexMatches()`: ```cpp server.on("^/user/([\\w]+)/?$", HTTP_GET, [](PsychicRequest *request, PsychicResponse *response) { std::smatch matches; if (request->getRegexMatches(matches)) { String username = matches.str(1).c_str(); return response->send(("Hello " + username).c_str()); } return response->send(404, "text/plain", "Not found"); })->setURIMatchFunction(MATCH_REGEX); ``` ### Default Headers You can set headers that will be automatically added to every response: ```cpp DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); DefaultHeaders::Instance().addHeader("X-Custom-Header", "value"); ``` ### HTTPS / SSL PsychicHttp supports HTTPS / SSL out of the box, however there are some limitations (see performance below). Enabling it also increases the code size by about 100kb. SSL connections require significant RAM — each TLS session consumes roughly 40–100KB of internal RAM plus two 16KB record buffers. **A board with PSRAM is strongly recommended**, and PSRAM must be enabled in your project configuration (see below). Without PSRAM, you will likely be limited to a single simultaneous SSL connection before the heap is exhausted. To use HTTPS, you need to modify your setup like so: ```cpp #include #include PsychicHttpsServer server; server.setCertificate(server_cert, server_key); ``` ```server_cert``` and ```server_key``` are both ```const char *``` parameters which contain the server certificate and private key, respectively. To generate your own key and self signed certificate, you can use the command below: ``` openssl req -x509 -newkey rsa:4096 -nodes -keyout server.key -out server.crt -sha256 -days 365 ``` You can also generate a self-signed certificate directly on the ESP32 at runtime using mbedTLS, which is already bundled with ESP-IDF. This is useful for devices that need a unique certificate without any external tooling — generate once on first boot, store the PEM strings to NVS or LittleFS, and load them on every subsequent boot. An example implementation can be found [here](https://github.com/hoeken/YarrboardFramework/blob/34be5c9457355a5e0dcdc0afacca9e2907cfab03/src/controllers/HTTPController.cpp#L328). Including the ```PsychicHttpsServer.h``` also defines ```PSY_ENABLE_SSL``` which you can use in your code to allow enabling / disabling calls in your code based on if the HTTPS server is available: ```cpp //our main server object #ifdef PSY_ENABLE_SSL PsychicHttpsServer server; #else PsychicHttpServer server; #endif ``` Last, but not least, you can create a separate HTTP server on port 80 that redirects all requests to the HTTPS server: ```cpp //this creates a 2nd server listening on port 80 and redirects all requests HTTPS PsychicHttpServer *redirectServer = new PsychicHttpServer(); redirectServer->config.ctrl_port = 20420; // just a random port different from the default one redirectServer->config.stack_size = 4096; // we dont need a large stack size for this. redirectServer->onNotFound([](PsychicRequest *request, PsychicResponse *response) { String url = "https://" + request->host() + request->url(); return response->redirect(url.c_str()); }); redirectServer->start(); ``` #### Enabling PSRAM for SSL To offload mbedTLS allocations to PSRAM and avoid exhausting internal heap under multiple SSL connections, add the following to your project. The example below targets an **ESP32-S3-WROOM-1-N16R8** (8 MB OPI PSRAM) but the mbedTLS options apply to any ESP32 variant with PSRAM. `sdkconfig.defaults`: ``` # Enable OPI PSRAM (8MB on ESP32-S3-WROOM-1-N16R8). # board_build.arduino.memory_type = qio_opi selects the right bootloader/linker, # but IDF still needs these Kconfig options to actually map PSRAM into the heap. CONFIG_SPIRAM=y CONFIG_SPIRAM_MODE_OCT=y CONFIG_SPIRAM_SPEED_80M=y CONFIG_SPIRAM_USE_MALLOC=y # Move mbedTLS dynamic allocations to PSRAM. # Without this, each TLS session chews ~40-100KB of internal RAM plus two 16KB # record buffers, exhausting internal heap when multiple clients connect. # Has no effect on targets without SPIRAM (CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC # depends on SPIRAM in Kconfig, so it is silently ignored on C3/C5/C6/esp32). CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y # Use asymmetric record buffer sizes: full 16KB in (needed for receiving TLS # records from browsers) but only 4KB out (sufficient for JSON API responses). # Saves ~24KB of PSRAM per session vs the default symmetric 32KB allocation. CONFIG_MBEDTLS_ASYMMETRIC_CONTENT_LEN=y CONFIG_MBEDTLS_SSL_IN_CONTENT_LEN=16384 CONFIG_MBEDTLS_SSL_OUT_CONTENT_LEN=4096 ``` `platformio.ini`: ```ini build_flags = -D BOARD_HAS_PSRAM ``` # TemplatePrinter **This is not specific to PsychicHttp, and it works with any `Print` object. You could for example, template data out to `File`, `Serial`, etc...**. The template engine is a `Print` interface and can be printed to directly, however, if you are just templating a few short strings, I'd probably just use `response.printf()` instead. **Its benefit will be seen when templating large inputs such as files.** One benefit may be **templating a **JSON** file avoiding the need to use ArduinoJson.** Before closing the underlying `Print`/`Stream` that this writes to, it must be flushed as small amounts of data can be buffered. A convenience method to take care of this is shows in `example 3`. The header file is not currently added to `PsychicHttp.h` and users will have to add it manually: ```C++ #include ``` ## Template parameter definition: - Must start and end with a preset delimiter, the default is `%` - Can only contain `a-z`, `A-Z`, `0-9`, and `_` - Maximum length of 63 characters (buffer is 64 including `null`). - A parameter must not be zero length (not including delimiters). - Spaces or any other character do not match as a parameter, and will be output as is. - Valid examples - `%MY_PARAM%` - `%SOME1%` - **Invalid** examples - `%MY PARAM%` - `%SOME1 %` - `%UNFINISHED` - `%%` ## Template processing A function or lambda is used to receive the parameter replacement. ```C++ bool templateHandler(Print &output, const char *param){ //... } [](Print &output, const char *param){ //... } ``` Parameters: - `Print &output` - the underlying `Print`, print the results of templating to this. - `const char *param` - a string containing the current parameter. The handler must return a `bool`. - `true`: the parameter was handled, continue as normal. - `false`: the input detected as a parameter is not, print literal. See output in **example 1** regarding the effects of returning `true` or `false`. ## Template input handler This is not needed unless using the static convenience function `TemplatePrinter::start()`. See **example 3**. ```C++ bool inputHandler(TemplatePrinter &printer){ //... } [](TemplatePrinter &printer){ //... } ``` Parameters: - `TemplatePrinter &printer` - The template engine, print your template text to this for processing. ## Example 1 - Simple use with `PsychicStreamResponse`: This example highlights its most basic usage. ```C++ // Function to handle parameter requests. bool templateHandler(Print &output, const char *param){ if(strcmp(param, "FREE_HEAP") == 0){ output.print((double)ESP.getFreeHeap() / 1024.0, 2); }else if(strcmp(param, "MIN_FREE_HEAP") == 0){ output.print((double)ESP.getMinFreeHeap() / 1024.0, 2); }else if(strcmp(param, "MAX_ALLOC_HEAP") == 0){ output.print((double)ESP.getMaxAllocHeap() / 1024.0, 2); }else if(strcmp(param, "HEAP_SIZE") == 0){ output.print((double)ESP.getHeapSize() / 1024.0, 2); }else{ return false; } output.print("Kb"); return true; } // Example serving a request server.on("/template", [](PsychicRequest *request, PsychicResponse *response) { PsychicStreamResponse streamResponse(request, "text/plain"); streamResponse.beginSend(); TemplatePrinter printer(streamResponse, templateHandler); printer.println("My ESP has %FREE_HEAP% left. Its lifetime minimum heap is %MIN_FREE_HEAP%."); printer.println("The maximum allocation size is %MAX_ALLOC_HEAP%, and its total size is %HEAP_SIZE%."); printer.println("This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%."); printer.println("This line finished with %UNFIN"); printer.flush(); return streamResponse.endSend(); }); ``` The output for example looks like: ``` My ESP has 170.92Kb left. Its lifetime minimum heap is 169.83Kb. The maximum allocation size is 107.99Kb, and its total size is 284.19Kb. This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%. This line finished with %UNFIN ``` ## Example 2 - Templating a file ```C++ server.on("/home", [](PsychicRequest *request, PsychicResponse *response) { PsychicStreamResponse streamResponse(request, "text/html"); File file = SD.open("/www/index.html"); streamResponse.beginSend(); TemplatePrinter printer(streamResponse, templateHandler); printer.copyFrom(file); printer.flush(); file.close(); return streamResponse.endSend(); }); ``` ## Example 3 - Using the `TemplatePrinter::start` method. This static method allows an RAII approach, allowing you to template a stream, etc... without needing a `flush()`. The function call is laid out as: ```C++ TemplatePrinter::start(host_stream, template_handler, input_handler); ``` \*these examples use the `templateHandler` function defined in example 1. ### Serve a file like example 2 ```C++ server.on("/home", [](PsychicRequest *request, PsychicResponse *response) { PsychicStreamResponse streamResponse(request, "text/html"); File file = SD.open("/www/index.html"); streamResponse.beginSend(); TemplatePrinter::start(streamResponse, templateHandler, [&file](TemplatePrinter &printer){ printer.copyFrom(file); }); file.close(); return streamResponse.endSend(); }); ``` ### Template a string like example 1 ```C++ server.on("/template2", [](PsychicRequest *request, PsychicResponse *response) { PsychicStreamResponse streamResponse(request, "text/plain"); streamResponse.beginSend(); TemplatePrinter::start(streamResponse, templateHandler, [](TemplatePrinter &printer){ printer.println("My ESP has %FREE_HEAP% left. Its lifetime minimum heap is %MIN_FREE_HEAP%."); printer.println("The maximum allocation size is %MAX_ALLOC_HEAP%, and its total size is %HEAP_SIZE%."); printer.println("This is an unhandled parameter: %UNHANDLED_PARAM% and this is an invalid param %INVALID PARAM%."); }); return streamResponse.endSend(); }); ``` # Performance In order to really see the differences between libraries, I created some basic benchmark firmwares for PsychicHttp, ESPAsyncWebserver, and ArduinoMongoose. I then ran the loadtest-http.sh and loadtest-websocket.sh scripts against each firmware to get some real numbers on the performance of each server library. All of the code and results are available in the /benchmark folder. If you want to see the collated data and graphs, there is a [LibreOffice spreadsheet](/benchmark/comparison.ods). ![Performance graph](/benchmark/performance.png) ![Latency graph](/benchmark/latency.png) ## HTTPS / SSL Yes, PsychicHttp supports SSL out of the box, but there are a few caveats: * SSL connections are memory-intensive — each TLS session consumes roughly 40–100KB of internal RAM plus two 16KB record buffers. On a board without PSRAM, a blank PsychicHttp sketch has around 150KB free, which limits you to 1–2 simultaneous SSL connections. **A board with PSRAM is strongly recommended** for any production HTTPS use. See the [Enabling PSRAM for SSL](#enabling-psram-for-ssl) section above for the required `sdkconfig.defaults` and `platformio.ini` settings to route mbedTLS allocations to PSRAM. * Speed and latency are still pretty good (see graph above) but the SSH handshake seems to take 1500ms. With websockets or browser its not an issue since the connection is kept alive, but if you are loading requests in another way it will be a bit slow * Unless you want to expose your ESP to the internet, you are limited to self signed keys and the annoying browser security warnings that come with them. ## Analysis The results clearly show some of the reasons for writing PsychicHttp: ESPAsyncWebserver crashes under heavy load on each test, across the board in a 60s test. That means in normal usage, you're just rolling the dice with how long it will go until it crashes. Every other number is moot, IMHO. ArduinoMongoose doesn't crash under heavy load, but it does bog down with extremely high latency (15s) for web requests and appears to not even respond at the highest loadings as the loadtest script crashes instead. The code itself doesnt crash, so bonus points there. After the high load, it does go back to serving normally. One area ArduinoMongoose does shine, is in websockets where its performance is almost 2x the performance of PsychicHttp. Both in requests per second and latency. Clearly an area of improvement for PsychicHttp. PsychicHttp has good performance across the board. No crashes and continously responds during each test. It is a clear winner in requests per second when serving files from memory, dynamic JSON, and has consistent performance when serving files from LittleFS. The only real downside is the lower performance of the websockets with a single connection handling 38rps, and maxing out at 120rps across multiple connections. ## Takeaways With all due respect to @me-no-dev who has done some amazing work in the open source community, I cannot recommend anyone use the ESPAsyncWebserver for anything other than simple projects that don't need to be reliable. Even then, PsychicHttp has taken the arcane api of the ESP-IDF web server library and made it nice and friendly to use with a very similar API to ESPAsyncWebserver. Also, ESPAsyncWebserver is more or less abandoned, with 150 open issues, 77 pending pull requests, and the last commit in over 2 years. ArduinoMongoose is a good alternative, although the latency issues when it gets fully loaded can be very annoying. I believe it is also cross platform to other microcontrollers as well, but I haven't tested that. The other issue here is that it is based on an old version of a modified Mongoose library that will be difficult to update as it is a major revision behind and several security updates behind as well. Big thanks to @jeremypoulter though as PsychicHttp is a fork of ArduinoMongoose so it's built on strong bones. # Community / Support The best way to get support is probably with Github issues. There is also a [Discord chat](https://discord.gg/CM5abjGG) that is pretty active. # Roadmap ## Longterm Wants * investigate websocket performance gap * Enable worker based multithreading with esp-idf v5.x * 100-continue support? If anyone wants to take a crack at implementing any of the above features I am more than happy to accept pull requests. ================================================ FILE: RELEASE.md ================================================ * Update CHANGELOG * Bump version in src/PsychicVersion.h * Bump version in library.json * Bump version in library.properties * Bump version in idf_component.yml * Make new release + tag * this will get pulled in automatically by Arduino Library Indexer * Platformio automatically publishes on release via .github hook ================================================ FILE: benchmark/arduinomongoose/.gitignore ================================================ .pio .vscode/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch ================================================ FILE: benchmark/arduinomongoose/include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: benchmark/arduinomongoose/lib/README ================================================ This directory is intended for project specific (private) libraries. PlatformIO will compile them to static libraries and link into executable file. The source code of each library should be placed in a an own separate directory ("lib/your_library_name/[here are source files]"). For example, see a structure of the following two libraries `Foo` and `Bar`: |--lib | | | |--Bar | | |--docs | | |--examples | | |--src | | |- Bar.c | | |- Bar.h | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html | | | |--Foo | | |- Foo.c | | |- Foo.h | | | |- README --> THIS FILE | |- platformio.ini |--src |- main.c and a contents of `src/main.c`: ``` #include #include int main (void) { ... } ``` PlatformIO Library Dependency Finder will find automatically dependent libraries scanning project source files. More information about PlatformIO Library Dependency Finder - https://docs.platformio.org/page/librarymanager/ldf.html ================================================ FILE: benchmark/arduinomongoose/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env] platform = espressif32 framework = arduino board = esp32dev monitor_speed = 115200 monitor_filters = esp32_exception_decoder lib_deps = jeremypoulter/ArduinoMongoose bblanchon/ArduinoJson board_build.filesystem = littlefs [env:default] ================================================ FILE: benchmark/arduinomongoose/src/main.cpp ================================================ /* Wi-Fi STA Connect and Disconnect Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ #include #include #include #include #include #include const char* ssid = ""; const char* password = ""; MongooseHttpServer server; const char* htmlContent = R"( Sample HTML

Hello, World!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

)"; bool connectToWifi() { Serial.println(); Serial.print("[WiFi] Connecting to "); Serial.println(ssid); WiFi.setSleep(false); WiFi.useStaticBuffers(true); WiFi.begin(ssid, password); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } void setup() { Serial.begin(115200); delay(10); Serial.println("ArduinoMongoose Benchmark"); // We start by connecting to a WiFi network // To debug, please enable Core Debug Level to Verbose if (connectToWifi()) { if (!LittleFS.begin()) { Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); return; } // start our server Mongoose.begin(); server.begin(80); // index file server.on("/", HTTP_GET, [](MongooseHttpServerRequest* request) { request->send(200, "text/html", htmlContent); }); // api - parameters passed in via query eg. /api/endpoint?foo=bar server.on("/api", HTTP_GET, [](MongooseHttpServerRequest* request) { //create a response object StaticJsonDocument<128> output; output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); //work with some params if (request->hasParam("foo")) { String foo = request->getParam("foo"); output["foo"] = foo; } //serialize and return String jsonBuffer; serializeJson(output, jsonBuffer); request->send(200, "application/json", jsonBuffer.c_str()); }); // websocket server.on("/ws$")->onFrame([](MongooseHttpWebSocketConnection* connection, int flags, uint8_t* data, size_t len) { connection->send(WEBSOCKET_OP_TEXT, data, len); // server.sendAll(connection, (char *)data); }); // hack - no servestatic server.on("/alien.png", HTTP_GET, [](MongooseHttpServerRequest* request) { //open our file File fp = LittleFS.open("/www/alien.png"); size_t length = fp.size(); //read our data uint8_t * data = (uint8_t *)malloc(length); if (data != NULL) { fp.readBytes((char *)data, length); //send it off MongooseHttpServerResponseBasic *response = request->beginResponse(); response->setContent(data, length); response->setContentType("image/png"); response->setCode(200); request->send(response); //free the memory free(data); } else request->send(503); }); } } void loop() { Mongoose.poll(1000); } ================================================ FILE: benchmark/arduinomongoose/test/README ================================================ This directory is intended for PlatformIO Test Runner and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. More information about PlatformIO Unit Testing: - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html ================================================ FILE: benchmark/espasyncwebserver/.gitignore ================================================ .pio .vscode/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch ================================================ FILE: benchmark/espasyncwebserver/include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: benchmark/espasyncwebserver/lib/README ================================================ This directory is intended for project specific (private) libraries. PlatformIO will compile them to static libraries and link into executable file. The source code of each library should be placed in a an own separate directory ("lib/your_library_name/[here are source files]"). For example, see a structure of the following two libraries `Foo` and `Bar`: |--lib | | | |--Bar | | |--docs | | |--examples | | |--src | | |- Bar.c | | |- Bar.h | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html | | | |--Foo | | |- Foo.c | | |- Foo.h | | | |- README --> THIS FILE | |- platformio.ini |--src |- main.c and a contents of `src/main.c`: ``` #include #include int main (void) { ... } ``` PlatformIO Library Dependency Finder will find automatically dependent libraries scanning project source files. More information about PlatformIO Library Dependency Finder - https://docs.platformio.org/page/librarymanager/ldf.html ================================================ FILE: benchmark/espasyncwebserver/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env] platform = espressif32 framework = arduino ; board = esp32dev board = esp32-s3-devkitc-1 upload_port = /dev/ttyACM0 monitor_port = /dev/ttyACM1 monitor_speed = 115200 monitor_filters = esp32_exception_decoder lib_compat_mode = strict lib_ldf_mode = chain lib_deps = ; mathieucarbou/AsyncTCP @ 3.2.10 https://github.com/mathieucarbou/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip mathieucarbou/ESPAsyncWebServer @ 3.3.16 bblanchon/ArduinoJson lib_ignore = AsyncTCP mathieucarbou/AsyncTCP board_build.filesystem = littlefs build_flags = -D CONFIG_ASYNC_TCP_MAX_ACK_TIME=3000 -D CONFIG_ASYNC_TCP_PRIORITY=10 -D CONFIG_ASYNC_TCP_QUEUE_SIZE=128 -D CONFIG_ASYNC_TCP_RUNNING_CORE=1 -D CONFIG_ASYNC_TCP_STACK_SIZE=4096 -D WS_MAX_QUEUED_MESSAGES=128 [env:default] ================================================ FILE: benchmark/espasyncwebserver/src/main.cpp ================================================ /* Wi-Fi STA Connect and Disconnect Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ #include "_secret.h" #include #include #include #include #include #include #ifndef WIFI_SSID #error "You need to enter your wifi credentials. Copy secret.h to _secret.h and enter your credentials there." #endif // Enter your WIFI credentials in secret.h const char* ssid = WIFI_SSID; const char* password = WIFI_PASS; // hostname for mdns (psychic.local) const char* local_hostname = "psychic"; AsyncWebServer server(80); AsyncWebSocket ws("/ws"); const char* htmlContent = R"( Sample HTML

Hello, World!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

)"; const size_t htmlContentLen = strlen(htmlContent); bool connectToWifi() { Serial.println(); Serial.print("[WiFi] Connecting to "); Serial.println(ssid); // WiFi.setSleep(false); // WiFi.useStaticBuffers(true); WiFi.begin(ssid, password); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } void onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) { if (type == WS_EVT_CONNECT) { // client connected // Serial.printf("ws[%s][%u] connect\n", server->url(), client->id()); // client->printf("Hello Client %u :)", client->id()); // client->ping(); } else if (type == WS_EVT_DISCONNECT) { // client disconnected // Serial.printf("ws[%s][%u] disconnect: %u\n", server->url(), client->id()); } else if (type == WS_EVT_ERROR) { // error was received from the other end // Serial.printf("ws[%s][%u] error(%u): %s\n", server->url(), client->id(), *((uint16_t*)arg), (char*)data); } else if (type == WS_EVT_PONG) { // pong message was received (in response to a ping request maybe) // Serial.printf("ws[%s][%u] pong[%u]: %s\n", server->url(), client->id(), len, (len)?(char*)data:""); } else if (type == WS_EVT_DATA) { // data packet AwsFrameInfo* info = (AwsFrameInfo*)arg; if (info->final && info->index == 0 && info->len == len) { // the whole message is in a single frame and we got all of it's data // Serial.printf("ws[%s][%u] %s-message[%llu]: ", server->url(), client->id(), (info->opcode == WS_TEXT)?"text":"binary", info->len); if (info->opcode == WS_TEXT) { data[len] = 0; // Serial.printf("%s\n", (char*)data); } else { // for(size_t i=0; i < info->len; i++){ // Serial.printf("%02x ", data[i]); // } // Serial.printf("\n"); } if (info->opcode == WS_TEXT) { client->text((char*)data, len); } // else // client->binary("I got your binary message"); } else { // message is comprised of multiple frames or the frame is split into multiple packets if (info->index == 0) { // if(info->num == 0) // Serial.printf("ws[%s][%u] %s-message start\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); // Serial.printf("ws[%s][%u] frame[%u] start[%llu]\n", server->url(), client->id(), info->num, info->len); } Serial.printf("ws[%s][%u] frame[%u] %s[%llu - %llu]: ", server->url(), client->id(), info->num, (info->message_opcode == WS_TEXT) ? "text" : "binary", info->index, info->index + len); if (info->message_opcode == WS_TEXT) { data[len] = 0; // Serial.printf("%s\n", (char*)data); } else { // for(size_t i=0; i < len; i++){ // Serial.printf("%02x ", data[i]); // } // Serial.printf("\n"); } if ((info->index + len) == info->len) { // Serial.printf("ws[%s][%u] frame[%u] end[%llu]\n", server->url(), client->id(), info->num, info->len); if (info->final) { // Serial.printf("ws[%s][%u] %s-message end\n", server->url(), client->id(), (info->message_opcode == WS_TEXT)?"text":"binary"); if (info->message_opcode == WS_TEXT) { client->text((char*)data, info->len); } // else // client->binary("I got your binary message"); } } } } } void setup() { Serial.begin(115200); delay(10); Serial.println("ESPAsyncWebserver Benchmark"); // We start by connecting to a WiFi network // To debug, please enable Core Debug Level to Verbose if (connectToWifi()) { // set up our esp32 to listen on the local_hostname.local domain if (!MDNS.begin(local_hostname)) { Serial.println("Error starting mDNS"); return; } MDNS.addService("http", "tcp", 80); if (!LittleFS.begin()) { Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); return; } // api - parameters passed in via query eg. /api/endpoint?foo=bar server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { // ESPAsyncWebServer, sending a char* does a buffer copy, unlike Psychic. // Sending flash data is done with the uint8_t* overload. request->send(200, "text/html", (uint8_t*)htmlContent, htmlContentLen); }); // api - parameters passed in via query eg. /api/endpoint?foo=bar server.on("/api", HTTP_GET, [](AsyncWebServerRequest* request) { // create a response object JsonDocument output; output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); // work with some params if (request->hasParam("foo")) { const AsyncWebParameter* foo = request->getParam("foo"); output["foo"] = foo->value(); } // serialize and return String jsonBuffer; serializeJson(output, jsonBuffer); request->send(200, "application/json", jsonBuffer.c_str()); }); ws.onEvent(onEvent); server.addHandler(&ws); // put this last, otherwise it clogs the other requests // serve static files from LittleFS/www on / server.serveStatic("/", LittleFS, "/www/"); server.begin(); } } void loop() { ws.cleanupClients(); Serial.printf("Free Heap: %d\n", esp_get_free_heap_size()); delay(1000); } ================================================ FILE: benchmark/espasyncwebserver/src/secret.h ================================================ #define WIFI_SSID "Your_SSID" #define WIFI_PASS "Your_PASS" ================================================ FILE: benchmark/espasyncwebserver/test/README ================================================ This directory is intended for PlatformIO Test Runner and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. More information about PlatformIO Unit Testing: - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html ================================================ FILE: benchmark/eventsource-client-test.js ================================================ #!/usr/bin/env node //stress test the client opening/closing code const EventSource = require('eventsource'); const url = 'http://psychic.local/events'; async function eventSourceClient() { console.log(`Starting test`); for (let i = 0; i < 1000000; i++) { if (i % 100 == 0) console.log(`Count: ${i}`); let eventSource = new EventSource(url); eventSource.onopen = () => { //console.log('EventSource connection opened.'); }; eventSource.onerror = (error) => { console.error('EventSource error:', error); // Close the connection on error eventSource.close(); }; await new Promise((resolve) => { eventSource.onmessage = (event) => { //console.log('Received message:', event.data); // Close the connection after receiving the first message eventSource.close(); resolve(); } }); } } eventSourceClient(); ================================================ FILE: benchmark/http-client-test.js ================================================ #!/usr/bin/env node //stress test the http request code const axios = require('axios'); const url = 'http://psychic.local/api'; const queryParams = { foo: 'bar', foo1: 'bar', foo2: 'bar', foo3: 'bar', foo4: 'bar', foo5: 'bar', foo6: 'bar', }; const totalRequests = 1000000; const requestsPerCount = 100; let requestCount = 0; function fetchData() { axios.get(url, { params: queryParams }) .then(response => { requestCount++; if (requestCount % requestsPerCount === 0) { console.log(`Requests completed: ${requestCount}`); } if (requestCount < totalRequests) { fetchData(); } else { console.log('All requests completed.'); } }) .catch(error => { console.error('Error making request:', error.message); }); } // Start making requests console.log(`Starting test`); fetchData(); ================================================ FILE: benchmark/loadtest-http.sh ================================================ #!/usr/bin/env bash #Command to install the testers: # npm install TEST_IP="psychic.local" TEST_TIME=10 #LOG_FILE=psychic-http-loadtest.log LOG_FILE=_psychic-http-loadtest.json RESULTS_FILE=http-loadtest-results.csv TIMEOUT=10000 WORKERS=1 PROTOCOL=http #PROTOCOL=https echo "url,connections,rps,latency,errors" > $RESULTS_FILE for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20 do printf "\n\nCLIENTS: *** $CONCURRENCY ***\n\n" >> $LOG_FILE echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/" autocannon -c $CONCURRENCY -w $WORKERS -d $TEST_TIME -j "$PROTOCOL://$TEST_IP/" > $LOG_FILE node parse-http-test.js $LOG_FILE $RESULTS_FILE sleep 5 done for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20 do echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/api" autocannon -c $CONCURRENCY -w $WORKERS -d $TEST_TIME -j "$PROTOCOL://$TEST_IP/api?foo=bar" > $LOG_FILE node parse-http-test.js $LOG_FILE $RESULTS_FILE sleep 5 done for CONCURRENCY in 1 2 3 4 5 6 7 8 9 10 15 20 do echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/alien.png" autocannon -c $CONCURRENCY -w $WORKERS -d $TEST_TIME -j "$PROTOCOL://$TEST_IP/alien.png" > $LOG_FILE node parse-http-test.js $LOG_FILE $RESULTS_FILE sleep 5 done rm $LOG_FILE ================================================ FILE: benchmark/loadtest-websocket.sh ================================================ #!/usr/bin/env bash #Command to install the testers: # npm install TEST_IP="psychic.local" TEST_TIME=60 LOG_FILE=psychic-websocket-loadtest.json RESULTS_FILE=websocket-loadtest-results.csv PROTOCOL=ws #PROTOCOL=wss if test -f "$LOG_FILE"; then rm $LOG_FILE fi echo "url,clients,rps,latency,errors" > $RESULTS_FILE CORES=1 for CONCURRENCY in 1 2 3 4 5 do echo "Testing $CONCURRENCY clients on $PROTOCOL://$TEST_IP/ws" loadtest -c $CONCURRENCY --cores $CORES -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE node parse-websocket-test.js $LOG_FILE $RESULTS_FILE sleep 2 done CORES=2 for CONNECTIONS in 6 8 10 12 14 do CONCURRENCY=$((CONNECTIONS / 2)) echo "Testing $CONNECTIONS clients on $PROTOCOL://$TEST_IP/ws" loadtest -c $CONCURRENCY --cores $CORES -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE node parse-websocket-test.js $LOG_FILE $RESULTS_FILE sleep 2 done CORES=4 for CONNECTIONS in 16 20 24 28 32 do CONCURRENCY=$((CONNECTIONS / CORES)) echo "Testing $CONNECTIONS clients on $PROTOCOL://$TEST_IP/ws" loadtest -c $CONCURRENCY --cores $CORES -t $TEST_TIME --insecure $PROTOCOL://$TEST_IP/ws --quiet 2> /dev/null >> $LOG_FILE node parse-websocket-test.js $LOG_FILE $RESULTS_FILE sleep 2 done ================================================ FILE: benchmark/package.json ================================================ { "dependencies": { "autocannon": "^7.15.0", "axios": "^1.6.2", "csv-writer": "^1.6.0", "eventsource": "^2.0.2", "loadtest": "^8.0.9", "ws": "^8.14.2" } } ================================================ FILE: benchmark/parse-http-test.js ================================================ const fs = require('fs'); const createCsvWriter = require('csv-writer').createObjectCsvWriter; // Get the input and output file paths from the command line arguments const inputFilePath = process.argv[2]; const outputFilePath = process.argv[3]; if (!inputFilePath || !outputFilePath) { console.error('Usage: node script.js '); process.exit(1); } // Read and parse the JSON file fs.readFile(inputFilePath, 'utf8', (err, data) => { if (err) { console.error('Error reading the input file:', err); return; } // Parse the JSON data const jsonData = JSON.parse(data); // Extract the desired fields const { url, connections, latency, requests, errors } = jsonData; const latencyMean = latency.mean; const requestsMean = requests.mean; // Set up the CSV writer const csvWriter = createCsvWriter({ path: outputFilePath, header: [ {id: 'url', title: 'URL'}, {id: 'connections', title: 'Connections'}, {id: 'requestsMean', title: 'Requests Mean'}, {id: 'latencyMean', title: 'Latency Mean'}, {id: 'errors', title: 'Errors'}, ], append: true // this will append to the existing file }); // Prepare the data to be written const records = [ { url: url, connections: connections, latencyMean: latencyMean, requestsMean: requestsMean, errors: errors } ]; // Write the data to the CSV file csvWriter.writeRecords(records) .then(() => { console.log('Data successfully appended to CSV file.'); }) .catch(err => { console.error('Error writing to the CSV file:', err); }); }); ================================================ FILE: benchmark/parse-websocket-test.js ================================================ const fs = require('fs'); const readline = require('readline'); if (process.argv.length !== 4) { console.error('Usage: node parse-websocket-test.js '); process.exit(1); } const inputFile = process.argv[2]; const outputFile = process.argv[3]; async function parseFile() { const fileStream = fs.createReadStream(inputFile); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity }); let targetUrl = null; let totalErrors = null; let meanLatency = null; let effectiveRps = null; let concurrentClients = null; for await (const line of rl) { if (line.startsWith('Target URL:')) { targetUrl = line.split(':').slice(1).join(':').trim(); } if (line.startsWith('Total errors:')) { totalErrors = parseInt(line.split(':')[1].trim(), 10); } if (line.startsWith('Mean latency:')) { meanLatency = parseFloat(line.split(':')[1].trim()); } if (line.startsWith('Effective rps:')) { effectiveRps = parseInt(line.split(':')[1].trim(), 10); } if (line.startsWith('Concurrent clients:')) { concurrentClients = parseInt(line.split(':')[1].trim(), 10); } } if (targetUrl === null || totalErrors === null || meanLatency === null || effectiveRps === null || concurrentClients === null) { console.error('Failed to extract necessary data from the input file'); process.exit(1); } const csvLine = `${targetUrl},${concurrentClients},${effectiveRps},${meanLatency},${totalErrors}\n`; fs.appendFile(outputFile, csvLine, (err) => { if (err) { console.error('Failed to append to CSV file:', err); process.exit(1); } console.log('Data successfully appended to CSV file.'); }); } parseFile().catch(err => { console.error('Error reading file:', err); process.exit(1); }); ================================================ FILE: benchmark/psychichttp/.gitignore ================================================ .pio .vscode/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch ================================================ FILE: benchmark/psychichttp/include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: benchmark/psychichttp/lib/README ================================================ This directory is intended for project specific (private) libraries. PlatformIO will compile them to static libraries and link into executable file. The source code of each library should be placed in a an own separate directory ("lib/your_library_name/[here are source files]"). For example, see a structure of the following two libraries `Foo` and `Bar`: |--lib | | | |--Bar | | |--docs | | |--examples | | |--src | | |- Bar.c | | |- Bar.h | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html | | | |--Foo | | |- Foo.c | | |- Foo.h | | | |- README --> THIS FILE | |- platformio.ini |--src |- main.c and a contents of `src/main.c`: ``` #include #include int main (void) { ... } ``` PlatformIO Library Dependency Finder will find automatically dependent libraries scanning project source files. More information about PlatformIO Library Dependency Finder - https://docs.platformio.org/page/librarymanager/ldf.html ================================================ FILE: benchmark/psychichttp/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env] platform = espressif32 framework = arduino board = esp32dev monitor_speed = 115200 monitor_filters = esp32_exception_decoder lib_deps = bblanchon/ArduinoJson board_build.filesystem = littlefs [env:default] lib_deps = https://github.com/hoeken/PsychicHttp [env:v2-dev] lib_deps = https://github.com/hoeken/PsychicHttp#v2-dev board = esp32-s3-devkitc-1 upload_port = /dev/ttyACM0 monitor_port = /dev/ttyACM1 ================================================ FILE: benchmark/psychichttp/src/main.cpp ================================================ /* Wi-Fi STA Connect and Disconnect Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ #include "_secret.h" #include #include #include #include #include #include #ifndef WIFI_SSID #error "You need to enter your wifi credentials. Copy secret.h to _secret.h and enter your credentials there." #endif // Enter your WIFI credentials in secret.h const char* ssid = WIFI_SSID; const char* password = WIFI_PASS; // hostname for mdns (psychic.local) const char* local_hostname = "psychic"; PsychicHttpServer server; PsychicWebSocketHandler websocketHandler; PsychicEventSource eventSource; const char* htmlContent = R"( Sample HTML

Hello, World!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

)"; bool connectToWifi() { Serial.println(); Serial.print("[WiFi] Connecting to "); Serial.println(ssid); // WiFi.setSleep(false); // WiFi.useStaticBuffers(true); WiFi.begin(ssid, password); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } void setup() { Serial.begin(115200); delay(10); Serial.println("PsychicHTTP Benchmark"); if (connectToWifi()) { // set up our esp32 to listen on the local_hostname.local domain if (!MDNS.begin(local_hostname)) { Serial.println("Error starting mDNS"); return; } MDNS.addService("http", "tcp", 80); if (!LittleFS.begin()) { Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); return; } // our index server.on("/", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send(200, "text/html", htmlContent); }); // serve static files from LittleFS/www on / server.serveStatic("/", LittleFS, "/www/"); // a websocket echo server websocketHandler.onOpen([](PsychicWebSocketClient* client) { // client->sendMessage("Hello!"); }); websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) { response->send(frame); return ESP_OK; }); server.on("/ws", &websocketHandler); // EventSource server eventSource.onOpen([](PsychicEventSourceClient* client) { client->send("Hello", NULL, millis(), 1000); }); server.on("/events", &eventSource); // api - parameters passed in via query eg. /api/endpoint?foo=bar server.on("/api", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { //create a response object JsonDocument output; output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); //work with some params if (request->hasParam("foo")) { String foo = request->getParam("foo")->value(); output["foo"] = foo; } //serialize and return String jsonBuffer; serializeJson(output, jsonBuffer); return response->send(200, "application/json", jsonBuffer.c_str()); }); server.begin(); } } unsigned long last; void loop() { if (millis() - last > 1000) { Serial.printf("Free Heap: %d\n", esp_get_free_heap_size()); last = millis(); } } ================================================ FILE: benchmark/psychichttp/src/secret.h ================================================ #define WIFI_SSID "Your_SSID" #define WIFI_PASS "Your_PASS" ================================================ FILE: benchmark/psychichttp/test/README ================================================ This directory is intended for PlatformIO Test Runner and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. More information about PlatformIO Unit Testing: - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html ================================================ FILE: benchmark/psychichttps/.gitignore ================================================ .pio .vscode/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch ================================================ FILE: benchmark/psychichttps/data/server.crt ================================================ -----BEGIN CERTIFICATE----- MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4 sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/ ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3 emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo= -----END CERTIFICATE----- ================================================ FILE: benchmark/psychichttps/data/server.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al 3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg 0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9 Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX 49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc +3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6 pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D 0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin 7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1 noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8 4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/ nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3 q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2 lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr v/t+MeGJP/0Zw8v/X2CFll96 -----END PRIVATE KEY----- ================================================ FILE: benchmark/psychichttps/include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: benchmark/psychichttps/lib/README ================================================ This directory is intended for project specific (private) libraries. PlatformIO will compile them to static libraries and link into executable file. The source code of each library should be placed in a an own separate directory ("lib/your_library_name/[here are source files]"). For example, see a structure of the following two libraries `Foo` and `Bar`: |--lib | | | |--Bar | | |--docs | | |--examples | | |--src | | |- Bar.c | | |- Bar.h | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html | | | |--Foo | | |- Foo.c | | |- Foo.h | | | |- README --> THIS FILE | |- platformio.ini |--src |- main.c and a contents of `src/main.c`: ``` #include #include int main (void) { ... } ``` PlatformIO Library Dependency Finder will find automatically dependent libraries scanning project source files. More information about PlatformIO Library Dependency Finder - https://docs.platformio.org/page/librarymanager/ldf.html ================================================ FILE: benchmark/psychichttps/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env] platform = espressif32 framework = arduino board = esp32dev monitor_speed = 115200 monitor_filters = esp32_exception_decoder lib_deps = https://github.com/hoeken/PsychicHttp bblanchon/ArduinoJson board_build.filesystem = littlefs [env:default] ================================================ FILE: benchmark/psychichttps/src/main.cpp ================================================ /* Wi-Fi STA Connect and Disconnect Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ #include "_secret.h" #include #include #include #include #include #include #ifndef WIFI_SSID #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #endif // Enter your WIFI credentials in secret.h const char* ssid = WIFI_SSID; const char* password = WIFI_PASS; PsychicHttpsServer server; PsychicWebSocketHandler websocketHandler; String server_cert; String server_key; const char* htmlContent = R"( Sample HTML

Hello, World!

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo dapibus elit, id varius sem dui id lacus.

)"; bool connectToWifi() { Serial.println(); Serial.print("[WiFi] Connecting to "); Serial.println(ssid); WiFi.setSleep(false); WiFi.begin(ssid, password); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } void setup() { Serial.begin(115200); delay(10); Serial.println("PsychicHTTP Benchmark"); if (connectToWifi()) { if (!LittleFS.begin()) { Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); return; } File fp = LittleFS.open("/server.crt"); if (fp) { server_cert = fp.readString(); } else { Serial.println("server.pem not found, SSL not available"); return; } fp.close(); File fp2 = LittleFS.open("/server.key"); if (fp2) { server_key = fp2.readString(); } else { Serial.println("server.key not found, SSL not available"); return; } fp2.close(); // start our server server.setCertificate(server_cert.c_str(), server_key.c_str()); // our index server.on("/", HTTP_GET, [](PsychicRequest* request) { return response->send(200, "text/html", htmlContent); }); // serve static files from LittleFS/www on / server.serveStatic("/", LittleFS, "/www/"); // a websocket echo server websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) { response->send(frame); return ESP_OK; }); server.on("/ws", &websocketHandler); // api - parameters passed in via query eg. /api/endpoint?foo=bar server.on("/api", HTTP_GET, [](PsychicRequest* request) { //create a response object StaticJsonDocument<128> output; output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); //work with some params if (request->hasParam("foo")) { String foo = request->getParam("foo")->value(); output["foo"] = foo; } //serialize and return String jsonBuffer; serializeJson(output, jsonBuffer); return response->send(200, "application/json", jsonBuffer.c_str()); }); } } unsigned long last; void loop() { if (millis() - last > 1000) { Serial.printf("Free Heap: %d\n", esp_get_free_heap_size()); last = millis(); } } ================================================ FILE: benchmark/psychichttps/src/secret.h ================================================ #define WIFI_SSID "Your_SSID" #define WIFI_PASS "Your_PASS" ================================================ FILE: benchmark/psychichttps/test/README ================================================ This directory is intended for PlatformIO Test Runner and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. More information about PlatformIO Unit Testing: - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html ================================================ FILE: benchmark/results/arduinomongoose-http-loadtest.log ================================================ CLIENTS: *** 1 *** Running 60s test @ http://192.168.2.131/ 1 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 720 ms │ 14880 ms │ 29087 ms │ 29519 ms │ 15024.03 ms │ 8572.14 ms │ 29737 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 11 │ 11 │ 12 │ 14 │ 12.47 │ 0.87 │ 11 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 48.2 kB │ 48.2 kB │ 52.6 kB │ 61.3 kB │ 54.6 kB │ 3.79 kB │ 48.2 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 748 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.07s, 3.28 MB read 748 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 1 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 781 ms │ 15061 ms │ 29571 ms │ 30046 ms │ 15137.29 ms │ 8833.98 ms │ 30393 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 17 │ 18 │ 20 │ 23 │ 20.34 │ 1.27 │ 17 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 2.99 kB │ 3.15 kB │ 3.52 kB │ 4.03 kB │ 3.57 kB │ 220 B │ 2.99 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1220 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.06s, 214 kB read 1k errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 1 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 969 ms │ 15300 ms │ 29831 ms │ 30132 ms │ 15143.83 ms │ 8807.54 ms │ 30385 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 3 │ 4 │ 3.15 │ 0.36 │ 3 │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.1 kB │ 86.1 kB │ 86.1 kB │ 115 kB │ 90.4 kB │ 10.3 kB │ 86.1 kB │ └───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 189 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 378 requests in 60.06s, 5.43 MB read 188 errors (0 timeouts) ---------------- CLIENTS: *** 2 *** Running 60s test @ http://192.168.2.131/ 2 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 803 ms │ 14773 ms │ 29269 ms │ 29642 ms │ 14925.25 ms │ 8556.82 ms │ 29974 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬───────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 17 │ 17 │ 19 │ 21 │ 18.9 │ 0.98 │ 17 │ ├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 74.5 kB │ 74.5 kB │ 83.3 kB │ 92 kB │ 82.8 kB │ 4.29 kB │ 74.5 kB │ └───────────┴─────────┴─────────┴─────────┴───────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1134 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.06s, 4.97 MB read 29 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 2 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 772 ms │ 15252 ms │ 29307 ms │ 29709 ms │ 15094.92 ms │ 8732.51 ms │ 30070 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 26 │ 27 │ 29 │ 32 │ 29.72 │ 1.43 │ 26 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 4.58 kB │ 4.75 kB │ 5.11 kB │ 5.63 kB │ 5.23 kB │ 251 B │ 4.58 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1783 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 314 kB read 1k errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 2 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 856 ms │ 15635 ms │ 28880 ms │ 29310 ms │ 15261.99 ms │ 8577.65 ms │ 29700 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 4 │ 4 │ 5 │ 6 │ 4.64 │ 0.64 │ 4 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 115 kB │ 115 kB │ 144 kB │ 172 kB │ 133 kB │ 18.1 kB │ 115 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 278 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 558 requests in 60.06s, 7.98 MB read 17 errors (0 timeouts) ---------------- CLIENTS: *** 3 *** Running 60s test @ http://192.168.2.131/ 3 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 794 ms │ 15166 ms │ 29323 ms │ 29697 ms │ 15066.78 ms │ 8676.25 ms │ 30114 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 22 │ 23 │ 26 │ 29 │ 25.99 │ 1.42 │ 22 │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 96.4 kB │ 101 kB │ 114 kB │ 127 kB │ 114 kB │ 6.22 kB │ 96.4 kB │ └───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1559 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 6.83 MB read 14 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 3 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 826 ms │ 14949 ms │ 29377 ms │ 29790 ms │ 15049.95 ms │ 8720.15 ms │ 30168 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 32 │ 32 │ 36 │ 39 │ 36.1 │ 1.82 │ 32 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 5.63 kB │ 5.63 kB │ 6.34 kB │ 6.87 kB │ 6.36 kB │ 319 B │ 5.63 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2166 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.1s, 381 kB read 1k errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 3 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬─────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼─────────┼──────────┤ │ Latency │ 995 ms │ 15127 ms │ 29464 ms │ 29993 ms │ 15219.94 ms │ 8704 ms │ 30258 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴─────────┴──────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 6 │ 6 │ 5.62 │ 0.99 │ 3 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.1 kB │ 86.1 kB │ 172 kB │ 172 kB │ 161 kB │ 28.3 kB │ 86.1 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 337 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 677 requests in 60.07s, 9.67 MB read 54 errors (0 timeouts) ---------------- CLIENTS: *** 4 *** Running 60s test @ http://192.168.2.131/ 4 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 702 ms │ 15255 ms │ 29811 ms │ 30226 ms │ 15144.93 ms │ 8830.16 ms │ 30616 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ │ Req/Sec │ 26 │ 27 │ 29 │ 32 │ 29.05 │ 1.58 │ 26 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ │ Bytes/Sec │ 114 kB │ 118 kB │ 127 kB │ 140 kB │ 127 kB │ 6.9 kB │ 114 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1743 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 7.63 MB read 20 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 4 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬───────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼───────────┼──────────┤ │ Latency │ 762 ms │ 15231 ms │ 29096 ms │ 29534 ms │ 15112.66 ms │ 8660.6 ms │ 29997 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴───────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ │ Req/Sec │ 35 │ 35 │ 41 │ 44 │ 40.89 │ 1.9 │ 35 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ │ Bytes/Sec │ 6.16 kB │ 6.16 kB │ 7.22 kB │ 7.75 kB │ 7.2 kB │ 334 B │ 6.16 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2453 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 432 kB read 977 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 4 connections 1 workers ┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 1175 ms │ 15413 ms │ 29485 ms │ 30066 ms │ 15217.07 ms │ 8604.35 ms │ 30126 ms │ └─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 4 │ 4 │ 8 │ 8 │ 6.47 │ 1.87 │ 4 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 115 kB │ 115 kB │ 230 kB │ 230 kB │ 186 kB │ 53.6 kB │ 115 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 388 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 780 requests in 60.05s, 11.1 MB read 39 errors (0 timeouts) ---------------- CLIENTS: *** 5 *** Running 60s test @ http://192.168.2.131/ 5 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 802 ms │ 14978 ms │ 29391 ms │ 29863 ms │ 15042.57 ms │ 8642.88 ms │ 30280 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ │ Req/Sec │ 26 │ 29 │ 33 │ 37 │ 33.19 │ 2.13 │ 26 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ │ Bytes/Sec │ 114 kB │ 127 kB │ 145 kB │ 162 kB │ 145 kB │ 9.3 kB │ 114 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1991 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.07s, 8.72 MB read 10 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 5 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 724 ms │ 15356 ms │ 29304 ms │ 29791 ms │ 15198.71 ms │ 8761.23 ms │ 30174 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 40 │ 40 │ 45 │ 50 │ 44.84 │ 2.78 │ 40 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.04 kB │ 7.04 kB │ 7.92 kB │ 8.81 kB │ 7.89 kB │ 489 B │ 7.04 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2690 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 473 kB read 603 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 5 connections 1 workers ┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 1250 ms │ 15289 ms │ 29607 ms │ 30193 ms │ 15345.82 ms │ 8719.55 ms │ 30354 ms │ └─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 10 │ 7.25 │ 2.25 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 172 kB │ 287 kB │ 208 kB │ 64.5 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 435 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 875 requests in 60.06s, 12.5 MB read 36 errors (0 timeouts) ---------------- CLIENTS: *** 6 *** Running 60s test @ http://192.168.2.131/ 6 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 847 ms │ 14920 ms │ 28899 ms │ 29392 ms │ 15008.54 ms │ 8456.33 ms │ 29870 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 33 │ 33 │ 36 │ 39 │ 35.75 │ 1.75 │ 33 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 145 kB │ 145 kB │ 158 kB │ 171 kB │ 157 kB │ 7.65 kB │ 145 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2145 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 9.4 MB read 3 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 6 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 890 ms │ 14807 ms │ 29142 ms │ 29732 ms │ 14934.33 ms │ 8513.39 ms │ 30168 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 44 │ 44 │ 48 │ 53 │ 48.3 │ 2.22 │ 44 │ ├───────────┼─────────┼─────────┼────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.75 kB │ 7.75 kB │ 8.5 kB │ 9.38 kB │ 8.54 kB │ 395 B │ 7.74 kB │ └───────────┴─────────┴─────────┴────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2898 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 6k requests in 60.07s, 512 kB read 386 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 6 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 853 ms │ 15325 ms │ 29742 ms │ 30578 ms │ 15287.1 ms │ 8714.92 ms │ 30650 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 6 │ 6 │ 6 │ 12 │ 7.7 │ 2.44 │ 6 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 172 kB │ 172 kB │ 172 kB │ 345 kB │ 221 kB │ 70 kB │ 172 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 462 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 930 requests in 60.06s, 13.3 MB read 24 errors (0 timeouts) ---------------- CLIENTS: *** 7 *** Running 60s test @ http://192.168.2.131/ 7 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 785 ms │ 15141 ms │ 29506 ms │ 29981 ms │ 15124.91 ms │ 8769.84 ms │ 30379 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ │ Req/Sec │ 32 │ 33 │ 37 │ 40 │ 36.64 │ 2.04 │ 32 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼────────┼────────┤ │ Bytes/Sec │ 140 kB │ 145 kB │ 162 kB │ 175 kB │ 160 kB │ 8.9 kB │ 140 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2198 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 9.63 MB read 1 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 7 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 773 ms │ 15322 ms │ 29472 ms │ 29911 ms │ 15145.75 ms │ 8792.14 ms │ 30238 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 45 │ 45 │ 51 │ 56 │ 50.62 │ 3.07 │ 45 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.97 kB │ 7.97 kB │ 9.03 kB │ 9.92 kB │ 8.96 kB │ 541 B │ 7.96 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3037 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 6k requests in 60.06s, 538 kB read 252 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 7 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 903 ms │ 15426 ms │ 29337 ms │ 30187 ms │ 15395.14 ms │ 8592.24 ms │ 30239 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 7 │ 7 │ 7 │ 14 │ 8.06 │ 2.03 │ 7 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 201 kB │ 201 kB │ 201 kB │ 402 kB │ 231 kB │ 58 kB │ 201 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 483 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 973 requests in 60.06s, 13.9 MB read 27 errors (0 timeouts) ---------------- CLIENTS: *** 8 *** Running 60s test @ http://192.168.2.131/ 8 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 947 ms │ 15230 ms │ 29194 ms │ 29710 ms │ 15139.81 ms │ 8698.37 ms │ 30418 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 31 │ 33 │ 38 │ 43 │ 37.92 │ 2.7 │ 31 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 136 kB │ 145 kB │ 167 kB │ 188 kB │ 166 kB │ 11.8 kB │ 136 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2275 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 9.96 MB read 1 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 8 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 818 ms │ 15102 ms │ 29043 ms │ 29502 ms │ 14999.69 ms │ 8454.75 ms │ 30295 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 44 │ 45 │ 51 │ 60 │ 51.37 │ 3.35 │ 44 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.79 kB │ 7.97 kB │ 9.03 kB │ 10.6 kB │ 9.09 kB │ 591 B │ 7.79 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3082 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 6k requests in 60s, 546 kB read 222 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 8 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 951 ms │ 15565 ms │ 30182 ms │ 30252 ms │ 15396.59 ms │ 8860.08 ms │ 31138 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 7 │ 8 │ 8 │ 14 │ 8.56 │ 1.59 │ 7 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 201 kB │ 230 kB │ 230 kB │ 402 kB │ 245 kB │ 45.5 kB │ 201 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 513 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.06s, 14.7 MB read 31 errors (0 timeouts) ---------------- CLIENTS: *** 9 *** Running 60s test @ http://192.168.2.131/ 9 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬───────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼───────────┼──────────┤ │ Latency │ 951 ms │ 15246 ms │ 28914 ms │ 29402 ms │ 15108.58 ms │ 8543.6 ms │ 30524 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴───────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 31 │ 32 │ 37 │ 43 │ 37.42 │ 2.9 │ 31 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 136 kB │ 140 kB │ 162 kB │ 188 kB │ 164 kB │ 12.7 kB │ 136 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2245 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.05s, 9.83 MB read 1 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 9 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 885 ms │ 15043 ms │ 29217 ms │ 29730 ms │ 14953.4 ms │ 8530.16 ms │ 30399 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 44 │ 46 │ 52 │ 58 │ 52.1 │ 3.22 │ 44 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.79 kB │ 8.14 kB │ 9.21 kB │ 10.3 kB │ 9.22 kB │ 570 B │ 7.79 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3126 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 6k requests in 60.07s, 553 kB read 189 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 9 connections 1 workers ┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 1079 ms │ 15444 ms │ 28978 ms │ 29935 ms │ 15389.63 ms │ 8469.15 ms │ 30003 ms │ └─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 9 │ 10 │ 8.56 │ 2.21 │ 1 │ ├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 258 kB │ 287 kB │ 245 kB │ 63.4 kB │ 28.7 kB │ └───────────┴─────┴──────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 513 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.06s, 14.7 MB read 20 errors (0 timeouts) ---------------- CLIENTS: *** 10 *** Running 60s test @ http://192.168.2.131/ 10 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 996 ms │ 15183 ms │ 29343 ms │ 29946 ms │ 15135.07 ms │ 8665.13 ms │ 30794 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 32 │ 32 │ 37 │ 43 │ 37.59 │ 2.43 │ 32 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 140 kB │ 140 kB │ 162 kB │ 188 kB │ 165 kB │ 10.6 kB │ 140 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2255 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 9.88 MB read 5 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 10 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 876 ms │ 15390 ms │ 29322 ms │ 29837 ms │ 15193.95 ms │ 8726.45 ms │ 30887 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 46 │ 47 │ 52 │ 60 │ 52.42 │ 3.63 │ 46 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 8.14 kB │ 8.32 kB │ 9.21 kB │ 10.6 kB │ 9.28 kB │ 642 B │ 8.14 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3145 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 6k requests in 60.07s, 557 kB read 193 errors (0 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 10 connections 1 workers ┌─────────┬─────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 1126 ms │ 15812 ms │ 30288 ms │ 31239 ms │ 15913.56 ms │ 8747.22 ms │ 32439 ms │ └─────────┴─────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────┬─────────┬────────┬────────┬────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼─────────┼────────┼────────┼────────┼───────┼─────────┤ │ Req/Sec │ 0 │ 2 │ 10 │ 10 │ 8.84 │ 2.37 │ 2 │ ├───────────┼─────┼─────────┼────────┼────────┼────────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 57.4 kB │ 287 kB │ 287 kB │ 254 kB │ 68 kB │ 57.4 kB │ └───────────┴─────┴─────────┴────────┴────────┴────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 530 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.06s, 15.2 MB read 42 errors (0 timeouts) ---------------- CLIENTS: *** 15 *** Running 60s test @ http://192.168.2.131/ 15 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 15 connections 1 workers ┌─────────┬────────┬──────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼──────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 690 ms │ 12131 ms │ 28685 ms │ 29798 ms │ 13051.88 ms │ 8385.32 ms │ 32739 ms │ └─────────┴────────┴──────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 46 │ 50 │ 56 │ 63 │ 55.99 │ 3.53 │ 46 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 8.14 kB │ 8.86 kB │ 9.92 kB │ 11.2 kB │ 9.91 kB │ 624 B │ 8.14 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3359 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 7k requests in 60.06s, 595 kB read 269 errors (5 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 15 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- CLIENTS: *** 20 *** Running 60s test @ http://192.168.2.131/ 20 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 20 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 377 ms │ 5749 ms │ 19598 ms │ 21657 ms │ 6953.55 ms │ 5299.16 ms │ 23969 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 45 │ 45 │ 55 │ 60 │ 54.27 │ 3.9 │ 45 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.88 kB │ 7.88 kB │ 9.63 kB │ 10.6 kB │ 9.53 kB │ 687 B │ 7.88 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3256 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 7k requests in 60.06s, 571 kB read 312 errors (30 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 20 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- ================================================ FILE: benchmark/results/arduinomongoose-websocket-loadtest.log ================================================ CLIENTS: *** 1 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 1 Agent: none Completed requests: 3750 Total errors: 0 Total time: 60.001 s Mean latency: 15.5 ms Effective rps: 62 Percentage of requests served within a certain time 50% 12 ms 90% 18 ms 95% 36 ms 99% 80 ms 100% 223 ms (longest request) CLIENTS: *** 2 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 2 Agent: none Completed requests: 5795 Total errors: 0 Total time: 60.004 s Mean latency: 20.2 ms Effective rps: 97 Percentage of requests served within a certain time 50% 16 ms 90% 27 ms 95% 64 ms 99% 86 ms 100% 108 ms (longest request) CLIENTS: *** 3 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 3 Agent: none Completed requests: 7445 Total errors: 0 Total time: 60.003 s Mean latency: 23.6 ms Effective rps: 124 Percentage of requests served within a certain time 50% 19 ms 90% 32 ms 95% 70 ms 99% 92 ms 100% 121 ms (longest request) CLIENTS: *** 4 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 4 Agent: none Completed requests: 8751 Total errors: 0 Total time: 60.005 s Mean latency: 26.9 ms Effective rps: 146 Percentage of requests served within a certain time 50% 22 ms 90% 38 ms 95% 73 ms 99% 95 ms 100% 115 ms (longest request) CLIENTS: *** 5 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 5 Agent: none Completed requests: 9953 Total errors: 0 Total time: 60.004 s Mean latency: 29.6 ms Effective rps: 166 Percentage of requests served within a certain time 50% 25 ms 90% 42 ms 95% 74 ms 99% 93 ms 100% 116 ms (longest request) CLIENTS: *** 6 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 6 Agent: none Completed requests: 10871 Total errors: 0 Total time: 60.005 s Mean latency: 32.6 ms Effective rps: 181 Percentage of requests served within a certain time 50% 27 ms 90% 50 ms 95% 82 ms 99% 100 ms 100% 116 ms (longest request) CLIENTS: *** 7 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 7 Agent: none Completed requests: 11777 Total errors: 0 Total time: 60.003 s Mean latency: 35.1 ms Effective rps: 196 Percentage of requests served within a certain time 50% 30 ms 90% 66 ms 95% 83 ms 99% 101 ms 100% 137 ms (longest request) CLIENTS: *** 8 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 8 Running on cores: 2 Agent: none Completed requests: 11639 Total errors: 0 Total time: 60.004 s Mean latency: 35.4 ms Effective rps: 194 Percentage of requests served within a certain time 50% 30 ms 90% 67 ms 95% 86 ms 99% 106 ms 100% 135 ms (longest request) CLIENTS: *** 10 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 10 Running on cores: 2 Agent: none Completed requests: 11619 Total errors: 0 Total time: 60.004 s Mean latency: 35.6 ms Effective rps: 194 Percentage of requests served within a certain time 50% 30 ms 90% 71 ms 95% 87 ms 99% 105 ms 100% 125 ms (longest request) CLIENTS: *** 16 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 16 Running on cores: 2 Agent: none Completed requests: 15314 Total errors: 0 Total time: 60.005 s Mean latency: 54.2 ms Effective rps: 255 Percentage of requests served within a certain time 50% 46 ms 90% 91 ms 95% 105 ms 99% 127 ms 100% 826 ms (longest request) CLIENTS: *** 20 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 20 Running on cores: 2 Agent: none Completed requests: 15370 Total errors: 0 Total time: 60.005 s Mean latency: 57.7 ms Effective rps: 256 Percentage of requests served within a certain time 50% 48 ms 90% 96 ms 95% 110 ms 99% 132 ms 100% 851 ms (longest request) ================================================ FILE: benchmark/results/espasync-http-loadtest.log ================================================ CLIENTS: *** 1 *** Running 60s test @ http://192.168.2.131/ 1 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 32 ms │ 40 ms │ 118 ms │ 118 ms │ 54.67 ms │ 29.29 ms │ 118 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.1 │ 0.31 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 4.38 kB │ 438 B │ 1.31 kB │ 4.38 kB │ └───────────┴─────┴──────┴─────┴─────────┴───────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 6 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 12 requests in 60.08s, 26.3 kB read 5 errors (5 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 1 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 61 ms │ 64 ms │ 131 ms │ 131 ms │ 93.84 ms │ 31.05 ms │ 131 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬───────┬────────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼───────┼────────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.1 │ 0.31 │ 1 │ ├───────────┼─────┼──────┼─────┼───────┼────────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 174 B │ 17.4 B │ 52.2 B │ 174 B │ └───────────┴─────┴──────┴─────┴───────┴────────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 6 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 12 requests in 60.08s, 1.04 kB read 5 errors (5 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 1 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 135 ms │ 142 ms │ 162 ms │ 162 ms │ 145.84 ms │ 10.32 ms │ 162 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.1 │ 0.31 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 2.88 kB │ 8.63 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 6 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 12 requests in 60.07s, 173 kB read 5 errors (5 timeouts) ---------------- CLIENTS: *** 2 *** Running 60s test @ http://192.168.2.131/ 2 connections 1 workers ┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬───────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼───────┤ │ Latency │ 29 ms │ 52 ms │ 93 ms │ 93 ms │ 56.09 ms │ 17.92 ms │ 93 ms │ └─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴───────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.2 │ 0.61 │ 2 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 8.76 kB │ 876 B │ 2.63 kB │ 8.76 kB │ └───────────┴─────┴──────┴─────┴─────────┴───────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 12 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 24 requests in 60.08s, 52.5 kB read 10 errors (10 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 2 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 60 ms │ 106 ms │ 126 ms │ 126 ms │ 102.5 ms │ 21.86 ms │ 126 ms │ └─────────┴───────┴────────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬───────┬────────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.2 │ 0.61 │ 2 │ ├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 348 B │ 34.8 B │ 104 B │ 348 B │ └───────────┴─────┴──────┴─────┴───────┴────────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 12 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 24 requests in 60.08s, 2.09 kB read 10 errors (10 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 2 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 228 ms │ 245 ms │ 268 ms │ 268 ms │ 245.34 ms │ 13.38 ms │ 268 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.2 │ 0.61 │ 2 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 57.5 kB │ 5.75 kB │ 17.3 kB │ 57.5 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 12 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 24 requests in 60.07s, 345 kB read 10 errors (10 timeouts) ---------------- CLIENTS: *** 3 *** Running 60s test @ http://192.168.2.131/ 3 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 40 ms │ 97 ms │ 133 ms │ 133 ms │ 95.45 ms │ 30.82 ms │ 133 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.3 │ 0.91 │ 3 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 13.1 kB │ 1.31 kB │ 3.94 kB │ 13.1 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 18 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 36 requests in 60.08s, 78.8 kB read 15 errors (15 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 3 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 55 ms │ 66 ms │ 211 ms │ 211 ms │ 82.78 ms │ 37.03 ms │ 211 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬───────┬────────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.3 │ 0.91 │ 3 │ ├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 522 B │ 52.2 B │ 157 B │ 522 B │ └───────────┴─────┴──────┴─────┴───────┴────────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 18 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 36 requests in 60.1s, 3.13 kB read 15 errors (15 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 3 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 215 ms │ 278 ms │ 348 ms │ 348 ms │ 280.56 ms │ 30.51 ms │ 348 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.3 │ 0.91 │ 3 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.3 kB │ 8.62 kB │ 25.9 kB │ 86.3 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 18 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 36 requests in 60.15s, 518 kB read 15 errors (15 timeouts) ---------------- CLIENTS: *** 4 *** Running 60s test @ http://192.168.2.131/ 4 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 29 ms │ 76 ms │ 137 ms │ 137 ms │ 86.55 ms │ 35.16 ms │ 137 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 4 │ 0.4 │ 1.21 │ 4 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 17.5 kB │ 1.75 kB │ 5.25 kB │ 17.5 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 24 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 48 requests in 60.13s, 105 kB read 20 errors (20 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 4 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬───────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼───────────┼────────┤ │ Latency │ 54 ms │ 108 ms │ 523 ms │ 523 ms │ 164.71 ms │ 130.96 ms │ 523 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴───────────┴────────┘ ┌───────────┬─────┬──────┬─────┬───────┬────────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 4 │ 0.4 │ 1.21 │ 4 │ ├───────────┼─────┼──────┼─────┼───────┼────────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 696 B │ 69.6 B │ 209 B │ 696 B │ └───────────┴─────┴──────┴─────┴───────┴────────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 24 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 48 requests in 60.09s, 4.18 kB read 20 errors (20 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 4 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼─────────┼────────┤ │ Latency │ 322 ms │ 379 ms │ 405 ms │ 405 ms │ 372.55 ms │ 20.5 ms │ 405 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴─────────┴────────┘ ┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 4 │ 0.4 │ 1.21 │ 4 │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 115 kB │ 11.5 kB │ 34.5 kB │ 115 kB │ └───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 24 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 48 requests in 60.08s, 690 kB read 20 errors (20 timeouts) ---------------- CLIENTS: *** 5 *** Running 60s test @ http://192.168.2.131/ 5 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬─────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼─────────┼─────────┼────────┤ │ Latency │ 36 ms │ 104 ms │ 148 ms │ 148 ms │ 94.7 ms │ 35.9 ms │ 148 ms │ └─────────┴───────┴────────┴────────┴────────┴─────────┴─────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 5 │ 0.5 │ 1.5 │ 5 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 21.9 kB │ 2.19 kB │ 6.57 kB │ 21.9 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 30 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 60 requests in 60.05s, 131 kB read 25 errors (25 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 5 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 66 ms │ 123 ms │ 263 ms │ 263 ms │ 127.67 ms │ 37.33 ms │ 263 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬───────┬──────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼───────┼──────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 5 │ 0.5 │ 1.5 │ 5 │ ├───────────┼─────┼──────┼─────┼───────┼──────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 870 B │ 87 B │ 261 B │ 870 B │ └───────────┴─────┴──────┴─────┴───────┴──────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 30 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 60 requests in 60.07s, 5.22 kB read 25 errors (25 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 5 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 348 ms │ 398 ms │ 465 ms │ 465 ms │ 399.27 ms │ 31.99 ms │ 465 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 5 │ 0.5 │ 1.5 │ 5 │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 144 kB │ 14.4 kB │ 43.1 kB │ 144 kB │ └───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 30 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 60 requests in 60.08s, 863 kB read 25 errors (25 timeouts) ---------------- CLIENTS: *** 6 *** Running 60s test @ http://192.168.2.131/ 6 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 34 ms │ 111 ms │ 188 ms │ 188 ms │ 113.25 ms │ 41.29 ms │ 188 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 6 │ 0.6 │ 1.81 │ 6 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 26.3 kB │ 2.63 kB │ 7.88 kB │ 26.3 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 72 requests in 60.07s, 158 kB read 30 errors (30 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 6 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 61 ms │ 121 ms │ 296 ms │ 296 ms │ 134.95 ms │ 46.95 ms │ 296 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 6 │ 0.6 │ 1.81 │ 6 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.05 kB │ 105 B │ 315 B │ 1.05 kB │ └───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 72 requests in 60.09s, 6.3 kB read 30 errors (30 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 6 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 322 ms │ 422 ms │ 522 ms │ 522 ms │ 422.98 ms │ 51.58 ms │ 522 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 6 │ 0.6 │ 1.81 │ 6 │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 173 kB │ 17.2 kB │ 51.7 kB │ 173 kB │ └───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 72 requests in 60.08s, 1.04 MB read 30 errors (30 timeouts) ---------------- CLIENTS: *** 7 *** Running 60s test @ http://192.168.2.131/ 7 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 60 ms │ 129 ms │ 220 ms │ 280 ms │ 127.81 ms │ 51.08 ms │ 280 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 7 │ 0.7 │ 2.1 │ 7 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 30.7 kB │ 3.06 kB │ 9.19 kB │ 30.6 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 42 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 84 requests in 60.08s, 184 kB read 35 errors (35 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 7 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 63 ms │ 125 ms │ 252 ms │ 289 ms │ 126.22 ms │ 46.41 ms │ 289 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 7 │ 0.7 │ 2.1 │ 7 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.23 kB │ 123 B │ 368 B │ 1.23 kB │ └───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 42 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 84 requests in 60.07s, 7.35 kB read 35 errors (35 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 7 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬──────────┬───────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼──────────┼───────────┼────────┤ │ Latency │ 144 ms │ 439 ms │ 877 ms │ 921 ms │ 439.3 ms │ 138.35 ms │ 921 ms │ └─────────┴────────┴────────┴────────┴────────┴──────────┴───────────┴────────┘ ┌───────────┬─────┬──────┬─────┬────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 7 │ 0.69 │ 1.87 │ 1 │ ├───────────┼─────┼──────┼─────┼────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 201 kB │ 19.6 kB │ 53.7 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 41 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 83 requests in 60.07s, 1.18 MB read 35 errors (35 timeouts) ---------------- CLIENTS: *** 8 *** Running 60s test @ http://192.168.2.131/ 8 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼────────┼──────────┼────────┤ │ Latency │ 74 ms │ 141 ms │ 193 ms │ 198 ms │ 143 ms │ 31.11 ms │ 198 ms │ └─────────┴───────┴────────┴────────┴────────┴────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬───────┬────────┬─────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼───────┼────────┼─────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 8 │ 0.8 │ 2.41 │ 8 │ ├───────────┼─────┼──────┼─────┼───────┼────────┼─────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 35 kB │ 3.5 kB │ 10.5 kB │ 35 kB │ └───────────┴─────┴──────┴─────┴───────┴────────┴─────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 48 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 96 requests in 60.08s, 210 kB read 40 errors (40 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 8 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 59 ms │ 138 ms │ 284 ms │ 319 ms │ 139.8 ms │ 53.96 ms │ 319 ms │ └─────────┴───────┴────────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬────────┬───────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼────────┼───────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 8 │ 0.8 │ 2.36 │ 1 │ ├───────────┼─────┼──────┼─────┼────────┼───────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.4 kB │ 140 B │ 411 B │ 175 B │ └───────────┴─────┴──────┴─────┴────────┴───────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 48 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 96 requests in 60.07s, 8.4 kB read 40 errors (40 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 8 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- CLIENTS: *** 9 *** Running 60s test @ http://192.168.2.131/ 9 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 31 ms │ 116 ms │ 206 ms │ 210 ms │ 109.58 ms │ 45.11 ms │ 210 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 9 │ 0.9 │ 2.7 │ 9 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 39.4 kB │ 3.94 kB │ 11.8 kB │ 39.4 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 54 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 108 requests in 60.08s, 236 kB read 45 errors (45 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 9 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 66 ms │ 131 ms │ 313 ms │ 349 ms │ 139.95 ms │ 60.66 ms │ 349 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 9 │ 0.9 │ 2.7 │ 9 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.57 kB │ 158 B │ 473 B │ 1.57 kB │ └───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 54 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 108 requests in 60.08s, 9.45 kB read 45 errors (45 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 9 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- CLIENTS: *** 10 *** Running 60s test @ http://192.168.2.131/ 10 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 35 ms │ 96 ms │ 264 ms │ 267 ms │ 103.97 ms │ 54.56 ms │ 267 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 10 │ 1 │ 3 │ 10 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 43.8 kB │ 4.38 kB │ 13.1 kB │ 43.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 60 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 120 requests in 60.08s, 263 kB read 50 errors (50 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 10 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 64 ms │ 144 ms │ 411 ms │ 437 ms │ 156.64 ms │ 80.67 ms │ 437 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 10 │ 1 │ 2.83 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.75 kB │ 175 B │ 495 B │ 175 B │ └───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 60 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 120 requests in 60.08s, 10.5 kB read 50 errors (50 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 10 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- CLIENTS: *** 15 *** Running 60s test @ http://192.168.2.131/ 15 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬──────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼──────────┼─────────┼────────┤ │ Latency │ 29 ms │ 104 ms │ 359 ms │ 362 ms │ 121.5 ms │ 77.5 ms │ 362 ms │ └─────────┴───────┴────────┴────────┴────────┴──────────┴─────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 14 │ 1.34 │ 4.02 │ 12 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 52.6 kB │ 5.12 kB │ 15.4 kB │ 48.3 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 80 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 170 requests in 60.09s, 307 kB read 75 errors (75 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 15 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬───────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼───────────┼────────┤ │ Latency │ 91 ms │ 148 ms │ 505 ms │ 574 ms │ 174.38 ms │ 100.31 ms │ 574 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴───────────┴────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 15 │ 1.5 │ 4.06 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 2.63 kB │ 263 B │ 710 B │ 175 B │ └───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 90 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 180 requests in 60.08s, 15.8 kB read 75 errors (75 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 15 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- CLIENTS: *** 20 *** Running 60s test @ http://192.168.2.131/ 20 connections 1 workers ┌─────────┬───────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ │ Latency │ 62 ms │ 1134 ms │ 6942 ms │ 6976 ms │ 1884.35 ms │ 1805.12 ms │ 6976 ms │ └─────────┴───────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 15 │ 1.44 │ 4.15 │ 4 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 57.2 kB │ 4.42 kB │ 14.2 kB │ 5.82 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 86 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 211 requests in 60.08s, 265 kB read 105 errors (100 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 20 connections 1 workers ┌─────────┬───────┬────────┬─────────┬─────────┬───────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼─────────┼─────────┼───────────┼────────────┼─────────┤ │ Latency │ 62 ms │ 232 ms │ 4030 ms │ 6972 ms │ 586.75 ms │ 1218.03 ms │ 6972 ms │ └─────────┴───────┴────────┴─────────┴─────────┴───────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬───────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 11 │ 1.39 │ 3.08 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼───────┼───────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 1.89 kB │ 238 B │ 529 B │ 172 B │ └───────────┴─────┴──────┴─────┴─────────┴───────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 83 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 203 requests in 60.09s, 14.3 kB read 100 errors (100 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 20 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- ================================================ FILE: benchmark/results/espasync-websocket-loadtest.log ================================================ CLIENTS: *** 1 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 1 Agent: none Completed requests: 4231 Total errors: 0 Total time: 60.002 s Mean latency: 13.6 ms Effective rps: 71 Percentage of requests served within a certain time 50% 10 ms 90% 16 ms 95% 24 ms 99% 81 ms 100% 280 ms (longest request) CLIENTS: *** 2 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 2 Agent: none Completed requests: 5914 Total errors: 0 Total time: 60.001 s Mean latency: 19.7 ms Effective rps: 99 Percentage of requests served within a certain time 50% 15 ms 90% 26 ms 95% 67 ms 99% 86 ms 100% 109 ms (longest request) CLIENTS: *** 3 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 3 Agent: none Completed requests: 8204 Total errors: 0 Total time: 60.003 s Mean latency: 21.4 ms Effective rps: 137 Percentage of requests served within a certain time 50% 17 ms 90% 29 ms 95% 68 ms 99% 87 ms 100% 104 ms (longest request) CLIENTS: *** 4 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 4 Agent: none Completed requests: 9634 Total errors: 0 Total time: 60.004 s Mean latency: 24.4 ms Effective rps: 161 Percentage of requests served within a certain time 50% 19 ms 90% 33 ms 95% 73 ms 99% 91 ms 100% 145 ms (longest request) CLIENTS: *** 5 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 5 Agent: none Completed requests: 10759 Total errors: 0 Total time: 60.003 s Mean latency: 27.3 ms Effective rps: 179 Percentage of requests served within a certain time 50% 22 ms 90% 39 ms 95% 76 ms 99% 95 ms 100% 117 ms (longest request) CLIENTS: *** 6 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 6 Agent: none Completed requests: 11302 Total errors: 0 Total time: 60.004 s Mean latency: 31.3 ms Effective rps: 188 Percentage of requests served within a certain time 50% 26 ms 90% 58 ms 95% 81 ms 99% 100 ms 100% 122 ms (longest request) CLIENTS: *** 7 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 7 Agent: none Completed requests: 12713 Total errors: 0 Total time: 60.003 s Mean latency: 32.5 ms Effective rps: 212 Percentage of requests served within a certain time 50% 27 ms 90% 52 ms 95% 81 ms 99% 99 ms 100% 125 ms (longest request) CLIENTS: *** 8 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 8 Running on cores: 2 Agent: none Completed requests: 13157 Total errors: 0 Total time: 60.003 s Mean latency: 35.9 ms Effective rps: 219 Percentage of requests served within a certain time 50% 30 ms 90% 71 ms 95% 88 ms 99% 107 ms 100% 132 ms (longest request) CLIENTS: *** 10 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 10 Running on cores: 2 Agent: none Completed requests: 13417 Total errors: 2 Total time: 60.001 s Mean latency: 34.4 ms Effective rps: 224 Percentage of requests served within a certain time 50% 30 ms 90% 53 ms 95% 81 ms 99% 101 ms 100% 124 ms (longest request) -1: 2 errors CLIENTS: *** 16 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 16 Running on cores: 2 Agent: none Completed requests: 12804 Total errors: 7 Total time: 60.001 s Mean latency: 36.4 ms Effective rps: 213 Percentage of requests served within a certain time 50% 30 ms 90% 70 ms 95% 86 ms 99% 106 ms 100% 135 ms (longest request) -1: 7 errors CLIENTS: *** 20 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 20 Running on cores: 2 Agent: none Completed requests: 8421 Total errors: 13 Total time: 60.003 s Mean latency: 37.2 ms Effective rps: 140 Percentage of requests served within a certain time 50% 20 ms 90% 50 ms 95% 77 ms 99% 105 ms 100% 9227 ms (longest request) -1: 13 errors ================================================ FILE: benchmark/results/psychic-http-loadtest.log ================================================ CLIENTS: *** 1 *** Running 60s test @ http://192.168.2.131/ 1 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 24 ms │ 31 ms │ 100 ms │ 134 ms │ 39.16 ms │ 22.82 ms │ 270 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 18 │ 21 │ 25 │ 30 │ 25.19 │ 2.28 │ 18 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 78.1 kB │ 91.1 kB │ 108 kB │ 130 kB │ 109 kB │ 9.88 kB │ 78.1 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1511 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.05s, 6.55 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 1 connections 1 workers ┌─────────┬───────┬───────┬───────┬───────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼───────┼───────┼──────────┼──────────┼────────┤ │ Latency │ 19 ms │ 25 ms │ 91 ms │ 99 ms │ 31.42 ms │ 18.78 ms │ 116 ms │ └─────────┴───────┴───────┴───────┴───────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 26 │ 28 │ 31 │ 34 │ 31.29 │ 1.79 │ 26 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 3.46 kB │ 3.73 kB │ 4.16 kB │ 4.56 kB │ 4.19 kB │ 241 B │ 3.46 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1877 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.06s, 251 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 1 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 141 ms │ 201 ms │ 317 ms │ 331 ms │ 206.85 ms │ 51.73 ms │ 366 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 4 │ 4 │ 5 │ 6 │ 4.82 │ 0.62 │ 4 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 115 kB │ 115 kB │ 144 kB │ 173 kB │ 139 kB │ 17.8 kB │ 115 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 289 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 290 requests in 60.07s, 8.32 MB read ---------------- CLIENTS: *** 2 *** Running 60s test @ http://192.168.2.131/ 2 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 26 ms │ 41 ms │ 167 ms │ 203 ms │ 55.52 ms │ 37.23 ms │ 373 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 29 │ 31 │ 35 │ 40 │ 35.64 │ 2.36 │ 29 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 126 kB │ 135 kB │ 152 kB │ 174 kB │ 155 kB │ 10.2 kB │ 126 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2138 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.06s, 9.27 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 2 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 25 ms │ 35 ms │ 103 ms │ 112 ms │ 43.19 ms │ 21.44 ms │ 142 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 40 │ 40 │ 46 │ 50 │ 45.7 │ 2.76 │ 40 │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 5.36 kB │ 5.36 kB │ 6.17 kB │ 6.7 kB │ 6.12 kB │ 370 B │ 5.36 kB │ └───────────┴─────────┴─────────┴─────────┴────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2742 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 367 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 2 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 188 ms │ 322 ms │ 475 ms │ 507 ms │ 324.51 ms │ 70.51 ms │ 535 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.15 │ 0.63 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 177 kB │ 18 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 369 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 371 requests in 60.06s, 10.6 MB read ---------------- CLIENTS: *** 3 *** Running 60s test @ http://192.168.2.131/ 3 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 31 ms │ 54 ms │ 185 ms │ 212 ms │ 68.63 ms │ 39.36 ms │ 386 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 38 │ 39 │ 43 │ 47 │ 43.32 │ 2.31 │ 38 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 165 kB │ 169 kB │ 187 kB │ 204 kB │ 188 kB │ 10 kB │ 165 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2599 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 11.3 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 3 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 29 ms │ 42 ms │ 116 ms │ 126 ms │ 51.59 ms │ 23.91 ms │ 175 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 40 │ 51 │ 58 │ 64 │ 57.59 │ 4.25 │ 40 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 5.36 kB │ 6.83 kB │ 7.78 kB │ 8.58 kB │ 7.72 kB │ 569 B │ 5.36 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3455 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 463 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 3 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬───────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼───────────┼────────┤ │ Latency │ 203 ms │ 458 ms │ 761 ms │ 814 ms │ 474.54 ms │ 168.16 ms │ 902 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴───────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 4 │ 5 │ 6 │ 7 │ 6.29 │ 0.67 │ 4 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 115 kB │ 144 kB │ 173 kB │ 201 kB │ 181 kB │ 19 kB │ 115 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 377 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 380 requests in 60.06s, 10.9 MB read ---------------- CLIENTS: *** 4 *** Running 60s test @ http://192.168.2.131/ 4 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 37 ms │ 63 ms │ 206 ms │ 239 ms │ 81.32 ms │ 45.45 ms │ 396 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 42 │ 44 │ 49 │ 56 │ 48.87 │ 3.01 │ 42 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 182 kB │ 191 kB │ 213 kB │ 243 kB │ 212 kB │ 13 kB │ 182 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2932 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.05s, 12.7 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 4 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 33 ms │ 50 ms │ 123 ms │ 131 ms │ 58.29 ms │ 23.99 ms │ 159 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 59 │ 59 │ 68 │ 78 │ 68 │ 4.3 │ 59 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 7.91 kB │ 7.91 kB │ 9.12 kB │ 10.5 kB │ 9.11 kB │ 576 B │ 7.91 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4080 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.05s, 547 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 4 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ │ Latency │ 326 ms │ 534 ms │ 1064 ms │ 1141 ms │ 635.61 ms │ 238.71 ms │ 1210 ms │ └─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.27 │ 0.63 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 180 kB │ 18.1 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 376 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 380 requests in 60.05s, 10.8 MB read ---------------- CLIENTS: *** 5 *** Running 60s test @ http://192.168.2.131/ 5 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 44 ms │ 74 ms │ 222 ms │ 261 ms │ 91.54 ms │ 47.88 ms │ 417 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 47 │ 47 │ 54 │ 60 │ 54.29 │ 3.05 │ 47 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 204 kB │ 204 kB │ 234 kB │ 260 kB │ 235 kB │ 13.2 kB │ 204 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3257 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 14.1 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 5 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 36 ms │ 57 ms │ 126 ms │ 138 ms │ 64.99 ms │ 24.42 ms │ 195 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 64 │ 69 │ 76 │ 84 │ 76.32 │ 3.97 │ 64 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 8.58 kB │ 9.25 kB │ 10.2 kB │ 11.3 kB │ 10.2 kB │ 532 B │ 8.58 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4579 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 614 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 5 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ │ Latency │ 439 ms │ 652 ms │ 1393 ms │ 1458 ms │ 780.62 ms │ 290.11 ms │ 2018 ms │ └─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 4 │ 5 │ 6 │ 8 │ 6.35 │ 0.78 │ 4 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 115 kB │ 144 kB │ 173 kB │ 230 kB │ 183 kB │ 22.1 kB │ 115 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 381 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 386 requests in 60.06s, 11 MB read ---------------- CLIENTS: *** 6 *** Running 60s test @ http://192.168.2.131/ 6 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 50 ms │ 85 ms │ 238 ms │ 268 ms │ 102.35 ms │ 50.03 ms │ 517 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 49 │ 53 │ 58 │ 65 │ 58.3 │ 3.13 │ 49 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 213 kB │ 230 kB │ 252 kB │ 282 kB │ 253 kB │ 13.6 kB │ 213 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3498 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.05s, 15.2 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 6 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 41 ms │ 64 ms │ 138 ms │ 151 ms │ 74.53 ms │ 27.06 ms │ 286 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 71 │ 71 │ 80 │ 90 │ 79.92 │ 4.07 │ 71 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 9.59 kB │ 9.59 kB │ 10.8 kB │ 12.2 kB │ 10.8 kB │ 548 B │ 9.59 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4795 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 647 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 6 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼───────────┼──────────┼─────────┤ │ Latency │ 579 ms │ 840 ms │ 1766 ms │ 1816 ms │ 973.38 ms │ 355.3 ms │ 2392 ms │ └─────────┴────────┴────────┴─────────┴─────────┴───────────┴──────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 4 │ 4 │ 6 │ 8 │ 6.1 │ 0.87 │ 4 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 115 kB │ 115 kB │ 173 kB │ 230 kB │ 176 kB │ 25 kB │ 115 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 366 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 372 requests in 60.06s, 10.5 MB read ---------------- CLIENTS: *** 7 *** Running 60s test @ http://192.168.2.131/ 7 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 57 ms │ 96 ms │ 249 ms │ 293 ms │ 113.4 ms │ 53.05 ms │ 640 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 53 │ 54 │ 62 │ 68 │ 61.44 │ 3.82 │ 53 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 230 kB │ 234 kB │ 269 kB │ 295 kB │ 266 kB │ 16.6 kB │ 230 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3686 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.05s, 16 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 7 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 45 ms │ 69 ms │ 145 ms │ 154 ms │ 79.11 ms │ 27.06 ms │ 274 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬───────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼───────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 78 │ 80 │ 89 │ 99 │ 87.9 │ 4.78 │ 78 │ ├───────────┼─────────┼─────────┼───────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 10.5 kB │ 10.8 kB │ 12 kB │ 13.4 kB │ 11.9 kB │ 644 B │ 10.5 kB │ └───────────┴─────────┴─────────┴───────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5274 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 712 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 7 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 671 ms │ 952 ms │ 1965 ms │ 2059 ms │ 1094.09 ms │ 402.31 ms │ 3654 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 8 │ 6.34 │ 0.79 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 230 kB │ 182 kB │ 22.7 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 380 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 387 requests in 60.07s, 10.9 MB read ---------------- CLIENTS: *** 8 *** Running 60s test @ http://192.168.2.131/ 8 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼─────────┤ │ Latency │ 56 ms │ 95 ms │ 243 ms │ 275 ms │ 110.53 ms │ 52.27 ms │ 1027 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 52 │ 54 │ 62 │ 73 │ 62.99 │ 4.89 │ 52 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 226 kB │ 234 kB │ 269 kB │ 317 kB │ 273 kB │ 21.2 kB │ 226 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3779 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 16.4 MB read 6 errors (6 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 8 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬─────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼─────────┼──────────┼────────┤ │ Latency │ 45 ms │ 70 ms │ 146 ms │ 156 ms │ 80.9 ms │ 28.68 ms │ 308 ms │ └─────────┴───────┴───────┴────────┴────────┴─────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 77 │ 77 │ 86 │ 95 │ 85.92 │ 4.89 │ 77 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 10.4 kB │ 10.4 kB │ 11.6 kB │ 12.8 kB │ 11.6 kB │ 659 B │ 10.4 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5155 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 696 kB read 6 errors (6 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 8 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 699 ms │ 979 ms │ 2038 ms │ 2128 ms │ 1111.14 ms │ 406.26 ms │ 3506 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.25 │ 0.6 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 180 kB │ 17.1 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 375 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 389 requests in 60.06s, 10.8 MB read 6 errors (6 timeouts) ---------------- CLIENTS: *** 9 *** Running 60s test @ http://192.168.2.131/ 9 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 57 ms │ 96 ms │ 249 ms │ 277 ms │ 112.81 ms │ 51.23 ms │ 626 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 52 │ 57 │ 61 │ 68 │ 61.7 │ 3.18 │ 52 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 226 kB │ 247 kB │ 265 kB │ 295 kB │ 268 kB │ 13.8 kB │ 226 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3702 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 16.1 MB read 12 errors (12 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 9 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 45 ms │ 70 ms │ 144 ms │ 154 ms │ 79.64 ms │ 27.25 ms │ 239 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬───────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼───────┼─────────┤ │ Req/Sec │ 76 │ 79 │ 87 │ 96 │ 87.34 │ 4.79 │ 76 │ ├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 10.3 kB │ 10.7 kB │ 11.8 kB │ 13 kB │ 11.8 kB │ 647 B │ 10.3 kB │ └───────────┴─────────┴─────────┴─────────┴───────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5240 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.1s, 707 kB read 12 errors (12 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 9 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 664 ms │ 952 ms │ 2001 ms │ 2055 ms │ 1092.56 ms │ 399.94 ms │ 3588 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.35 │ 0.63 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 183 kB │ 18 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 381 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 402 requests in 60.06s, 11 MB read 12 errors (12 timeouts) ---------------- CLIENTS: *** 10 *** Running 60s test @ http://192.168.2.131/ 10 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 56 ms │ 97 ms │ 244 ms │ 277 ms │ 113.43 ms │ 51.43 ms │ 616 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 48 │ 51 │ 61 │ 69 │ 61.42 │ 3.92 │ 48 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 208 kB │ 221 kB │ 265 kB │ 300 kB │ 266 kB │ 17 kB │ 208 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3685 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 16 MB read 18 errors (18 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 10 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 45 ms │ 71 ms │ 147 ms │ 154 ms │ 81.57 ms │ 28.91 ms │ 335 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 75 │ 76 │ 86 │ 92 │ 85.32 │ 4.46 │ 75 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 10.1 kB │ 10.3 kB │ 11.6 kB │ 12.4 kB │ 11.5 kB │ 601 B │ 10.1 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5119 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 691 kB read 18 errors (18 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 10 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 705 ms │ 940 ms │ 1962 ms │ 2052 ms │ 1075.72 ms │ 385.45 ms │ 3313 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.45 │ 0.65 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 186 kB │ 18.5 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 387 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 415 requests in 60.05s, 11.1 MB read 18 errors (18 timeouts) ---------------- CLIENTS: *** 15 *** Running 60s test @ http://192.168.2.131/ 15 connections 1 workers node:internal/event_target:1084 process.nextTick(() => { throw err; }); ^ TypeError: colorize is not a function at /Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:46:31 at Array.forEach () at printResult (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/printResult.js:43:43) at EventEmitter. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/progressTracker.js:79:28) at EventEmitter.emit (node:events:527:35) at _cb (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/init.js:76:13) at handleFinish (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:41:5) at Worker. (/Users/hoeken/.nvm/versions/node/v21.1.0/lib/node_modules/autocannon/lib/manager.js:78:13) at Worker.emit (node:events:515:28) at MessagePort. (node:internal/worker:263:53) Node.js v21.1.0 ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 15 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼─────────┤ │ Latency │ 44 ms │ 69 ms │ 158 ms │ 318 ms │ 82.99 ms │ 44.17 ms │ 1074 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴─────────┘ ┌───────────┬─────────┬────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 22 │ 23 │ 88 │ 98 │ 83.74 │ 16.45 │ 22 │ ├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 2.97 kB │ 3.1 kB │ 11.9 kB │ 13.2 kB │ 11.3 kB │ 2.22 kB │ 2.97 kB │ └───────────┴─────────┴────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5024 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.06s, 678 kB read 48 errors (48 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 15 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 665 ms │ 954 ms │ 1989 ms │ 2105 ms │ 1092.91 ms │ 396.34 ms │ 3476 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.35 │ 0.66 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 183 kB │ 18.8 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 381 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 444 requests in 60.06s, 11 MB read 48 errors (48 timeouts) ---------------- CLIENTS: *** 20 *** Running 60s test @ http://192.168.2.131/ 20 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 57 ms │ 97 ms │ 255 ms │ 322 ms │ 114.69 ms │ 55.79 ms │ 683 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 26 │ 48 │ 61 │ 67 │ 60.75 │ 5.8 │ 26 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 113 kB │ 208 kB │ 265 kB │ 291 kB │ 264 kB │ 25.2 kB │ 113 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3645 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.05s, 15.8 MB read 78 errors (78 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 20 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 44 ms │ 69 ms │ 142 ms │ 153 ms │ 78.14 ms │ 26.88 ms │ 256 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬───────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ │ Req/Sec │ 76 │ 78 │ 88 │ 100 │ 88.92 │ 5.59 │ 76 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ │ Bytes/Sec │ 10.3 kB │ 10.5 kB │ 11.9 kB │ 13.5 kB │ 12 kB │ 754 B │ 10.3 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴───────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5335 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 720 kB read 78 errors (78 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 20 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 705 ms │ 940 ms │ 1991 ms │ 2070 ms │ 1085.97 ms │ 389.59 ms │ 3357 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 5 │ 5 │ 6 │ 7 │ 6.39 │ 0.64 │ 5 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 144 kB │ 144 kB │ 173 kB │ 201 kB │ 184 kB │ 18.2 kB │ 144 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 383 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 481 requests in 60.06s, 11 MB read 78 errors (78 timeouts) ---------------- ================================================ FILE: benchmark/results/psychic-ssl-http-loadtest.log ================================================ CLIENTS: *** 1 *** Running 60s test @ https://192.168.2.131/ 1 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬─────────┬─────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼─────────┼─────────┼─────────┤ │ Latency │ 33 ms │ 40 ms │ 139 ms │ 157 ms │ 51.6 ms │ 58.1 ms │ 1757 ms │ └─────────┴───────┴───────┴────────┴────────┴─────────┴─────────┴─────────┘ ┌───────────┬─────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 5 │ 20 │ 24 │ 19.19 │ 4.05 │ 5 │ ├───────────┼─────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 21.7 kB │ 86.8 kB │ 104 kB │ 83.2 kB │ 17.5 kB │ 21.7 kB │ └───────────┴─────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1151 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.07s, 4.99 MB read ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 1 connections 1 workers ┌─────────┬───────┬───────┬───────┬────────┬──────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼───────┼────────┼──────────┼──────────┼─────────┤ │ Latency │ 21 ms │ 27 ms │ 89 ms │ 100 ms │ 32.17 ms │ 43.71 ms │ 1745 ms │ └─────────┴───────┴───────┴───────┴────────┴──────────┴──────────┴─────────┘ ┌───────────┬─────┬─────────┬─────────┬─────────┬────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ │ Req/Sec │ 0 │ 8 │ 32 │ 38 │ 30.57 │ 5.91 │ 8 │ ├───────────┼─────┼─────────┼─────────┼─────────┼────────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 1.07 kB │ 4.29 kB │ 5.09 kB │ 4.1 kB │ 791 B │ 1.07 kB │ └───────────┴─────┴─────────┴─────────┴─────────┴────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1834 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.07s, 246 kB read ---------------- Running 60s test @ https://192.168.2.131/alien.png 1 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼───────────┼─────────┤ │ Latency │ 216 ms │ 252 ms │ 400 ms │ 408 ms │ 281.55 ms │ 122.23 ms │ 1871 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴───────────┴─────────┘ ┌───────────┬─────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 1 │ 4 │ 5 │ 3.54 │ 0.83 │ 1 │ ├───────────┼─────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 28.8 kB │ 115 kB │ 144 kB │ 102 kB │ 23.8 kB │ 28.8 kB │ └───────────┴─────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 212 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 213 requests in 60.07s, 6.1 MB read ---------------- CLIENTS: *** 2 *** Running 60s test @ https://192.168.2.131/ 2 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼───────────┼─────────┤ │ Latency │ 37 ms │ 54 ms │ 149 ms │ 164 ms │ 64.14 ms │ 106.26 ms │ 3230 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 0 │ 0 │ 33 │ 36 │ 30.92 │ 7.43 │ 27 │ ├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 143 kB │ 156 kB │ 134 kB │ 32.2 kB │ 117 kB │ └───────────┴─────┴──────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1855 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.06s, 8.05 MB read ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 2 connections 1 workers ┌─────────┬───────┬───────┬───────┬────────┬──────────┬─────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼───────┼────────┼──────────┼─────────┼─────────┤ │ Latency │ 27 ms │ 38 ms │ 95 ms │ 106 ms │ 44.65 ms │ 87.8 ms │ 3211 ms │ └─────────┴───────┴───────┴───────┴────────┴──────────┴─────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 47 │ 52 │ 44.29 │ 10.57 │ 35 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 6.35 kB │ 7.02 kB │ 5.98 kB │ 1.43 kB │ 4.72 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2657 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.07s, 359 kB read ---------------- Running 60s test @ https://192.168.2.131/alien.png 2 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼───────────┼─────────┤ │ Latency │ 320 ms │ 456 ms │ 633 ms │ 701 ms │ 488.51 ms │ 296.92 ms │ 3820 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 4 │ 5 │ 4.09 │ 1.09 │ 3 │ ├───────────┼─────┼──────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 115 kB │ 144 kB │ 118 kB │ 31.2 kB │ 86.3 kB │ └───────────┴─────┴──────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 245 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 247 requests in 60.07s, 7.05 MB read ---------------- CLIENTS: *** 3 *** Running 60s test @ https://192.168.2.131/ 3 connections 1 workers ┌─────────┬─────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 1535 ms │ 6477 ms │ 14517 ms │ 14517 ms │ 6462.25 ms │ 2849.18 ms │ 14517 ms │ └─────────┴─────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.27 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 75 requests in 60.1s, 156 kB read 5 errors (0 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 3 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 1517 ms │ 1638 ms │ 3675 ms │ 3675 ms │ 1697.95 ms │ 343.44 ms │ 3675 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 73 requests in 60.11s, 4.72 kB read 1 errors (0 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 3 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬─────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼─────────────┼────────────┼──────────┤ │ Latency │ 386 ms │ 9117 ms │ 50313 ms │ 50313 ms │ 12942.77 ms │ 11833.9 ms │ 50313 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴─────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.57 │ 0.7 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.4 kB │ 16.3 kB │ 19.9 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 34 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 68 requests in 60.11s, 979 kB read 28 errors (0 timeouts) ---------------- CLIENTS: *** 4 *** Running 60s test @ https://192.168.2.131/ 4 connections 1 workers ┌─────────┬─────────┬─────────┬──────────┬──────────┬─────────────┬─────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼──────────┼──────────┼─────────────┼─────────────┼──────────┤ │ Latency │ 1530 ms │ 1686 ms │ 40733 ms │ 40733 ms │ 10336.06 ms │ 12608.67 ms │ 40733 ms │ └─────────┴─────────┴─────────┴──────────┴──────────┴─────────────┴─────────────┴──────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.27 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 76 requests in 60.15s, 156 kB read 11 errors (0 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 4 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼──────────┼─────────┤ │ Latency │ 1495 ms │ 1586 ms │ 4490 ms │ 4490 ms │ 1673.35 ms │ 485.3 ms │ 4490 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴──────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 74 requests in 60.15s, 4.72 kB read 3 errors (0 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 4 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬─────────────┬─────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼─────────────┼─────────────┼──────────┤ │ Latency │ 340 ms │ 3642 ms │ 49950 ms │ 49950 ms │ 13180.63 ms │ 14189.21 ms │ 49950 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴─────────────┴─────────────┴──────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 2 │ 0.59 │ 0.65 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 28.8 kB │ 57.6 kB │ 16.8 kB │ 18.4 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 72 requests in 60.15s, 1.01 MB read 30 errors (0 timeouts) ---------------- CLIENTS: *** 5 *** Running 60s test @ https://192.168.2.131/ 5 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 1449 ms │ 1597 ms │ 4810 ms │ 4810 ms │ 1691.72 ms │ 537.09 ms │ 4810 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.53 kB │ 2.14 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 75 requests in 60.13s, 152 kB read 11 errors (0 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 5 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 1510 ms │ 1584 ms │ 4971 ms │ 4971 ms │ 1685.03 ms │ 564.99 ms │ 4971 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 75 requests in 60.14s, 4.72 kB read 1 errors (0 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 5 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 409 ms │ 5565 ms │ 38132 ms │ 38132 ms │ 9430.76 ms │ 9263.66 ms │ 38132 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 2 │ 0.56 │ 0.62 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 57.6 kB │ 15.8 kB │ 17.8 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 33 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 69 requests in 60.14s, 950 kB read 29 errors (1 timeouts) ---------------- CLIENTS: *** 6 *** Running 60s test @ https://192.168.2.131/ 6 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 1007 ms │ 1603 ms │ 5373 ms │ 5373 ms │ 1696.69 ms │ 639.15 ms │ 5373 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.53 kB │ 2.14 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 76 requests in 60.14s, 152 kB read 15 errors (2 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 6 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 976 ms │ 1586 ms │ 6118 ms │ 6118 ms │ 1683.15 ms │ 773.87 ms │ 6118 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.59 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 78.8 B │ 66.6 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 35 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 76 requests in 60.14s, 4.72 kB read 4 errors (2 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 6 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 437 ms │ 3543 ms │ 18174 ms │ 18174 ms │ 4264.16 ms │ 3629.94 ms │ 18174 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.54 │ 0.6 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 15.3 kB │ 17 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 32 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 69 requests in 60.15s, 921 kB read 30 errors (5 timeouts) ---------------- CLIENTS: *** 7 *** Running 60s test @ https://192.168.2.131/ 7 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬───────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼───────────┼──────────┤ │ Latency │ 979 ms │ 1600 ms │ 37778 ms │ 37778 ms │ 2654.48 ms │ 5987.7 ms │ 37778 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴───────────┴──────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.27 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 80 requests in 60.1s, 156 kB read 17 errors (7 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 7 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 928 ms │ 1591 ms │ 7128 ms │ 7128 ms │ 1648.53 ms │ 955.92 ms │ 7128 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 79 requests in 60.09s, 4.86 kB read 13 errors (8 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 7 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬───────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼───────────┼────────────┼──────────┤ │ Latency │ 487 ms │ 1740 ms │ 24134 ms │ 24134 ms │ 3069.3 ms │ 4835.41 ms │ 24134 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴───────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.4 │ 0.53 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 11.5 kB │ 15 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 24 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 69 requests in 60.08s, 691 kB read 37 errors (21 timeouts) ---------------- CLIENTS: *** 8 *** Running 60s test @ https://192.168.2.131/ 8 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 892 ms │ 1585 ms │ 6973 ms │ 6973 ms │ 1613.52 ms │ 927.38 ms │ 6973 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.62 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.68 kB │ 2.11 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 37 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 89 requests in 60.09s, 161 kB read 25 errors (16 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 8 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 964 ms │ 1593 ms │ 7380 ms │ 7380 ms │ 1645.62 ms │ 998.66 ms │ 7380 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.53 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 70.6 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 86 requests in 60.1s, 4.86 kB read 14 errors (14 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 8 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 341 ms │ 4820 ms │ 26999 ms │ 26999 ms │ 7220.49 ms │ 6874.47 ms │ 26999 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.56 │ 0.67 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.4 kB │ 15.8 kB │ 19.3 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 33 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 84 requests in 60.08s, 950 kB read 41 errors (27 timeouts) ---------------- CLIENTS: *** 9 *** Running 60s test @ https://192.168.2.131/ 9 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ │ Latency │ 960 ms │ 1598 ms │ 7751 ms │ 7751 ms │ 1665.89 ms │ 1056.66 ms │ 7751 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.13 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 93 requests in 60.1s, 156 kB read 25 errors (21 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 9 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ │ Latency │ 933 ms │ 1589 ms │ 7703 ms │ 7703 ms │ 1649.14 ms │ 1051.77 ms │ 7703 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 93 requests in 60.09s, 4.86 kB read 21 errors (20 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 9 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼─────────┤ │ Latency │ 1049 ms │ 1783 ms │ 7382 ms │ 7382 ms │ 1758 ms │ 1027.58 ms │ 7382 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.57 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 28.8 kB │ 28.8 kB │ 16.3 kB │ 14.3 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 34 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 91 requests in 60.1s, 979 kB read 48 errors (29 timeouts) ---------------- CLIENTS: *** 10 *** Running 60s test @ https://192.168.2.131/ 10 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ │ Latency │ 979 ms │ 1597 ms │ 7548 ms │ 7548 ms │ 1660.23 ms │ 1024.01 ms │ 7548 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.13 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 100 requests in 60.09s, 156 kB read 29 errors (26 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 10 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 976 ms │ 1590 ms │ 7480 ms │ 7480 ms │ 1628.95 ms │ 1021.2 ms │ 7480 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 101 requests in 60.12s, 4.86 kB read 28 errors (28 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 10 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 585 ms │ 5882 ms │ 27503 ms │ 27503 ms │ 6338.82 ms │ 6773.89 ms │ 27503 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.54 │ 0.6 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 28.8 kB │ 15.3 kB │ 17 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 32 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 97 requests in 60.14s, 921 kB read 54 errors (38 timeouts) ---------------- CLIENTS: *** 15 *** Running 60s test @ https://192.168.2.131/ 15 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ │ Latency │ 957 ms │ 1601 ms │ 7609 ms │ 7609 ms │ 1654.73 ms │ 1034.04 ms │ 7609 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 4.34 kB │ 4.34 kB │ 2.6 kB │ 2.13 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 135 requests in 60.14s, 156 kB read 64 errors (56 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 15 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 966 ms │ 1576 ms │ 7369 ms │ 7369 ms │ 1612.68 ms │ 991.59 ms │ 7369 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬────────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.62 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼────────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 83.3 B │ 65.6 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴────────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 37 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 136 requests in 60.14s, 5 kB read 61 errors (60 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 15 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬──────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼──────────┼────────────┼──────────┤ │ Latency │ 300 ms │ 7281 ms │ 34953 ms │ 34953 ms │ 10153 ms │ 9647.72 ms │ 34953 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴──────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 3 │ 0.52 │ 0.68 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 86.4 kB │ 14.9 kB │ 19.3 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 31 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 132 requests in 60.11s, 892 kB read 84 errors (70 timeouts) ---------------- CLIENTS: *** 20 *** Running 60s test @ https://192.168.2.131/ 20 connections 1 workers ┌─────────┬────────┬─────────┬──────────┬──────────┬────────────┬────────────┬──────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼──────────┼──────────┼────────────┼────────────┼──────────┤ │ Latency │ 944 ms │ 1600 ms │ 36783 ms │ 36783 ms │ 2881.21 ms │ 6487.79 ms │ 36783 ms │ └─────────┴────────┴─────────┴──────────┴──────────┴────────────┴────────────┴──────────┘ ┌───────────┬─────┬──────┬─────┬─────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────┼─────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 0 │ 1 │ 0.49 │ 0.54 │ 1 │ ├───────────┼─────┼──────┼─────┼─────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 0 B │ 4.34 kB │ 2.1 kB │ 2.31 kB │ 4.34 kB │ └───────────┴─────┴──────┴─────┴─────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 29 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 165 requests in 60.1s, 126 kB read 96 errors (92 timeouts) ---------------- Running 60s test @ https://192.168.2.131/api?foo=bar 20 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 931 ms │ 1568 ms │ 7097 ms │ 7097 ms │ 1637.53 ms │ 950.49 ms │ 7097 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────┬──────┬───────┬───────┬──────┬────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.6 │ 0.49 │ 1 │ ├───────────┼─────┼──────┼───────┼───────┼──────┼────────┼───────┤ │ Bytes/Sec │ 0 B │ 0 B │ 135 B │ 135 B │ 81 B │ 66.1 B │ 135 B │ └───────────┴─────┴──────┴───────┴───────┴──────┴────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 36 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 170 requests in 60.12s, 4.86 kB read 87 errors (85 timeouts) ---------------- Running 60s test @ https://192.168.2.131/alien.png 20 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤ │ Latency │ 1116 ms │ 1707 ms │ 7481 ms │ 7481 ms │ 1754.83 ms │ 1032.66 ms │ 7481 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘ ┌───────────┬─────┬──────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 0 │ 0 │ 1 │ 1 │ 0.57 │ 0.5 │ 1 │ ├───────────┼─────┼──────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 0 B │ 0 B │ 28.8 kB │ 28.8 kB │ 16.3 kB │ 14.3 kB │ 28.8 kB │ └───────────┴─────┴──────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 34 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 168 requests in 60.09s, 979 kB read 114 errors (95 timeouts) ---------------- ================================================ FILE: benchmark/results/psychic-ssl-websocket-loadtest.log ================================================ CLIENTS: *** 1 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 1 Agent: none Completed requests: 2039 Total errors: 0 Total time: 60.002 s Mean latency: 27.8 ms Effective rps: 34 Percentage of requests served within a certain time 50% 24 ms 90% 34 ms 95% 62 ms 99% 97 ms 100% 109 ms (longest request) CLIENTS: *** 2 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 2 Agent: none Completed requests: 2969 Total errors: 0 Total time: 60.003 s Mean latency: 37.7 ms Effective rps: 49 Percentage of requests served within a certain time 50% 32 ms 90% 52 ms 95% 92 ms 99% 110 ms 100% 126 ms (longest request) CLIENTS: *** 3 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 3 Agent: none Completed requests: 2883 Total errors: 0 Total time: 60.003 s Mean latency: 38.2 ms Effective rps: 48 Percentage of requests served within a certain time 50% 32 ms 90% 52 ms 95% 86 ms 99% 109 ms 100% 1711 ms (longest request) CLIENTS: *** 4 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 4 Agent: none Completed requests: 2858 Total errors: 0 Total time: 60.003 s Mean latency: 37.9 ms Effective rps: 48 Percentage of requests served within a certain time 50% 32 ms 90% 49 ms 95% 76 ms 99% 104 ms 100% 1740 ms (longest request) CLIENTS: *** 5 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 5 Agent: none Completed requests: 2772 Total errors: 0 Total time: 60.003 s Mean latency: 38.6 ms Effective rps: 46 Percentage of requests served within a certain time 50% 32 ms 90% 49 ms 95% 79 ms 99% 106 ms 100% 1634 ms (longest request) CLIENTS: *** 6 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 6 Agent: none Completed requests: 2722 Total errors: 0 Total time: 60.003 s Mean latency: 38.7 ms Effective rps: 45 Percentage of requests served within a certain time 50% 32 ms 90% 49 ms 95% 72 ms 99% 102 ms 100% 1694 ms (longest request) CLIENTS: *** 7 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 7 Agent: none Completed requests: 2552 Total errors: 0 Total time: 60.003 s Mean latency: 40.7 ms Effective rps: 43 Percentage of requests served within a certain time 50% 32 ms 90% 52 ms 95% 86 ms 99% 112 ms 100% 1816 ms (longest request) CLIENTS: *** 8 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 8 Running on cores: 2 Agent: none Completed requests: 2507 Total errors: 0 Total time: 60.005 s Mean latency: 40.8 ms Effective rps: 42 Percentage of requests served within a certain time 50% 32 ms 90% 50 ms 95% 80 ms 99% 112 ms 100% 1646 ms (longest request) CLIENTS: *** 10 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 10 Running on cores: 2 Agent: none Completed requests: 2265 Total errors: 0 Total time: 60.008 s Mean latency: 43.7 ms Effective rps: 38 Percentage of requests served within a certain time 50% 33 ms 90% 52 ms 95% 79 ms 99% 114 ms 100% 1675 ms (longest request) CLIENTS: *** 16 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 16 Running on cores: 2 Agent: none Completed requests: 1795 Total errors: 0 Total time: 60.003 s Mean latency: 49.7 ms Effective rps: 30 Percentage of requests served within a certain time 50% 33 ms 90% 51 ms 95% 77 ms 99% 112 ms 100% 1741 ms (longest request) CLIENTS: *** 20 *** Target URL: wss://192.168.2.131/ws Max time (s): 60 Concurrent clients: 20 Running on cores: 2 Agent: none Completed requests: 1603 Total errors: 0 Total time: 60.004 s Mean latency: 54.1 ms Effective rps: 27 Percentage of requests served within a certain time 50% 33 ms 90% 60 ms 95% 94 ms 99% 133 ms 100% 1729 ms (longest request) ================================================ FILE: benchmark/results/psychic-v1.1-http-loadtest.log ================================================ CLIENTS: *** 1 *** Running 60s test @ http://192.168.2.131/ 1 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 25 ms │ 34 ms │ 137 ms │ 175 ms │ 44.75 ms │ 33.04 ms │ 363 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 13 │ 14 │ 23 │ 27 │ 22.05 │ 3.62 │ 13 │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 56.4 kB │ 60.7 kB │ 99.8 kB │ 117 kB │ 95.6 kB │ 15.7 kB │ 56.4 kB │ └───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1323 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.06s, 5.74 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 1 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 21 ms │ 29 ms │ 122 ms │ 173 ms │ 40.61 ms │ 38.19 ms │ 646 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬───────┬───────┬─────────┬─────────┬─────────┬─────────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼───────┼───────┼─────────┼─────────┼─────────┼─────────┼───────┤ │ Req/Sec │ 2 │ 6 │ 27 │ 34 │ 24.29 │ 7.62 │ 2 │ ├───────────┼───────┼───────┼─────────┼─────────┼─────────┼─────────┼───────┤ │ Bytes/Sec │ 268 B │ 804 B │ 3.62 kB │ 4.56 kB │ 3.25 kB │ 1.02 kB │ 268 B │ └───────────┴───────┴───────┴─────────┴─────────┴─────────┴─────────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1457 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 1k requests in 60.07s, 195 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 1 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 254 ms │ 291 ms │ 441 ms │ 487 ms │ 311.18 ms │ 52.89 ms │ 503 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 2 │ 3 │ 4 │ 3.2 │ 0.58 │ 2 │ ├───────────┼─────────┼─────────┼─────────┼────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 57.6 kB │ 86.4 kB │ 115 kB │ 92.1 kB │ 16.4 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴─────────┴────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 192 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 193 requests in 60.07s, 5.53 MB read ---------------- CLIENTS: *** 2 *** Running 60s test @ http://192.168.2.131/ 2 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 29 ms │ 46 ms │ 184 ms │ 213 ms │ 63.03 ms │ 43.98 ms │ 437 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 13 │ 13 │ 34 │ 43 │ 31.44 │ 7.44 │ 13 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 56.4 kB │ 56.4 kB │ 148 kB │ 187 kB │ 136 kB │ 32.2 kB │ 56.4 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 1886 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 2k requests in 60.07s, 8.18 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 2 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 27 ms │ 37 ms │ 107 ms │ 120 ms │ 44.09 ms │ 20.34 ms │ 188 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬───────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ │ Req/Sec │ 24 │ 25 │ 46 │ 52 │ 44.79 │ 6.1 │ 24 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼───────┼───────┼─────────┤ │ Bytes/Sec │ 3.22 kB │ 3.35 kB │ 6.17 kB │ 6.97 kB │ 6 kB │ 817 B │ 3.22 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴───────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2687 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.06s, 360 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 2 connections 1 workers ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 375 ms │ 552 ms │ 759 ms │ 848 ms │ 556.76 ms │ 95.75 ms │ 875 ms │ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 4 │ 4 │ 3.59 │ 0.53 │ 3 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 115 kB │ 103 kB │ 15.1 kB │ 86.3 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 215 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 217 requests in 60.09s, 6.19 MB read ---------------- CLIENTS: *** 3 *** Running 60s test @ http://192.168.2.131/ 3 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼─────────┼────────┤ │ Latency │ 36 ms │ 56 ms │ 167 ms │ 193 ms │ 67.22 ms │ 33.6 ms │ 343 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴─────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 32 │ 34 │ 45 │ 52 │ 44.25 │ 4.31 │ 32 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 139 kB │ 148 kB │ 195 kB │ 226 kB │ 192 kB │ 18.7 kB │ 139 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 2655 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.08s, 11.5 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 3 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼─────────┼────────┤ │ Latency │ 30 ms │ 45 ms │ 114 ms │ 128 ms │ 51.89 ms │ 22.2 ms │ 294 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴─────────┴────────┘ ┌───────────┬─────────┬────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 37 │ 44 │ 58 │ 67 │ 57.24 │ 6.16 │ 37 │ ├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 4.96 kB │ 5.9 kB │ 7.78 kB │ 8.98 kB │ 7.67 kB │ 825 B │ 4.96 kB │ └───────────┴─────────┴────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3434 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.1s, 460 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 3 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ │ Latency │ 412 ms │ 786 ms │ 1293 ms │ 1397 ms │ 829.09 ms │ 295.53 ms │ 1412 ms │ └─────────┴────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 3 │ 4 │ 4 │ 3.59 │ 0.53 │ 2 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 115 kB │ 103 kB │ 15.1 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 215 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 218 requests in 60.11s, 6.19 MB read ---------------- CLIENTS: *** 4 *** Running 60s test @ http://192.168.2.131/ 4 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 41 ms │ 65 ms │ 189 ms │ 221 ms │ 79.49 ms │ 39.69 ms │ 385 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 41 │ 41 │ 51 │ 56 │ 50 │ 4.07 │ 41 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 178 kB │ 178 kB │ 221 kB │ 243 kB │ 217 kB │ 17.6 kB │ 178 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3000 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.1s, 13 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 4 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 35 ms │ 51 ms │ 128 ms │ 154 ms │ 58.79 ms │ 24.56 ms │ 347 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 38 │ 50 │ 69 │ 77 │ 67.49 │ 7.34 │ 38 │ ├───────────┼─────────┼────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 5.09 kB │ 6.7 kB │ 9.25 kB │ 10.3 kB │ 9.04 kB │ 983 B │ 5.09 kB │ └───────────┴─────────┴────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4049 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.11s, 543 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 4 connections 1 workers ┌─────────┬────────┬────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 630 ms │ 885 ms │ 1840 ms │ 1877 ms │ 1082.82 ms │ 406.47 ms │ 2050 ms │ └─────────┴────────┴────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 4 │ 5 │ 3.65 │ 0.55 │ 3 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 144 kB │ 105 kB │ 15.6 kB │ 86.3 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 219 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 223 requests in 60.08s, 6.3 MB read ---------------- CLIENTS: *** 5 *** Running 60s test @ http://192.168.2.131/ 5 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 47 ms │ 77 ms │ 213 ms │ 242 ms │ 91.79 ms │ 44.06 ms │ 398 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 41 │ 42 │ 55 │ 62 │ 54.12 │ 4.67 │ 41 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 178 kB │ 182 kB │ 239 kB │ 269 kB │ 235 kB │ 20.2 kB │ 178 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3247 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 3k requests in 60.08s, 14.1 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 5 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 39 ms │ 60 ms │ 126 ms │ 141 ms │ 65.76 ms │ 21.03 ms │ 172 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬─────────┬─────────┬─────────┬─────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼─────────┼─────────┼─────────┼─────────┼───────┼────────┤ │ Req/Sec │ 53 │ 58 │ 76 │ 86 │ 75.34 │ 7.04 │ 53 │ ├───────────┼────────┼─────────┼─────────┼─────────┼─────────┼───────┼────────┤ │ Bytes/Sec │ 7.1 kB │ 7.78 kB │ 10.2 kB │ 11.5 kB │ 10.1 kB │ 943 B │ 7.1 kB │ └───────────┴────────┴─────────┴─────────┴─────────┴─────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4520 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.08s, 606 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 5 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 832 ms │ 1125 ms │ 2405 ms │ 2530 ms │ 1344.04 ms │ 504.13 ms │ 3485 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.69 │ 0.57 │ 2 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 106 kB │ 16.2 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 221 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 226 requests in 60.08s, 6.36 MB read ---------------- CLIENTS: *** 6 *** Running 60s test @ http://192.168.2.131/ 6 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 51 ms │ 84 ms │ 214 ms │ 249 ms │ 98.05 ms │ 44.48 ms │ 553 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 40 │ 50 │ 62 │ 66 │ 60.87 │ 4.59 │ 40 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 174 kB │ 217 kB │ 269 kB │ 286 kB │ 264 kB │ 19.9 kB │ 174 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3652 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.09s, 15.8 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 6 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 45 ms │ 68 ms │ 144 ms │ 159 ms │ 75.43 ms │ 24.46 ms │ 223 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 53 │ 63 │ 80 │ 91 │ 78.95 │ 7.74 │ 53 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 7.16 kB │ 8.51 kB │ 10.8 kB │ 12.3 kB │ 10.7 kB │ 1.04 kB │ 7.16 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4737 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.08s, 639 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 6 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 893 ms │ 1385 ms │ 3045 ms │ 3336 ms │ 1630.97 ms │ 621.58 ms │ 4350 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.62 │ 0.58 │ 2 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 104 kB │ 16.7 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 217 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 223 requests in 60.1s, 6.25 MB read ---------------- CLIENTS: *** 7 *** Running 60s test @ http://192.168.2.131/ 7 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 59 ms │ 96 ms │ 232 ms │ 260 ms │ 109.85 ms │ 47.04 ms │ 531 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬───────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Req/Sec │ 44 │ 53 │ 65 │ 72 │ 63.39 │ 5.76 │ 44 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼───────┼────────┤ │ Bytes/Sec │ 191 kB │ 230 kB │ 282 kB │ 313 kB │ 275 kB │ 25 kB │ 191 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴───────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3803 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.08s, 16.5 MB read ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 7 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 48 ms │ 75 ms │ 139 ms │ 152 ms │ 80.37 ms │ 22.69 ms │ 291 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬───────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 52 │ 67 │ 88 │ 96 │ 86.59 │ 7.62 │ 52 │ ├───────────┼─────────┼─────────┼─────────┼───────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 7.02 kB │ 9.05 kB │ 11.9 kB │ 13 kB │ 11.7 kB │ 1.03 kB │ 7.02 kB │ └───────────┴─────────┴─────────┴─────────┴───────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5195 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.11s, 701 kB read ---------------- Running 60s test @ http://192.168.2.131/alien.png 7 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 845 ms │ 1642 ms │ 3537 ms │ 3656 ms │ 1887.17 ms │ 728.73 ms │ 6130 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 4 │ 4 │ 3.64 │ 0.52 │ 3 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 115 kB │ 105 kB │ 14.8 kB │ 86.3 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 218 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 225 requests in 60.1s, 6.27 MB read ---------------- CLIENTS: *** 8 *** Running 60s test @ http://192.168.2.131/ 8 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼────────┤ │ Latency │ 58 ms │ 96 ms │ 225 ms │ 256 ms │ 108.89 ms │ 45.19 ms │ 571 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴────────┘ ┌───────────┬────────┬────────┬────────┬────────┬────────┬─────────┬────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Req/Sec │ 51 │ 54 │ 65 │ 70 │ 63.92 │ 4.63 │ 51 │ ├───────────┼────────┼────────┼────────┼────────┼────────┼─────────┼────────┤ │ Bytes/Sec │ 221 kB │ 234 kB │ 282 kB │ 304 kB │ 277 kB │ 20.1 kB │ 221 kB │ └───────────┴────────┴────────┴────────┴────────┴────────┴─────────┴────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3835 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.07s, 16.6 MB read 6 errors (6 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 8 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬─────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼─────────┼────────┤ │ Latency │ 50 ms │ 76 ms │ 148 ms │ 166 ms │ 83.61 ms │ 26.5 ms │ 282 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴─────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 63 │ 66 │ 82 │ 98 │ 83.15 │ 8.78 │ 63 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 8.51 kB │ 8.91 kB │ 11.1 kB │ 13.2 kB │ 11.2 kB │ 1.18 kB │ 8.51 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4989 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.08s, 674 kB read 6 errors (6 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 8 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬───────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼───────────┼───────────┼─────────┤ │ Latency │ 806 ms │ 1614 ms │ 3526 ms │ 3769 ms │ 1851.3 ms │ 712.64 ms │ 5877 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴───────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 4 │ 5 │ 3.72 │ 0.52 │ 3 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 144 kB │ 107 kB │ 14.9 kB │ 86.3 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 223 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 237 requests in 60.09s, 6.42 MB read 6 errors (6 timeouts) ---------------- CLIENTS: *** 9 *** Running 60s test @ http://192.168.2.131/ 9 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬─────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼─────────┼─────────┤ │ Latency │ 60 ms │ 98 ms │ 232 ms │ 268 ms │ 111.69 ms │ 57.8 ms │ 1065 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴─────────┴─────────┘ ┌───────────┬───────┬────────┬────────┬────────┬────────┬───────┬───────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼───────┼────────┼────────┼────────┼────────┼───────┼───────┤ │ Req/Sec │ 3 │ 54 │ 63 │ 72 │ 62.37 │ 8.98 │ 3 │ ├───────────┼───────┼────────┼────────┼────────┼────────┼───────┼───────┤ │ Bytes/Sec │ 13 kB │ 234 kB │ 273 kB │ 313 kB │ 271 kB │ 39 kB │ 13 kB │ └───────────┴───────┴────────┴────────┴────────┴────────┴───────┴───────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3742 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.07s, 16.2 MB read 12 errors (12 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 9 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 50 ms │ 77 ms │ 148 ms │ 160 ms │ 83.55 ms │ 24.72 ms │ 275 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬───────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Req/Sec │ 63 │ 67 │ 85 │ 95 │ 83.27 │ 7.2 │ 63 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼───────┼─────────┤ │ Bytes/Sec │ 8.51 kB │ 9.05 kB │ 11.5 kB │ 12.8 kB │ 11.2 kB │ 972 B │ 8.51 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴───────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4996 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.08s, 674 kB read 12 errors (12 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 9 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 857 ms │ 1631 ms │ 3541 ms │ 3814 ms │ 1883.14 ms │ 730.83 ms │ 6021 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 3 │ 3 │ 4 │ 4 │ 3.67 │ 0.48 │ 3 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 86.4 kB │ 86.4 kB │ 115 kB │ 115 kB │ 106 kB │ 13.5 kB │ 86.3 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 220 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 241 requests in 60.08s, 6.33 MB read 12 errors (12 timeouts) ---------------- CLIENTS: *** 10 *** Running 60s test @ http://192.168.2.131/ 10 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼─────────┤ │ Latency │ 59 ms │ 95 ms │ 235 ms │ 276 ms │ 111.77 ms │ 57.35 ms │ 1010 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴─────────┘ ┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 17 │ 47 │ 64 │ 70 │ 62.32 │ 8.14 │ 17 │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 73.8 kB │ 204 kB │ 278 kB │ 304 kB │ 270 kB │ 35.3 kB │ 73.7 kB │ └───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3739 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.08s, 16.2 MB read 18 errors (18 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 10 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 49 ms │ 82 ms │ 179 ms │ 209 ms │ 91.77 ms │ 33.92 ms │ 312 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 46 │ 48 │ 76 │ 92 │ 75.85 │ 11.6 │ 46 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 6.21 kB │ 6.48 kB │ 10.3 kB │ 12.4 kB │ 10.2 kB │ 1.57 kB │ 6.21 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4551 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 614 kB read 18 errors (18 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 10 connections 1 workers ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 1062 ms │ 1673 ms │ 3588 ms │ 3643 ms │ 1903.02 ms │ 693.11 ms │ 5495 ms │ └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 2 │ 4 │ 5 │ 3.6 │ 0.64 │ 2 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 57.6 kB │ 115 kB │ 144 kB │ 104 kB │ 18.3 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 216 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 244 requests in 60.08s, 6.22 MB read 18 errors (18 timeouts) ---------------- CLIENTS: *** 15 *** Running 60s test @ http://192.168.2.131/ 15 connections 1 workers ┌─────────┬───────┬────────┬────────┬────────┬───────────┬─────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼────────┼────────┼────────┼───────────┼─────────┼─────────┤ │ Latency │ 62 ms │ 103 ms │ 252 ms │ 297 ms │ 119.53 ms │ 58.1 ms │ 1050 ms │ └─────────┴───────┴────────┴────────┴────────┴───────────┴─────────┴─────────┘ ┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 22 │ 41 │ 60 │ 70 │ 58.27 │ 8.6 │ 22 │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 95.5 kB │ 178 kB │ 260 kB │ 304 kB │ 253 kB │ 37.3 kB │ 95.4 kB │ └───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3496 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.06s, 15.2 MB read 48 errors (48 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 15 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬──────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼──────────┼──────────┼────────┤ │ Latency │ 49 ms │ 75 ms │ 151 ms │ 164 ms │ 82.32 ms │ 25.95 ms │ 313 ms │ └─────────┴───────┴───────┴────────┴────────┴──────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 61 │ 66 │ 85 │ 99 │ 84.52 │ 8.42 │ 61 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 8.24 kB │ 8.91 kB │ 11.5 kB │ 13.4 kB │ 11.4 kB │ 1.14 kB │ 8.23 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 5071 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 685 kB read 48 errors (48 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 15 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 801 ms │ 1709 ms │ 3676 ms │ 3762 ms │ 1929.08 ms │ 744.64 ms │ 5846 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.57 │ 0.59 │ 2 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 103 kB │ 16.9 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 214 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 277 requests in 60.09s, 6.16 MB read 48 errors (48 timeouts) ---------------- CLIENTS: *** 20 *** Running 60s test @ http://192.168.2.131/ 20 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬───────────┬──────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼───────────┼──────────┼─────────┤ │ Latency │ 60 ms │ 97 ms │ 232 ms │ 267 ms │ 112.02 ms │ 53.32 ms │ 1054 ms │ └─────────┴───────┴───────┴────────┴────────┴───────────┴──────────┴─────────┘ ┌───────────┬─────────┬────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 21 │ 52 │ 63 │ 69 │ 62.12 │ 6.59 │ 21 │ ├───────────┼─────────┼────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 91.1 kB │ 226 kB │ 273 kB │ 300 kB │ 269 kB │ 28.6 kB │ 91.1 kB │ └───────────┴─────────┴────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 3727 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 4k requests in 60.1s, 16.2 MB read 78 errors (78 timeouts) ---------------- Running 60s test @ http://192.168.2.131/api?foo=bar 20 connections 1 workers ┌─────────┬───────┬───────┬────────┬────────┬─────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼───────┼───────┼────────┼────────┼─────────┼──────────┼────────┤ │ Latency │ 49 ms │ 78 ms │ 164 ms │ 178 ms │ 87.8 ms │ 35.83 ms │ 658 ms │ └─────────┴───────┴───────┴────────┴────────┴─────────┴──────────┴────────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Req/Sec │ 35 │ 55 │ 81 │ 95 │ 79.27 │ 10.5 │ 35 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤ │ Bytes/Sec │ 4.73 kB │ 7.43 kB │ 10.9 kB │ 12.8 kB │ 10.7 kB │ 1.42 kB │ 4.72 kB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 4756 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 5k requests in 60.07s, 642 kB read 78 errors (78 timeouts) ---------------- Running 60s test @ http://192.168.2.131/alien.png 20 connections 1 workers ┌─────────┬────────┬─────────┬─────────┬─────────┬────────────┬───────────┬─────────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼────────┼─────────┼─────────┼─────────┼────────────┼───────────┼─────────┤ │ Latency │ 806 ms │ 1613 ms │ 3348 ms │ 4009 ms │ 1854.07 ms │ 733.32 ms │ 6606 ms │ └─────────┴────────┴─────────┴─────────┴─────────┴────────────┴───────────┴─────────┘ ┌───────────┬─────────┬─────────┬────────┬────────┬────────┬─────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Req/Sec │ 2 │ 3 │ 4 │ 5 │ 3.7 │ 0.62 │ 2 │ ├───────────┼─────────┼─────────┼────────┼────────┼────────┼─────────┼─────────┤ │ Bytes/Sec │ 57.6 kB │ 86.4 kB │ 115 kB │ 144 kB │ 106 kB │ 17.7 kB │ 57.6 kB │ └───────────┴─────────┴─────────┴────────┴────────┴────────┴─────────┴─────────┘ ┌──────┬───────┐ │ Code │ Count │ ├──────┼───────┤ │ 200 │ 222 │ └──────┴───────┘ Req/Bytes counts sampled once per second. # of samples: 60 320 requests in 60.05s, 6.39 MB read 78 errors (78 timeouts) ---------------- ================================================ FILE: benchmark/results/psychic-v1.1-websocket-loadtest.log ================================================ CLIENTS: *** 1 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 1 Agent: none Completed requests: 1972 Total errors: 0 Total time: 60.003 s Mean latency: 29.8 ms Effective rps: 33 Percentage of requests served within a certain time 50% 25 ms 90% 40 ms 95% 66 ms 99% 96 ms 100% 147 ms (longest request) CLIENTS: *** 2 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 2 Agent: none Completed requests: 3144 Total errors: 0 Total time: 60.003 s Mean latency: 37.6 ms Effective rps: 52 Percentage of requests served within a certain time 50% 32 ms 90% 58 ms 95% 82 ms 99% 114 ms 100% 160 ms (longest request) CLIENTS: *** 3 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 3 Agent: none Completed requests: 4113 Total errors: 0 Total time: 60.005 s Mean latency: 43.2 ms Effective rps: 69 Percentage of requests served within a certain time 50% 38 ms 90% 63 ms 95% 88 ms 99% 119 ms 100% 339 ms (longest request) CLIENTS: *** 4 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 4 Agent: none Completed requests: 4902 Total errors: 0 Total time: 60.004 s Mean latency: 48.3 ms Effective rps: 82 Percentage of requests served within a certain time 50% 42 ms 90% 74 ms 95% 97 ms 99% 125 ms 100% 217 ms (longest request) CLIENTS: *** 5 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 5 Agent: none Completed requests: 5522 Total errors: 0 Total time: 60.003 s Mean latency: 53.7 ms Effective rps: 92 Percentage of requests served within a certain time 50% 48 ms 90% 81 ms 95% 102 ms 99% 122 ms 100% 324 ms (longest request) CLIENTS: *** 6 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 6 Agent: none Completed requests: 5808 Total errors: 0 Total time: 60.004 s Mean latency: 61.4 ms Effective rps: 97 Percentage of requests served within a certain time 50% 54 ms 90% 94 ms 95% 117 ms 99% 142 ms 100% 348 ms (longest request) CLIENTS: *** 7 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 7 Agent: none Completed requests: 6478 Total errors: 0 Total time: 60.006 s Mean latency: 64.1 ms Effective rps: 108 Percentage of requests served within a certain time 50% 59 ms 90% 94 ms 95% 110 ms 99% 137 ms 100% 195 ms (longest request) CLIENTS: *** 8 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 8 Running on cores: 2 Agent: none Completed requests: 6124 Total errors: 0 Total time: 60.004 s Mean latency: 67.8 ms Effective rps: 102 Percentage of requests served within a certain time 50% 59 ms 90% 107 ms 95% 131 ms 99% 173 ms 100% 260 ms (longest request) CLIENTS: *** 10 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 10 Running on cores: 2 Agent: none Completed requests: 5640 Total errors: 0 Total time: 60.004 s Mean latency: 73.7 ms Effective rps: 94 Percentage of requests served within a certain time 50% 61 ms 90% 120 ms 95% 140 ms 99% 240 ms 100% 780 ms (longest request) CLIENTS: *** 16 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 16 Running on cores: 2 Agent: none Completed requests: 5809 Total errors: 0 Total time: 60.006 s Mean latency: 71.6 ms Effective rps: 97 Percentage of requests served within a certain time 50% 64 ms 90% 111 ms 95% 130 ms 99% 162 ms 100% 226 ms (longest request) CLIENTS: *** 20 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 20 Running on cores: 2 Agent: none Completed requests: 5590 Total errors: 0 Total time: 60.003 s Mean latency: 74.4 ms Effective rps: 93 Percentage of requests served within a certain time 50% 61 ms 90% 122 ms 95% 151 ms 99% 247 ms 100% 513 ms (longest request) ================================================ FILE: benchmark/results/psychic-websocket-loadtest.log ================================================ CLIENTS: *** 1 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 1 Agent: none Completed requests: 2304 Total errors: 0 Total time: 60.002 s Mean latency: 25.5 ms Effective rps: 38 Percentage of requests served within a certain time 50% 22 ms 90% 32 ms 95% 58 ms 99% 92 ms 100% 105 ms (longest request) CLIENTS: *** 2 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 2 Agent: none Completed requests: 3647 Total errors: 0 Total time: 60.002 s Mean latency: 32.3 ms Effective rps: 61 Percentage of requests served within a certain time 50% 28 ms 90% 43 ms 95% 67 ms 99% 93 ms 100% 135 ms (longest request) CLIENTS: *** 3 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 3 Agent: none Completed requests: 4629 Total errors: 0 Total time: 60.004 s Mean latency: 38.3 ms Effective rps: 77 Percentage of requests served within a certain time 50% 34 ms 90% 51 ms 95% 79 ms 99% 110 ms 100% 152 ms (longest request) CLIENTS: *** 4 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 4 Agent: none Completed requests: 5290 Total errors: 0 Total time: 60.003 s Mean latency: 44.7 ms Effective rps: 88 Percentage of requests served within a certain time 50% 40 ms 90% 67 ms 95% 92 ms 99% 115 ms 100% 159 ms (longest request) CLIENTS: *** 5 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 5 Agent: none Completed requests: 5935 Total errors: 0 Total time: 60.002 s Mean latency: 50 ms Effective rps: 99 Percentage of requests served within a certain time 50% 45 ms 90% 74 ms 95% 97 ms 99% 123 ms 100% 172 ms (longest request) CLIENTS: *** 6 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 6 Agent: none Completed requests: 6533 Total errors: 0 Total time: 60.003 s Mean latency: 54.5 ms Effective rps: 109 Percentage of requests served within a certain time 50% 49 ms 90% 78 ms 95% 101 ms 99% 129 ms 100% 170 ms (longest request) CLIENTS: *** 7 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 7 Agent: none Completed requests: 7086 Total errors: 0 Total time: 60.004 s Mean latency: 58.6 ms Effective rps: 118 Percentage of requests served within a certain time 50% 54 ms 90% 85 ms 95% 107 ms 99% 130 ms 100% 184 ms (longest request) CLIENTS: *** 8 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 8 Running on cores: 2 Agent: none Completed requests: 6994 Total errors: 0 Total time: 60.004 s Mean latency: 59.3 ms Effective rps: 117 Percentage of requests served within a certain time 50% 54 ms 90% 88 ms 95% 109 ms 99% 134 ms 100% 176 ms (longest request) CLIENTS: *** 10 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 10 Running on cores: 2 Agent: none Completed requests: 7197 Total errors: 0 Total time: 60.004 s Mean latency: 57.7 ms Effective rps: 120 Percentage of requests served within a certain time 50% 53 ms 90% 83 ms 95% 98 ms 99% 123 ms 100% 176 ms (longest request) CLIENTS: *** 16 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 16 Running on cores: 2 Agent: none Completed requests: 7173 Total errors: 0 Total time: 60.002 s Mean latency: 57.9 ms Effective rps: 120 Percentage of requests served within a certain time 50% 53 ms 90% 83 ms 95% 100 ms 99% 123 ms 100% 156 ms (longest request) CLIENTS: *** 20 *** Target URL: ws://192.168.2.131/ws Max time (s): 60 Concurrent clients: 20 Running on cores: 2 Agent: none Completed requests: 6883 Total errors: 0 Total time: 60.002 s Mean latency: 60.4 ms Effective rps: 115 Percentage of requests served within a certain time 50% 55 ms 90% 92 ms 95% 111 ms 99% 138 ms 100% 175 ms (longest request) ================================================ FILE: benchmark/websocket-client-test.js ================================================ #!/usr/bin/env node //stress test the client open/close for websockets const WebSocket = require('ws'); const uri = 'ws://psychic.local/ws'; async function websocketClient() { console.log(`Starting test`); for (let i = 0; i < 1000000; i++) { const ws = new WebSocket(uri); if (i % 100 == 0) console.log(`Count: ${i}`); ws.on('open', () => { //console.log(`Connected`); }); ws.on('message', (message) => { //console.log(`Message: ${message}`); ws.close(); }); ws.on('error', (error) => { console.error(`Error: ${error.message}`); }); await new Promise((resolve) => { ws.on('close', () => { resolve(); }); }); } } websocketClient(); ================================================ FILE: component.mk ================================================ COMPONENT_ADD_INCLUDEDIRS := src COMPONENT_SRCDIRS := src CXXFLAGS += -fno-rtti ================================================ FILE: examples/esp-idf/.gitignore ================================================ build/ sdkconfig sdkconfig.old components/ managed_components/ dependencies.lock main/_secret.h ================================================ FILE: examples/esp-idf/CMakeLists.txt ================================================ # The following lines of boilerplate have to be in your project's # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.5) include($ENV{IDF_PATH}/tools/cmake/project.cmake) if(DEFINED ENV{HTTP_PATH}) set(HTTP_PATH $ENV{HTTP_PATH}) else() #these both work set(HTTP_PATH "../../") #set(HTTP_PATH ${CMAKE_CURRENT_LIST_DIR}/../../../) #this does not work for me... #set(HTTP_PATH ${CMAKE_CURRENT_LIST_DIR}/../../../PsychicHttp) endif(DEFINED ENV{HTTP_PATH}) set(EXTRA_COMPONENT_DIRS ${HTTP_PATH}) if(${IDF_VERSION_MAJOR} LESS "5") include_directories("managed_components/joltwallet__littlefs/include") endif() project(PsychicHttp_IDF) ================================================ FILE: examples/esp-idf/README.md ================================================ # PsychicHttp - ESP IDF Example * Download and install [ESP IDF 4.4.7](https://github.com/espressif/esp-idf/releases/tag/v4.4.7) (or later version) * Clone the project: ```git clone --recursive git@github.com:hoeken/PsychicHttp.git``` * Run build command: ```cd PsychicHttp/examples/esp-idf``` and then ```idf.py build``` * Flash the LittleFS filesystem: ```esptool.py write_flash --flash_mode dio --flash_freq 40m --flash_size 4MB 0x317000 build/littlefs.bin``` * Flash the app firmware: ```idf.py flash monitor``` and visit the IP address shown in the console with a web browser. * Learn more about [Arduino as ESP-IDF Component](https://docs.espressif.com/projects/arduino-esp32/en/latest/esp-idf_component.html) ================================================ FILE: examples/esp-idf/data/custom.txt ================================================ Custom text file. ================================================ FILE: examples/esp-idf/data/server.crt ================================================ -----BEGIN CERTIFICATE----- MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4 sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/ ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3 emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo= -----END CERTIFICATE----- ================================================ FILE: examples/esp-idf/data/server.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al 3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg 0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9 Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX 49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc +3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6 pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D 0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin 7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1 noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8 4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/ nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3 q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2 lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr v/t+MeGJP/0Zw8v/X2CFll96 -----END PRIVATE KEY----- ================================================ FILE: examples/esp-idf/data/www/index.html ================================================ PsychicHTTP Demo

Basic Request Examples

Static Serving

Text File

Simple POST Form



Basic File Upload

Multipart POST Form




Websocket Demo

EventSource Demo

================================================ FILE: examples/esp-idf/data/www/text.txt ================================================ Test File. ================================================ FILE: examples/esp-idf/data/www-ap/index.html ================================================ PsychicHTTP SoftAP Demo

SoftAP Demo

You are connected to the ESP in SoftAP mode.

================================================ FILE: examples/esp-idf/include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: examples/esp-idf/lib/README ================================================ This directory is intended for project specific (private) libraries. PlatformIO will compile them to static libraries and link into executable file. The source code of each library should be placed in a an own separate directory ("lib/your_library_name/[here are source files]"). For example, see a structure of the following two libraries `Foo` and `Bar`: |--lib | | | |--Bar | | |--docs | | |--examples | | |--src | | |- Bar.c | | |- Bar.h | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html | | | |--Foo | | |- Foo.c | | |- Foo.h | | | |- README --> THIS FILE | |- platformio.ini |--src |- main.c and a contents of `src/main.c`: ``` #include #include int main (void) { ... } ``` PlatformIO Library Dependency Finder will find automatically dependent libraries scanning project source files. More information about PlatformIO Library Dependency Finder - https://docs.platformio.org/page/librarymanager/ldf.html ================================================ FILE: examples/esp-idf/main/CMakeLists.txt ================================================ # This file was automatically generated for projects # without default 'CMakeLists.txt' file. idf_component_register( SRCS "main.cpp" INCLUDE_DIRS ".") littlefs_create_partition_image(littlefs ${project_dir}/data FLASH_IN_PROJECT) ================================================ FILE: examples/esp-idf/main/idf_component.yml ================================================ dependencies: joltwallet/littlefs: ^1.16.4 ================================================ FILE: examples/esp-idf/main/main.cpp ================================================ /* PsychicHTTP Server Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ /********************************************************************************************** * Note: this demo relies on various files to be uploaded on the LittleFS partition * Follow instructions here: https://randomnerdtutorials.com/esp32-littlefs-arduino-ide/ **********************************************************************************************/ #include "_secret.h" #include #include #include #include #include #include #include // set CONFIG_ESP_HTTPS_SERVER_ENABLE=y in menuconfig to enable SSL #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #include #endif #ifndef WIFI_SSID #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #endif // Enter your WIFI credentials in secret.h const char* ssid = WIFI_SSID; const char* password = WIFI_PASS; // Set your SoftAP credentials const char* softap_ssid = "PsychicHttp"; const char* softap_password = ""; IPAddress softap_ip(10, 0, 0, 1); // credentials for the /auth-basic and /auth-digest examples const char* app_user = "admin"; const char* app_pass = "admin"; const char* app_name = "Your App"; LoggingMiddleware loggingMiddleware; AuthenticationMiddleware basicAuth; AuthenticationMiddleware digestAuth; // hostname for mdns (psychic.local) const char* local_hostname = "psychic"; // set CONFIG_ESP_HTTPS_SERVER_ENABLE=y in menuconfig to enable ssl #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE bool app_enable_ssl = true; String server_cert; String server_key; #endif // our main server object #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE PsychicHttpsServer server; #else PsychicHttpServer server; #endif PsychicWebSocketHandler websocketHandler; PsychicEventSource eventSource; CorsMiddleware corsMiddleware; // NTP server stuff const char* ntpServer1 = "pool.ntp.org"; const char* ntpServer2 = "time.nist.gov"; const long gmtOffset_sec = 0; const int daylightOffset_sec = 0; struct tm timeinfo; // Callback function (gets called when time adjusts via NTP) void timeAvailable(struct timeval* t) { if (!getLocalTime(&timeinfo)) { Serial.println("Failed to obtain time"); return; } Serial.print("NTP update: "); char buffer[40]; strftime(buffer, 40, "%FT%T%z", &timeinfo); Serial.println(buffer); } bool connectToWifi() { // WiFi.mode(WIFI_AP); // ap only mode // WiFi.mode(WIFI_STA); // client only mode WiFi.mode(WIFI_AP_STA); // ap and client // Configure SoftAP // dual client and AP mode WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 WiFi.softAP(softap_ssid, softap_password); IPAddress myIP = WiFi.softAPIP(); Serial.print("SoftAP IP Address: "); Serial.println(myIP); Serial.println(); Serial.print("[WiFi] Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); // Auto reconnect is set true as default // To set auto connect off, use the following function // WiFi.setAutoReconnect(false); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } void setup() { esp_log_level_set(PH_TAG, ESP_LOG_DEBUG); esp_log_level_set("httpd_uri", ESP_LOG_DEBUG); Serial.begin(115200); delay(10); Serial.printf("ESP-IDF Version: %s\n", esp_get_idf_version()); #ifdef ESP_ARDUINO_VERSION_STR Serial.printf("Arduino Version: %s\n", ESP_ARDUINO_VERSION_STR); #else Serial.printf("Arduino Version: %d.%d.%d\n", ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH); #endif Serial.printf("PsychicHttp Version: %s\n", PSYCHIC_VERSION_STR); // We start by connecting to a WiFi network // To debug, please enable Core Debug Level to Verbose if (connectToWifi()) { // Setup our NTP to get the current time. sntp_set_time_sync_notification_cb(timeAvailable); sntp_servermode_dhcp(1); // (optional) configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2); // set up our esp32 to listen on the psychic.local domain if (MDNS.begin(local_hostname)) MDNS.addService("http", "tcp", 80); else Serial.println("Error starting mDNS"); if (!LittleFS.begin(false, "/spiffs", 5, "littlefs")) { Serial.println("LittleFS Mount Failed. Upload filesystem image first."); return; } #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE // look up our keys? if (app_enable_ssl) { File fp = LittleFS.open("/server.crt"); if (fp) { server_cert = fp.readString(); // Serial.println("Server Cert:"); // Serial.println(server_cert); } else { Serial.println("server.pem not found, SSL not available"); app_enable_ssl = false; } fp.close(); File fp2 = LittleFS.open("/server.key"); if (fp2) { server_key = fp2.readString(); // Serial.println("Server Key:"); // Serial.println(server_key); } else { Serial.println("server.key not found, SSL not available"); app_enable_ssl = false; } fp2.close(); } // do we want secure or not? if (app_enable_ssl) { server.setCertificate(server_cert.c_str(), server_key.c_str()); // this creates a 2nd server listening on port 80 and redirects all requests HTTPS PsychicHttpServer* redirectServer = new PsychicHttpServer(); redirectServer->config.ctrl_port = 20424; // just a random port different from the default one redirectServer->config.stack_size = 4096; // we dont need a large stack size for this. redirectServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) { String url = "https://" + request->host() + request->url(); return response->redirect(url.c_str()); }); redirectServer->start(); } #endif DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); loggingMiddleware.setOutput(Serial); basicAuth.setUsername(app_user); basicAuth.setPassword(app_pass); basicAuth.setRealm(app_name); basicAuth.setAuthMethod(HTTPAuthMethod::BASIC_AUTH); basicAuth.setAuthFailureMessage("You must log in."); digestAuth.setUsername(app_user); digestAuth.setPassword(app_pass); digestAuth.setRealm(app_name); digestAuth.setAuthMethod(HTTPAuthMethod::DIGEST_AUTH); digestAuth.setAuthFailureMessage("You must log in."); // corsMiddleware.setAllowCredentials(true); // corsMiddleware.setOrigin("http://www.example.com,https://www.example.com,http://api.example.com,https://api.example.com"); // corsMiddleware.setHeaders("Origin,X-Requested-With,Content-Type,Accept,Content-Type,Authorization,X-Access-Token"); server.addMiddleware(&loggingMiddleware); // this will send CORS headers on every HTTP_OPTIONS request that contains the Origin: header server.addMiddleware(&corsMiddleware); // rewrites! server.rewrite("/rewrite", "/api?foo=rewrite"); // serve static files from LittleFS/www on / only to clients on same wifi network // this is where our /index.html file lives // curl -i http://psychic.local/ server.serveStatic("/", LittleFS, "/www/") ->setCacheControl("max-age=60") ->addFilter(ON_STA_FILTER); // serve static files from LittleFS/www-ap on / only to clients on SoftAP // this is where our /index.html file lives server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER); // serve static files from LittleFS/img on /img // it's more efficient to serve everything from a single www directory, but this is also possible. // curl -i http://psychic.local/img/request_flow.png server.serveStatic("/img", LittleFS, "/img/"); // you can also serve single files // curl -i http://psychic.local/myfile.txt server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); // example callback everytime a connection is opened server.onOpen([](PsychicClient* client) { Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); // example callback everytime a connection is closed server.onClose([](PsychicClient* client) { Serial.printf("[http] connection #%u closed\n", client->socket()); }); // api - json message passed in as post body // curl -i -X POST -H "Content-Type: application/json" -d '{"foo":"bar"}' http://psychic.local/api server.on("/api", HTTP_POST, [](PsychicRequest* request, PsychicResponse* resp, JsonVariant& json) { JsonObject input = json.as(); // create our response json PsychicJsonResponse response(resp); JsonObject output = response.getRoot(); output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); output["method"] = request->methodStr(); // work with some params if (input.containsKey("foo")) { String foo = input["foo"]; output["foo"] = foo; } return response.send(); }); // ip - get info about the client // curl -i http://psychic.local/ip server.on("/ip", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { String output = "Your IP is: " + request->client()->remoteIP().toString(); return response->send(output.c_str()); }); // client connect/disconnect to a url // curl -i http://psychic.local/handler PsychicWebHandler* connectionHandler = new PsychicWebHandler(); connectionHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { return response->send("OK"); }); connectionHandler->onOpen([](PsychicClient* client) { Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); connectionHandler->onClose([](PsychicClient* client) { Serial.printf("[handler] connection #%u closed\n", client->socket()); }); // add it to our server server.on("/handler", connectionHandler); // api - parameters passed in via query eg. /api?foo=bar // curl -i 'http://psychic.local/api?foo=bar' server.on("/api", HTTP_GET, [](PsychicRequest* request, PsychicResponse* resp) { // create our response json PsychicJsonResponse response = PsychicJsonResponse(resp); JsonObject output = response.getRoot(); output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); output["method"] = request->methodStr(); // work with some params if (request->hasParam("foo")) { String foo = request->getParam("foo")->value(); output["foo"] = foo; } return response.send(); }); // curl -i -X GET 'http://psychic.local/any' // curl -i -X POST 'http://psychic.local/any' server.on("/any", HTTP_ANY, [](PsychicRequest* request, PsychicResponse* resp) { // create our response json PsychicJsonResponse response = PsychicJsonResponse(resp); JsonObject output = response.getRoot(); output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); output["method"] = request->methodStr(); return response.send(); }); // curl -i 'http://psychic.local/simple' server.on("/simple", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Simple"); }) ->setURIMatchFunction(MATCH_SIMPLE); #ifdef PSY_ENABLE_REGEX // curl -i 'http://psychic.local/regex/23' // curl -i 'http://psychic.local/regex/4223' server.on("^/regex/([\\d]+)/?$", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { // look up our regex matches std::smatch matches; if (request->getRegexMatches(matches)) { String output; output += "Matches: " + String(matches.size()) + "
\n"; output += "Matched URI: " + String(matches.str(0).c_str()) + "
\n"; output += "Match 1: " + String(matches.str(1).c_str()) + "
\n"; return response->send(output.c_str()); } else return response->send("No regex match."); }) ->setURIMatchFunction(MATCH_REGEX); #endif // JsonResponse example // curl -i http://psychic.local/json server.on("/json", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { PsychicJsonResponse jsonResponse = PsychicJsonResponse(response); char key[16]; char value[32]; JsonObject root = jsonResponse.getRoot(); for (int i = 0; i < 100; i++) { sprintf(key, "key%d", i); sprintf(value, "value is %d", i); root[key] = value; } return jsonResponse.send(); }); // how to redirect a request // curl -i http://psychic.local/redirect server.on("/redirect", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/alien.png"); }); // how to do basic auth // curl -i --user admin:admin http://psychic.local/auth-basic server.on("/auth-basic", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Auth Basic Success!"); }) ->addMiddleware(&basicAuth); // how to do digest auth // curl -i --user admin:admin http://psychic.local/auth-digest server.on("/auth-digest", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Auth Digest Success!"); }) ->addMiddleware(&digestAuth); // example of getting / setting cookies // curl -i -b cookie.txt -c cookie.txt http://psychic.local/cookies server.on("/cookies", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { int counter = 0; char cookie[14]; size_t size = sizeof(cookie); if (request->getCookie("counter", cookie, &size) == ESP_OK) { // value is null-terminated. counter = std::stoi(cookie); counter++; } sprintf(cookie, "%d", counter); response->setCookie("counter", cookie); response->setContent(cookie); return response->send(); }); // example of getting POST variables // curl -i -d "param1=value1¶m2=value2" -X POST http://psychic.local/post // curl -F "param1=value1" -F "param2=value2" -X POST http://psychic.local/post server.on("/post", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) { String output; output += "Param 1: " + request->getParam("param1")->value() + "
\n"; output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); // you can set up a custom 404 handler. // curl -i http://psychic.local/404 server.onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->send(404, "text/html", "Custom 404 Handler"); }); // handle a very basic upload as post body PsychicUploadHandler* uploadHandler = new PsychicUploadHandler(); uploadHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) { static File file; String path = "/www/" + filename; Serial.printf("Writing %d/%d bytes to: %s\n", (int)index + (int)len, request->contentLength(), path.c_str()); if (last) Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index + (uint64_t)len); if (!index) file = LittleFS.open(path, FILE_WRITE); if (!file) { Serial.println("Failed to open file"); return ESP_FAIL; } if (!file.write(data, len)) { Serial.println("Write failed"); return ESP_FAIL; } if (last) file.close(); return ESP_OK; }); // gets called after upload has been handled uploadHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { String url = "/" + request->getFilename(); String output = "" + url + ""; return response->send(output.c_str()); }); // wildcard basic file upload - POST to /upload/filename.ext // use http://psychic.local/ to test server.on("/upload/*", HTTP_POST, uploadHandler); // a little bit more complicated multipart form PsychicUploadHandler* multipartHandler = new PsychicUploadHandler(); multipartHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) { static File file; String path = "/www/" + filename; // some progress over serial. Serial.printf("Writing %d bytes to: %s @ index %llu\n", (int)len, path.c_str(), index); if (last) Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index + (uint64_t)len); if (!index) file = LittleFS.open(path, FILE_WRITE); if (!file) { Serial.println("Failed to open file"); return ESP_FAIL; } if (!file.write(data, len)) { Serial.println("Write failed"); return ESP_FAIL; } if (last) file.close(); return ESP_OK; }); // gets called after upload has been handled multipartHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { String output; if (request->hasParam("file_upload")) { PsychicWebParameter* file = request->getParam("file_upload"); String url = "/" + file->value(); output += "" + url + "
\n"; output += "Bytes: " + String(file->size()) + "
\n"; } if (request->hasParam("param1")) output += "Param 1: " + request->getParam("param1")->value() + "
\n"; if (request->hasParam("param2")) output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); // wildcard basic file upload - POST to /upload/filename.ext // use http://psychic.local/ to test // just multipart data: curl -F "param1=multi" -F "param2=part" http://psychic.local/multipart server.on("/multipart", HTTP_POST, multipartHandler); // form only multipart handler // curl -F "param1=multi" -F "param2=part" http://psychic.local/multipart-data PsychicUploadHandler* multipartFormHandler = new PsychicUploadHandler(); multipartFormHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { String output; if (request->hasParam("param1")) output += "Param 1: " + request->getParam("param1")->value() + "
\n"; if (request->hasParam("param2")) output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); server.on("/multipart-data", HTTP_POST, multipartFormHandler); // a websocket echo server // npm install -g wscat // Plaintext: wscat -c ws://psychic.local/ws // SSL: wscat -n -c wss://psychic.local/ws websocketHandler.onOpen([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->sendMessage("Hello!"); }); websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) { // Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), String((char*)frame->payload, frame->len).c_str()); return request->reply(frame); }); websocketHandler.onClose([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u closed\n", client->socket()); }); server.on("/ws", &websocketHandler); // EventSource server // curl -i -N http://psychic.local/events eventSource.onOpen([](PsychicEventSourceClient* client) { Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->send("Hello user!", NULL, millis(), 1000); }); eventSource.onClose([](PsychicEventSourceClient* client) { Serial.printf("[eventsource] connection #%u closed\n", client->socket()); }); server.on("/events", &eventSource); // example of using POST data inside the filter // works: curl -F "secret=password" http://psychic.local/post-filter // 404: curl -F "foo=bar" http://psychic.local/post-filter server.on("/post-filter", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) { String output; output += "Secret: " + request->getParam("secret")->value() + "
\n"; return response->send(output.c_str()); }) ->addFilter([](PsychicRequest* request) { request->loadParams(); return request->hasParam("secret"); }); server.begin(); } } unsigned long lastUpdate = 0; char output[60]; void loop() { if (millis() - lastUpdate > 1000) { sprintf(output, "Millis: %lu\n", millis()); websocketHandler.sendAll(output); sprintf(output, "%lu", millis()); eventSource.send(output, "millis", millis(), 0); lastUpdate = millis(); } vTaskDelay(1 / portTICK_PERIOD_MS); // Feed WDT } ================================================ FILE: examples/esp-idf/main/secret.h ================================================ #define WIFI_SSID "Your_SSID" #define WIFI_PASS "Your_PASS" ================================================ FILE: examples/esp-idf/partitions_custom.csv ================================================ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x11000, 0xC000 otadata, data, ota, 0x1D000, 0x2000 phy_init, data, phy, 0x1F000, 0x1000 app0, app, ota_0, 0x20000, 0x177000 app1, app, ota_1, 0x1A0000, 0x177000 littlefs, data, spiffs, 0x317000, 0xE1000 ================================================ FILE: examples/esp-idf/sdkconfig.defaults ================================================ CONFIG_AUTOSTART_ARDUINO=y # CONFIG_WS2812_LED_ENABLE is not set CONFIG_FREERTOS_HZ=1000 CONFIG_BOOTLOADER_COMPILER_OPTIMIZATION_SIZE=y CONFIG_COMPILER_OPTIMIZATION_SIZE=y # # Serial flasher config # CONFIG_ESPTOOLPY_FLASHMODE_QIO=y CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y CONFIG_ESPTOOLPY_FLASHSIZE="4MB" # # Partition Table # CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_custom.csv" #CONFIG_PARTITION_TABLE_FILENAME="partitions_custom.csv" #CONFIG_PARTITION_TABLE_OFFSET=0xE000 CONFIG_PARTITION_TABLE_MD5=y # # ESP HTTPS OTA # CONFIG_ESP_HTTPS_OTA_DECRYPT_CB=y CONFIG_ESP_HTTPS_OTA_ALLOW_HTTP=y # end of ESP HTTPS OTA # # ESP HTTP client # CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS=y CONFIG_ESP_HTTP_CLIENT_ENABLE_BASIC_AUTH=y CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y # end of ESP HTTP client # # HTTP Server # CONFIG_HTTPD_MAX_REQ_HDR_LEN=1024 CONFIG_HTTPD_MAX_URI_LEN=512 CONFIG_HTTPD_ERR_RESP_NO_DELAY=y CONFIG_HTTPD_PURGE_BUF_LEN=32 # CONFIG_HTTPD_LOG_PURGE_DATA is not set CONFIG_HTTPD_WS_SUPPORT=y # end of HTTP Server # # ESP HTTPS server # CONFIG_ESP_HTTPS_SERVER_ENABLE=n # end of ESP HTTPS server # # TLS Key Exchange Methods # # 2 option require for arduino Arduino CONFIG_MBEDTLS_PSK_MODES=y CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y ================================================ FILE: examples/platformio/.gitignore ================================================ .pio .vscode/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch src/_secret.h ================================================ FILE: examples/platformio/data/custom.txt ================================================ Custom text file. ================================================ FILE: examples/platformio/data/server.crt ================================================ -----BEGIN CERTIFICATE----- MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4 sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/ ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3 emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo= -----END CERTIFICATE----- ================================================ FILE: examples/platformio/data/server.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al 3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg 0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9 Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX 49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc +3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6 pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D 0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin 7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1 noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8 4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/ nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3 q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2 lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr v/t+MeGJP/0Zw8v/X2CFll96 -----END PRIVATE KEY----- ================================================ FILE: examples/platformio/data/www/index.html ================================================ PsychicHTTP Demo

Basic Request Examples

Utilities

Static Serving

Text File

Simple POST Form



Basic File Upload

Multipart POST Form




Websocket Demo

EventSource Demo

================================================ FILE: examples/platformio/data/www/text.txt ================================================ Test File. ================================================ FILE: examples/platformio/data/www/websocket-test.html ================================================ WebSocket Message Rate Test

WebSocket Message Rate Test

Time Remaining: 0

Messages Count: 0

Messages per second: 0

================================================ FILE: examples/platformio/data/www-ap/index.html ================================================ PsychicHTTP SoftAP Demo

SoftAP Demo

You are connected to the ESP in SoftAP mode.

================================================ FILE: examples/platformio/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env] platform = espressif32 framework = arduino board = esp32-s3-devkitc-1 upload_port = /dev/ttyACM0 monitor_port = /dev/ttyACM1 monitor_speed = 115200 monitor_filters = esp32_exception_decoder lib_deps = ; hoeken/PsychicHttp ; PIO is not able to consider installed project in CI ;../.. board_build.filesystem = littlefs build_flags = -Wall -Wextra [env:arduino2] platform = espressif32@6.8.1 [env:arduino2-ssl] platform = espressif32@6.8.1 build_flags = -D PSY_ENABLE_SSL [env:arduino2-regex] platform = espressif32@6.8.1 build_flags = -D PSY_ENABLE_REGEX [env:arduino3] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip [env:arduino3-ssl] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip build_flags = -D PSY_ENABLE_SSL [env:arduino3-regex] platform = https://github.com/pioarduino/platform-espressif32/releases/download/51.03.05/platform-espressif32.zip build_flags = -D PSY_ENABLE_REGEX [env:waveshare-4-3-touchscreen] lib_deps = ${env.lib_deps} https://github.com/esp-arduino-libs/ESP32_IO_Expander build_flags = -D PSY_ENABLE_SDCARD -D WAVESHARE_43_TOUCH ================================================ FILE: examples/platformio/src/main.cpp ================================================ /* PsychicHTTP Server Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ /********************************************************************************************** * Note: this demo relies on various files to be uploaded on the LittleFS partition * PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image **********************************************************************************************/ #include "_secret.h" #include #include #include #include #include #include #include // #define this to enable SD card support #ifdef PSY_ENABLE_SDCARD #ifdef WAVESHARE_43_TOUCH #include // Extend IO Pin define #define TP_RST 1 #define LCD_BL 2 #define LCD_RST 3 #define SD_CS 4 #define USB_SEL 5 // I2C Pin define #define I2C_MASTER_NUM I2C_NUM_0 #define I2C_MASTER_SDA_IO 8 #define I2C_MASTER_SCL_IO 9 #define SD_MOSI 11 #define SD_CLK 12 #define SD_MISO 13 #define SD_SS -1 #else #define SD_MOSI 11 #define SD_CLK 12 #define SD_MISO 13 #define SD_SS 5 #endif #include #include #include #endif // #define this to enable SSL at build (or switch to the 'ssl' build target in vscode) #ifdef PSY_ENABLE_SSL #include #endif // debugging library #ifdef PSY_DEVMODE #include #endif #ifndef WIFI_SSID #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #endif // Enter your WIFI credentials in secret.h const char* ssid = WIFI_SSID; const char* password = WIFI_PASS; // Set your SoftAP credentials const char* softap_ssid = "PsychicHttp"; const char* softap_password = ""; IPAddress softap_ip(10, 0, 0, 1); // credentials for the /auth-basic and /auth-digest examples const char* app_user = "admin"; const char* app_pass = "admin"; const char* app_name = "Your App"; LoggingMiddleware loggingMiddleware; AuthenticationMiddleware basicAuth; AuthenticationMiddleware digestAuth; // hostname for mdns (psychic.local) const char* local_hostname = "psychic"; // #define PSY_ENABLE_SSL to enable ssl #ifdef PSY_ENABLE_SSL bool app_enable_ssl = true; String server_cert; String server_key; #endif // our main server object #ifdef PSY_ENABLE_SSL PsychicHttpsServer server; #else PsychicHttpServer server; #endif PsychicWebSocketHandler websocketHandler; PsychicEventSource eventSource; CorsMiddleware corsMiddleware; // NTP server stuff const char* ntpServer1 = "pool.ntp.org"; const char* ntpServer2 = "time.nist.gov"; const long gmtOffset_sec = 0; const int daylightOffset_sec = 0; struct tm timeinfo; // Callback function (gets called when time adjusts via NTP) void timeAvailable(struct timeval* t) { if (!getLocalTime(&timeinfo)) { Serial.println("Failed to obtain time"); return; } Serial.print("NTP update: "); char buffer[40]; strftime(buffer, 40, "%FT%T%z", &timeinfo); Serial.println(buffer); } bool connectToWifi() { // WiFi.mode(WIFI_AP); // ap only mode // WiFi.mode(WIFI_STA); // client only mode WiFi.mode(WIFI_AP_STA); // ap and client // Configure SoftAP // dual client and AP mode WiFi.softAPConfig(softap_ip, softap_ip, IPAddress(255, 255, 255, 0)); // subnet FF FF FF 00 WiFi.softAP(softap_ssid, softap_password); IPAddress myIP = WiFi.softAPIP(); Serial.print("SoftAP IP Address: "); Serial.println(myIP); Serial.println(); Serial.print("[WiFi] Connecting to "); Serial.println(ssid); WiFi.begin(ssid, password); // Auto reconnect is set true as default // To set auto connect off, use the following function // WiFi.setAutoReconnect(false); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } #ifdef PSY_ENABLE_SDCARD bool setupSDCard() { #ifdef WAVESHARE_43_TOUCH ESP_IOExpander* expander = new ESP_IOExpander_CH422G((i2c_port_t)I2C_MASTER_NUM, ESP_IO_EXPANDER_I2C_CH422G_ADDRESS_000, I2C_MASTER_SCL_IO, I2C_MASTER_SDA_IO); expander->init(); expander->begin(); expander->multiPinMode(TP_RST | LCD_BL | LCD_RST | SD_CS | USB_SEL, OUTPUT); expander->multiDigitalWrite(TP_RST | LCD_BL | LCD_RST, HIGH); // use extend GPIO for SD card expander->digitalWrite(SD_CS, LOW); SPI.setHwCs(false); #endif SPI.begin(SD_CLK, SD_MISO, SD_MOSI, SD_SS); if (!SD.begin()) { Serial.println("SD Card Mount Failed"); return false; } uint8_t cardType = SD.cardType(); if (cardType == CARD_NONE) { Serial.println("No SD card attached"); return false; } Serial.print("SD Card Type: "); if (cardType == CARD_MMC) { Serial.println("MMC"); } else if (cardType == CARD_SD) { Serial.println("SDSC"); } else if (cardType == CARD_SDHC) { Serial.println("SDHC"); } else { Serial.println("UNKNOWN"); } uint64_t cardSize = SD.cardSize() / (1024 * 1024); Serial.printf("SD Card Size: %lluMB\n", cardSize); Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024)); Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024)); return true; } #endif void setup() { esp_log_level_set(PH_TAG, ESP_LOG_DEBUG); esp_log_level_set("httpd_uri", ESP_LOG_DEBUG); Serial.begin(115200); delay(10); Serial.printf("ESP-IDF Version: %s\n", esp_get_idf_version()); #ifdef ESP_ARDUINO_VERSION_STR Serial.printf("Arduino Version: %s\n", ESP_ARDUINO_VERSION_STR); #else Serial.printf("Arduino Version: %d.%d.%d\n", ESP_ARDUINO_VERSION_MAJOR, ESP_ARDUINO_VERSION_MINOR, ESP_ARDUINO_VERSION_PATCH); #endif Serial.printf("PsychicHttp Version: %s\n", PSYCHIC_VERSION_STR); // We start by connecting to a WiFi network // To debug, please enable Core Debug Level to Verbose if (connectToWifi()) { // Setup our NTP to get the current time. sntp_set_time_sync_notification_cb(timeAvailable); sntp_servermode_dhcp(1); // (optional) configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2); // set up our esp32 to listen on the psychic.local domain if (MDNS.begin(local_hostname)) MDNS.addService("http", "tcp", 80); else Serial.println("Error starting mDNS"); if (!LittleFS.begin()) { Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); return; } #ifdef PSY_ENABLE_SSL // look up our keys? if (app_enable_ssl) { File fp = LittleFS.open("/server.crt"); if (fp) { server_cert = fp.readString(); // Serial.println("Server Cert:"); // Serial.println(server_cert); } else { Serial.println("server.pem not found, SSL not available"); app_enable_ssl = false; } fp.close(); File fp2 = LittleFS.open("/server.key"); if (fp2) { server_key = fp2.readString(); // Serial.println("Server Key:"); // Serial.println(server_key); } else { Serial.println("server.key not found, SSL not available"); app_enable_ssl = false; } fp2.close(); } // do we want secure or not? if (app_enable_ssl) { server.setCertificate(server_cert.c_str(), server_key.c_str()); // this creates a 2nd server listening on port 80 and redirects all requests HTTPS PsychicHttpServer* redirectServer = new PsychicHttpServer(); redirectServer->config.ctrl_port = 20424; // just a random port different from the default one redirectServer->config.stack_size = 4096; // we dont need a large stack size for this. redirectServer->onNotFound([](PsychicRequest* request, PsychicResponse* response) { String url = "https://" + request->host() + request->url(); return response->redirect(url.c_str()); }); redirectServer->start(); } #endif DefaultHeaders::Instance().addHeader("Server", "PsychicHttp"); loggingMiddleware.setOutput(Serial); basicAuth.setUsername(app_user); basicAuth.setPassword(app_pass); basicAuth.setRealm(app_name); basicAuth.setAuthMethod(HTTPAuthMethod::BASIC_AUTH); basicAuth.setAuthFailureMessage("You must log in."); digestAuth.setUsername(app_user); digestAuth.setPassword(app_pass); digestAuth.setRealm(app_name); digestAuth.setAuthMethod(HTTPAuthMethod::DIGEST_AUTH); digestAuth.setAuthFailureMessage("You must log in."); // corsMiddleware.setAllowCredentials(true); // corsMiddleware.setOrigin("http://www.example.com,https://www.example.com,http://api.example.com,https://api.example.com"); // corsMiddleware.setHeaders("Origin,X-Requested-With,Content-Type,Accept,Content-Type,Authorization,X-Access-Token"); server.addMiddleware(&loggingMiddleware); // this will send CORS headers on every HTTP_OPTIONS request that contains the Origin: header server.addMiddleware(&corsMiddleware); // rewrites! server.rewrite("/rewrite", "/api?foo=rewrite"); // serve static files from LittleFS/www on / only to clients on same wifi network // this is where our /index.html file lives // curl -i http://psychic.local/ server.serveStatic("/", LittleFS, "/www/") ->setCacheControl("max-age=60") ->addFilter(ON_STA_FILTER); // serve static files from LittleFS/www-ap on / only to clients on SoftAP // this is where our /index.html file lives server.serveStatic("/", LittleFS, "/www-ap/")->addFilter(ON_AP_FILTER); // serve static files from LittleFS/img on /img // it's more efficient to serve everything from a single www directory, but this is also possible. // curl -i http://psychic.local/img/request_flow.png server.serveStatic("/img", LittleFS, "/img/"); #ifdef PSY_ENABLE_SDCARD // if we detect an SD card, serve all files from sd:/ on http://psychic.local/sd if (setupSDCard()) server.serveStatic("/sd", SD, "/"); #endif // you can also serve single files // curl -i http://psychic.local/myfile.txt server.serveStatic("/myfile.txt", LittleFS, "/custom.txt"); // example callback everytime a connection is opened server.onOpen([](PsychicClient* client) { Serial.printf("[http] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); // example callback everytime a connection is closed server.onClose([](PsychicClient* client) { Serial.printf("[http] connection #%u closed\n", client->socket()); }); // api - json message passed in as post body // curl -i -X POST -H "Content-Type: application/json" -d '{"foo":"bar"}' http://psychic.local/api server.on("/api", HTTP_POST, [](PsychicRequest* request, PsychicResponse* resp, JsonVariant& json) { JsonObject input = json.as(); // create our response json PsychicJsonResponse response(resp); JsonObject output = response.getRoot(); output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); output["method"] = request->methodStr(); // work with some params if (input.containsKey("foo")) { String foo = input["foo"]; output["foo"] = foo; } return response.send(); }); // ip - get info about the client // curl -i http://psychic.local/ip server.on("/ip", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { String output = "Your IP is: " + request->client()->remoteIP().toString(); return response->send(output.c_str()); }); // client connect/disconnect to a url // curl -i http://psychic.local/handler PsychicWebHandler* connectionHandler = new PsychicWebHandler(); connectionHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { return response->send("OK"); }); connectionHandler->onOpen([](PsychicClient* client) { Serial.printf("[handler] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); connectionHandler->onClose([](PsychicClient* client) { Serial.printf("[handler] connection #%u closed\n", client->socket()); }); // add it to our server server.on("/handler", connectionHandler); // api - parameters passed in via query eg. /api?foo=bar // curl -i 'http://psychic.local/api?foo=bar' server.on("/api", HTTP_GET, [](PsychicRequest* request, PsychicResponse* resp) { // create our response json PsychicJsonResponse response = PsychicJsonResponse(resp); JsonObject output = response.getRoot(); output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); output["method"] = request->methodStr(); // work with some params if (request->hasParam("foo")) { String foo = request->getParam("foo")->value(); output["foo"] = foo; } return response.send(); }); // curl -i -X GET 'http://psychic.local/any' // curl -i -X POST 'http://psychic.local/any' server.on("/any", HTTP_ANY, [](PsychicRequest* request, PsychicResponse* resp) { // create our response json PsychicJsonResponse response = PsychicJsonResponse(resp); JsonObject output = response.getRoot(); output["msg"] = "status"; output["status"] = "success"; output["millis"] = millis(); output["method"] = request->methodStr(); return response.send(); }); // curl -i 'http://psychic.local/simple' server.on("/simple", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Simple"); }) ->setURIMatchFunction(MATCH_SIMPLE); #ifdef PSY_ENABLE_REGEX // curl -i 'http://psychic.local/regex/23' // curl -i 'http://psychic.local/regex/4223' server.on("^/regex/([\\d]+)/?$", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { // look up our regex matches std::smatch matches; if (request->getRegexMatches(matches)) { String output; output += "Matches: " + String(matches.size()) + "
\n"; output += "Matched URI: " + String(matches.str(0).c_str()) + "
\n"; output += "Match 1: " + String(matches.str(1).c_str()) + "
\n"; return response->send(output.c_str()); } else return response->send("No regex match."); }) ->setURIMatchFunction(MATCH_REGEX); #endif // JsonResponse example // curl -i http://psychic.local/json server.on("/json", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { PsychicJsonResponse jsonResponse = PsychicJsonResponse(response); char key[16]; char value[32]; JsonObject root = jsonResponse.getRoot(); for (int i = 0; i < 100; i++) { sprintf(key, "key%d", i); sprintf(value, "value is %d", i); root[key] = value; } return jsonResponse.send(); }); // how to redirect a request // curl -i http://psychic.local/redirect server.on("/redirect", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->redirect("/alien.png"); }); // how to do basic auth // curl -i --user admin:admin http://psychic.local/auth-basic server.on("/auth-basic", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Auth Basic Success!"); }) ->addMiddleware(&basicAuth); // how to do digest auth // curl -i --user admin:admin http://psychic.local/auth-digest server.on("/auth-digest", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { return response->send("Auth Digest Success!"); }) ->addMiddleware(&digestAuth); // example of getting / setting cookies // curl -i -b cookie.txt -c cookie.txt http://psychic.local/cookies server.on("/cookies", HTTP_GET, [](PsychicRequest* request, PsychicResponse* response) { int counter = 0; char cookie[14]; size_t size = sizeof(cookie); if (request->getCookie("counter", cookie, &size) == ESP_OK) { // value is null-terminated. counter = std::stoi(cookie); counter++; } sprintf(cookie, "%d", counter); response->setCookie("counter", cookie); response->setContent(cookie); return response->send(); }); // example of getting POST variables // curl -i -d "param1=value1¶m2=value2" -X POST http://psychic.local/post // curl -F "param1=value1" -F "param2=value2" -X POST http://psychic.local/post server.on("/post", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) { String output; output += "Param 1: " + request->getParam("param1")->value() + "
\n"; output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); // you can set up a custom 404 handler. // curl -i http://psychic.local/404 server.onNotFound([](PsychicRequest* request, PsychicResponse* response) { return response->send(404, "text/html", "Custom 404 Handler"); }); // handle a very basic upload as post body PsychicUploadHandler* uploadHandler = new PsychicUploadHandler(); uploadHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) { static File file; String path = "/www/" + filename; Serial.printf("Writing %d/%d bytes to: %s\n", (int)index + (int)len, request->contentLength(), path.c_str()); if (last) Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index + (uint64_t)len); if (!index) file = LittleFS.open(path, FILE_WRITE); if (!file) { Serial.println("Failed to open file"); return ESP_FAIL; } if (!file.write(data, len)) { Serial.println("Write failed"); return ESP_FAIL; } if (last) file.close(); return ESP_OK; }); // gets called after upload has been handled uploadHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { String url = "/" + request->getFilename(); String output = "" + url + ""; return response->send(output.c_str()); }); // wildcard basic file upload - POST to /upload/filename.ext // use http://psychic.local/ to test server.on("/upload/*", HTTP_POST, uploadHandler); // a little bit more complicated multipart form PsychicUploadHandler* multipartHandler = new PsychicUploadHandler(); multipartHandler->onUpload([](PsychicRequest* request, const String& filename, uint64_t index, uint8_t* data, size_t len, bool last) { static File file; String path = "/www/" + filename; // some progress over serial. Serial.printf("Writing %d bytes to: %s @ index %llu\n", (int)len, path.c_str(), index); if (last) Serial.printf("%s is finished. Total bytes: %llu\n", path.c_str(), (uint64_t)index + (uint64_t)len); if (!index) file = LittleFS.open(path, FILE_WRITE); if (!file) { Serial.println("Failed to open file"); return ESP_FAIL; } if (!file.write(data, len)) { Serial.println("Write failed"); return ESP_FAIL; } if (last) file.close(); return ESP_OK; }); // gets called after upload has been handled multipartHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { String output; if (request->hasParam("file_upload")) { PsychicWebParameter* file = request->getParam("file_upload"); String url = "/" + file->value(); output += "" + url + "
\n"; output += "Bytes: " + String(file->size()) + "
\n"; } if (request->hasParam("param1")) output += "Param 1: " + request->getParam("param1")->value() + "
\n"; if (request->hasParam("param2")) output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); // wildcard basic file upload - POST to /upload/filename.ext // use http://psychic.local/ to test // just multipart data: curl -F "param1=multi" -F "param2=part" http://psychic.local/multipart server.on("/multipart", HTTP_POST, multipartHandler); // form only multipart handler // curl -F "param1=multi" -F "param2=part" http://psychic.local/multipart-data PsychicUploadHandler* multipartFormHandler = new PsychicUploadHandler(); multipartFormHandler->onRequest([](PsychicRequest* request, PsychicResponse* response) { String output; if (request->hasParam("param1")) output += "Param 1: " + request->getParam("param1")->value() + "
\n"; if (request->hasParam("param2")) output += "Param 2: " + request->getParam("param2")->value() + "
\n"; return response->send(output.c_str()); }); server.on("/multipart-data", HTTP_POST, multipartFormHandler); // a websocket echo server // npm install -g wscat // Plaintext: wscat -c ws://psychic.local/ws // SSL: wscat -n -c wss://psychic.local/ws websocketHandler.onOpen([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->sendMessage("Hello!"); }); websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) { // Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), String((char*)frame->payload, frame->len).c_str()); return request->reply(frame); }); websocketHandler.onClose([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u closed\n", client->socket()); }); server.on("/ws", &websocketHandler); // EventSource server // curl -i -N http://psychic.local/events eventSource.onOpen([](PsychicEventSourceClient* client) { Serial.printf("[eventsource] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->send("Hello user!", NULL, millis(), 1000); }); eventSource.onClose([](PsychicEventSourceClient* client) { Serial.printf("[eventsource] connection #%u closed\n", client->socket()); }); server.on("/events", &eventSource); // example of using POST data inside the filter // works: curl -F "secret=password" http://psychic.local/post-filter // 404: curl -F "foo=bar" http://psychic.local/post-filter server.on("/post-filter", HTTP_POST, [](PsychicRequest* request, PsychicResponse* response) { String output; output += "Secret: " + request->getParam("secret")->value() + "
\n"; return response->send(output.c_str()); }) ->addFilter([](PsychicRequest* request) { request->loadParams(); return request->hasParam("secret"); }); server.begin(); } } unsigned long lastUpdate = 0; char output[60]; void loop() { if (millis() - lastUpdate > 1000) { sprintf(output, "Millis: %lu\n", millis()); websocketHandler.sendAll(output); sprintf(output, "%lu", millis()); eventSource.send(output, "millis", millis(), 0); lastUpdate = millis(); } // just some dev code to test that starting / stopping the server works okay. // delay(5000); // Serial.println("Stopping Server"); // server.stop(); // delay(5000); // Serial.println("Starting Server"); // server.start(); } ================================================ FILE: examples/platformio/src/secret.h ================================================ #define WIFI_SSID "Your_SSID" #define WIFI_PASS "Your_PASS" ================================================ FILE: examples/websockets/.gitignore ================================================ .pio .vscode/ .vscode/.browse.c_cpp.db* .vscode/c_cpp_properties.json .vscode/launch.json .vscode/ipch src/_secret.h lib/PsychicHttp/ ================================================ FILE: examples/websockets/data/www/index.html ================================================ PsychicHTTP Websocket Demo

Websocket Demo

================================================ FILE: examples/websockets/include/README ================================================ This directory is intended for project header files. A header file is a file containing C declarations and macro definitions to be shared between several project source files. You request the use of a header file in your project source file (C, C++, etc) located in `src` folder by including it, with the C preprocessing directive `#include'. ```src/main.c #include "header.h" int main (void) { ... } ``` Including a header file produces the same results as copying the header file into each source file that needs it. Such copying would be time-consuming and error-prone. With a header file, the related declarations appear in only one place. If they need to be changed, they can be changed in one place, and programs that include the header file will automatically use the new version when next recompiled. The header file eliminates the labor of finding and changing all the copies as well as the risk that a failure to find one copy will result in inconsistencies within a program. In C, the usual convention is to give header files names that end with `.h'. It is most portable to use only letters, digits, dashes, and underscores in header file names, and at most one dot. Read more about using header files in official GCC documentation: * Include Syntax * Include Operation * Once-Only Headers * Computed Includes https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html ================================================ FILE: examples/websockets/lib/README ================================================ This directory is intended for project specific (private) libraries. PlatformIO will compile them to static libraries and link into executable file. The source code of each library should be placed in a an own separate directory ("lib/your_library_name/[here are source files]"). For example, see a structure of the following two libraries `Foo` and `Bar`: |--lib | | | |--Bar | | |--docs | | |--examples | | |--src | | |- Bar.c | | |- Bar.h | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html | | | |--Foo | | |- Foo.c | | |- Foo.h | | | |- README --> THIS FILE | |- platformio.ini |--src |- main.c and a contents of `src/main.c`: ``` #include #include int main (void) { ... } ``` PlatformIO Library Dependency Finder will find automatically dependent libraries scanning project source files. More information about PlatformIO Library Dependency Finder - https://docs.platformio.org/page/librarymanager/ldf.html ================================================ FILE: examples/websockets/platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env] platform = espressif32 framework = arduino board = esp32dev monitor_speed = 115200 monitor_filters = esp32_exception_decoder lib_deps = ; devmode: with this disabled make a symlink from platformio/lib to the PsychicHttp directory ;hoeken/PsychicHttp bblanchon/ArduinoJson board_build.filesystem = littlefs [env:default] build_flags = -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_WARN ================================================ FILE: examples/websockets/src/main.cpp ================================================ /* PsychicHTTP Server Example This example code is in the Public Domain (or CC0 licensed, at your option.) Unless required by applicable law or agreed to in writing, this software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ /********************************************************************************************** * Note: this demo relies on various files to be uploaded on the LittleFS partition * PlatformIO -> Build Filesystem Image and then PlatformIO -> Upload Filesystem Image **********************************************************************************************/ #include "_secret.h" #include #include #include #include #include #include #include #include #ifndef WIFI_SSID #error "You need to enter your wifi credentials. Rename secret.h to _secret.h and enter your credentials there." #endif // Enter your WIFI credentials in secret.h const char* ssid = WIFI_SSID; const char* password = WIFI_PASS; // hostname for mdns (psychic.local) const char* local_hostname = "psychic"; PsychicHttpServer server; PsychicWebSocketHandler websocketHandler; typedef struct { int socket; char* buffer; size_t len; } WebsocketMessage; QueueHandle_t wsMessages; bool connectToWifi() { Serial.print("[WiFi] Connecting to "); Serial.println(ssid); // setup our wifi WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); // Will try for about 10 seconds (20x 500ms) int tryDelay = 500; int numberOfTries = 20; // Wait for the WiFi event while (true) { switch (WiFi.status()) { case WL_NO_SSID_AVAIL: Serial.println("[WiFi] SSID not found"); break; case WL_CONNECT_FAILED: Serial.print("[WiFi] Failed - WiFi not connected! Reason: "); return false; break; case WL_CONNECTION_LOST: Serial.println("[WiFi] Connection was lost"); break; case WL_SCAN_COMPLETED: Serial.println("[WiFi] Scan is completed"); break; case WL_DISCONNECTED: Serial.println("[WiFi] WiFi is disconnected"); break; case WL_CONNECTED: Serial.println("[WiFi] WiFi is connected!"); Serial.print("[WiFi] IP address: "); Serial.println(WiFi.localIP()); return true; break; default: Serial.print("[WiFi] WiFi Status: "); Serial.println(WiFi.status()); break; } delay(tryDelay); if (numberOfTries <= 0) { Serial.print("[WiFi] Failed to connect to WiFi!"); // Use disconnect function to force stop trying to connect WiFi.disconnect(); return false; } else { numberOfTries--; } } return false; } void setup() { Serial.begin(115200); delay(10); // prepare our message queue of 10 messages wsMessages = xQueueCreate(10, sizeof(WebsocketMessage)); if (wsMessages == 0) Serial.printf("Failed to create queue= %p\n", wsMessages); // We start by connecting to a WiFi network // To debug, please enable Core Debug Level to Verbose if (connectToWifi()) { // set up our esp32 to listen on the local_hostname.local domain if (!MDNS.begin(local_hostname)) { Serial.println("Error starting mDNS"); return; } MDNS.addService("http", "tcp", 80); if (!LittleFS.begin()) { Serial.println("LittleFS Mount Failed. Do Platform -> Build Filesystem Image and Platform -> Upload Filesystem Image from VSCode"); return; } // this is where our /index.html file lives // curl -i http://psychic.local/ server.serveStatic("/", LittleFS, "/www/"); // a websocket echo server // npm install -g wscat // wscat -c ws://psychic.local/ws websocketHandler.onOpen([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u connected from %s\n", client->socket(), client->remoteIP().toString().c_str()); client->sendMessage("Hello!"); }); websocketHandler.onFrame([](PsychicWebSocketRequest* request, httpd_ws_frame* frame) { Serial.printf("[socket] #%d sent: %s\n", request->client()->socket(), (char *)frame->payload); //we are allocating memory here, and the worker will free it WebsocketMessage wm; wm.socket = request->client()->socket(); wm.len = frame->len; wm.buffer = (char *)malloc(frame->len); //did we flame out? if (wm.buffer == NULL) { Serial.printf("Queue message: unable to allocate %d bytes\n", frame->len); return ESP_FAIL; } //okay, copy it over memcpy(wm.buffer, frame->payload, frame->len); //try to throw it in our queue if (xQueueSend(wsMessages, &wm, 1) != pdTRUE) { Serial.printf("[socket] queue full #%d\n", wm.socket); //free the memory... no worker to do it for us. free(wm.buffer); } //send a throttle message if we're full if (!uxQueueSpacesAvailable(wsMessages)) return request->reply("Queue Full"); return ESP_OK; }); websocketHandler.onClose([](PsychicWebSocketClient* client) { Serial.printf("[socket] connection #%u closed from %s\n", client->socket(), client->remoteIP().toString().c_str()); }); server.on("/ws", &websocketHandler); } } unsigned long lastUpdate = 0; char output[60]; void loop() { // process our websockets outside the callback. WebsocketMessage message; while (xQueueReceive(wsMessages, &message, 0) == pdTRUE) { // make sure our client is still good. PsychicWebSocketClient* client = websocketHandler.getClient(message.socket); if (client == NULL) { Serial.printf("[socket] client #%d bad, bailing\n", message.socket); return; } // echo it back to the client. // alternatively, this is where you would deserialize a json message, parse it, and generate a response if needed client->sendMessage(HTTPD_WS_TYPE_TEXT, message.buffer, message.len); // make sure to release our memory! free(message.buffer); } // send a periodic update to all clients if (millis() - lastUpdate > 2000) { sprintf(output, "Millis: %lu\n", millis()); websocketHandler.sendAll(output); lastUpdate = millis(); } } ================================================ FILE: examples/websockets/src/secret.h ================================================ #define WIFI_SSID "Your_SSID" #define WIFI_PASS "Your_PASS" ================================================ FILE: examples/websockets/test/README ================================================ This directory is intended for PlatformIO Test Runner and project tests. Unit Testing is a software testing method by which individual units of source code, sets of one or more MCU program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use. Unit testing finds problems early in the development cycle. More information about PlatformIO Unit Testing: - https://docs.platformio.org/en/latest/advanced/unit-testing/index.html ================================================ FILE: idf_component.yml ================================================ ## IDF Component Manager Manifest File version: "2.2.0" license: "MIT" description: "Asyncronous Webserver library for ESP32 + Arduino framework" url: "https://github.com/hoeken/PsychicHttp" repository: "https://github.com/hoeken/PsychicHttp" documentation: "https://github.com/hoeken/PsychicHttp" tags: - webserver - websocket - http - https dependencies: bblanchon/arduinojson: ^7.3.0 espressif/arduino-esp32: matches: - if: "idf_version ^4" version: "^2" - if: "idf_version ^5.1" version: "^3.0" - if: "idf_version ^5.4" version: "3.2.0" ================================================ FILE: library.json ================================================ { "name": "PsychicHttp", "version": "2.2.0", "description": "Arduino style wrapper around ESP-IDF HTTP library. HTTP server with SSL + websockets. Works on esp32 and probably esp8266", "keywords": "network,http,https,tcp,ssl,tls,websocket,espasyncwebserver", "repository": { "type": "git", "url": "https://github.com/hoeken/PsychicHttp" }, "authors": [ { "name": "Zach Hoeken", "email": "hoeken@gmail.com", "maintainer": true } ], "license": "MIT", "examples": [ { "name": "platformio", "base": "examples/platformio", "files": [ "src/main.cpp" ] } ], "frameworks": "arduino", "platforms": "espressif32", "dependencies": [ { "owner": "bblanchon", "name": "ArduinoJson", "version": "^7.0.4" } ], "export": { "include": [ "examples/platformio", "src", "library.json", "library.properties", "LICENSE", "README.md" ] } } ================================================ FILE: library.properties ================================================ name=PsychicHttp version=2.2.0 author=Zach Hoeken maintainer=Zach Hoeken sentence=PsychicHttp is a robust webserver that supports http/https + websockets. paragraph=This library is based on the ESP-IDF HTTP Server library which is asynchronous, does http / https+ssl and supports websockets. category=Communication architectures=esp32 url=https://github.com/hoeken/PsychicHttp includes=PsychicHttp.h depends=ArduinoJson ================================================ FILE: middleware.md ================================================ # PsychicHandler - [x] create addMiddleware() - [x] create runMiddleware() - [ ] move all the handler::canHandle() stuff into filter(); - [ ] canHandle should be declared static # PsychicEndpoint - [ ] convert setAuthentication() to add AuthMiddleware instead. ## PsychicHttpServer - [ ] add _chain - [ ] create addMiddleware() - [ ] create runMiddleware() - [ ] create removeMiddleware(name) - [ ] _filters -> _middleware - [ ] destructor / cleanup # PsychicRequest - [ ] add _response pointer to PsychicRequest, created in constructor - [ ] request->beginReply() should return existing _response pointer - [ ] requestAuthentication() -> should move to response? # PsychicResponse - how do we have extended classes when we have a pre-declared base PsychicResponse object? - the delegation style is really ugly and causes problems with inheritance ================================================ FILE: partitions-4MB.csv ================================================ # Name ,Type ,SubType ,Offset ,Size ,Flags nvs ,data ,nvs ,36K ,20K , otadata ,data ,ota ,56K ,8K , app0 ,app ,ota_0 ,64K ,1856K , app1 ,app ,ota_1 ,1920K ,1856K , spiffs ,data ,spiffs ,3776K ,256K , coredump ,data ,coredump ,4032K ,64K , ================================================ FILE: platformio.ini ================================================ ; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [platformio] lib_dir = . src_dir = examples/platformio/src data_dir = examples/platformio/data ; src_dir = examples/websockets/src ; data_dir = examples/websockets/data ; src_dir = examples/arduino ; data_dir = examples/arduino/data ; src_dir = examples/arduino/arduino_captive_portal ; data_dir = examples/arduino/arduino_captive_portal/data ; src_dir = examples/arduino/arduino_ota ; data_dir = examples/arduino/arduino_ota/data [env] platform = espressif32 framework = arduino board = esp32-s3-devkitc-1 monitor_speed = 115200 monitor_filters = esp32_exception_decoder board_upload.flash_size = 8MB board_build.partitions = default_8MB.csv board_build.filesystem = littlefs lib_deps = bblanchon/ArduinoJson lib_ignore = examples build_flags = -Wall -Wextra -Og -D CONFIG_ARDUHAL_LOG_COLORS -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE [env:arduino2] platform = espressif32@6.8.1 [env:arduino2-ssl] platform = espressif32@6.8.1 build_flags = -DPSY_ENABLE_SSL [env:arduino2-regex] platform = espressif32@6.8.1 build_flags = -DPSY_ENABLE_REGEX [env:arduino3] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip [env:arduino3-ssl] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip build_flags = -DPSY_ENABLE_SSL [env:arduino3-regex] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip build_flags = -D PSY_ENABLE_REGEX [env:pioarduino-c6] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32-c6-devkitc-1 [env:mathieu] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32dev lib_deps = ${env.lib_deps} ; bblanchon/ArduinoTrace@^1.2.0 build_flags = ${env.build_flags} ; -D PSY_DEVMODE ; -D PSY_ENABLE_REGEX -D PSY_ENABLE_SSL [env:hoeken] platform = https://github.com/pioarduino/platform-espressif32/releases/download/stable/platform-espressif32.zip board = esp32-s3-devkitc-1 lib_deps = ${env.lib_deps} bblanchon/ArduinoTrace@^1.2.0 build_flags = ${env.build_flags} -D PSY_DEVMODE [env:ci] platform = ${sysenv.PIO_PLATFORM} board = ${sysenv.PIO_BOARD} ================================================ FILE: request flow.drawio ================================================ 7Vxbd5s4EP41Pmf3wTkgAcaPza3Zbq/bbNM+yiAbNhi5ICd2f/0KIxmQFIypsZ1jpw+1hBCS5pv5ZgaJHryaLt4maBZ8ID6OesDwFz143QPANIDD/stqlnnNwIJ5xSQJfd6oqPga/sLiTl47D32cVhpSQiIazqqVHolj7NFKHUoS8lxtNiZR9akzNMFKxVcPRWrtQ+jTIK91waCov8PhJBBPNp1hfmWKRGM+kzRAPnkuVcGbHrxKCKH5r+niCkfZ4ol1efhr+RC9f3TevvuS/kT/Xv59//FbP+/sdptb1lNIcExbd+0/B97S/uJ9MrxvX76P0/f9+Gff5MvwhKI5XzA+WboUK5iQeezjrBezBy+fg5DirzPkZVefGWZYXUCnEb+8XiSDFcYkphwRbL3ZRXZbGE9Y0c6uhlF0RSKSrB4Dx3b2j99Vqs//srtpQh5x6Yqz+mNXGq4PX8cnnFC8KKGDr9dbTKaYJkvWRFwVIObYN21efi6QZA95XVBCkevwSsTRO1n3XUiI/eBC0gvsevkufbie3t/Yd9blDfz0bvHQ70NzG4EZVeH8hyldcoGgOSWsiiQ0IBMSo+g9ITPeriQ3Myvj2H+TqSIrjyLiPeZVt2E29NUzWIm3d9eCEtqmoCJCIxxdIu9xshqoEGhMYpx15TP15XMpBndT1LKHsYX8nnV2YYvij/VAWOF6USktRWkR0tJtrPSDDy/7XdyUFcQ9KUUJFXPnI9wObSmZJx6uUUEuP/agCaZ1kueQyhaoFrwJjhANn6r2TwdEfutnErKJrEEPbFgBPRyCahf5jPhdZYOzoSPbkDrKp6x0tNKL9XwaqYpWAODkNaU9UrXtoIpUbTtn10BtajHrRl2Cwd39/WdW8w/+Occp/T2+KwOAzfFyEqE0XRuPWulK/Ocj7I49Lc95Lh6Nu+Q5t6qpQGhqiedMoOE5awc0p52CpQjtB3MiT0x92xLdjtXeaaj2ptlQ7znq+sYFgIZbQV7f5N53WxITTch4nOJ6foKGdVB+shWMfyQKxDej5FUrQTdQ3YhA4c8b+wObVTWxljHYK9gcBWw3sT/jY/yAqBdkELpVTWxApqN52iLwu0XTMMpme4ejJ0xDD2noEkXhJGYFj8kfJ3o0rYNGpyjdr9DLaEINI8dj4Glp1HdGjt1puDiUPGdnoNKooQsXu6JRU40WP6dLLwi9OxT7EVvxs/MjOz+2uUfnRxvpaaR2it5Pi3B9/x6TXoCamH5XLpMNoC25TPtjMcuQWAxILNY2N2ANm9EhwydalpqtGCzVTErQrmSSLRfUj0um6Wp79iMfQVturoVLSd9XjJyxHnAiBqDLEbPUzoSuoOqgaWaH41Ga/ScM+Zm7f4sFZP9sr9ytR4WauLjGYzSPMrgWztvJ87cle12udWE3ZHC7K9mp+YvTi+0KMq5QccHMOyHjjXlzoUdHGBRam4LCpnQKZTo1O4su9Wus5jLOHuur8lib5vZbeKyOpST5Duix2m1VTNJVW3Z9d+Sx2rIqH6XHqqaTPuJnVnGV7+IISax1SA/jnBwyJSS/WXEbJhdAZ26luuMjxQmb0QWJP81wnAkRRdGImcFjkZ9vY9e3dPJzwQh2ugMEwqrpAoODJ4eGivxUx7KgRS9b9dCriqbKw4K2zBJpFRS2iba69O0205bm3VQt6jfSVnlnj0as9m8y1xpWktEWsNqalVw5ISN11LHjB9QgtUQBzJREJH3hxcLJc4Fra1IMe+UCoIapQZ5OYmSwEp2af1qTxQvXBXnUef8nwx6Sb+ZCt2lqQnZTdydzNVQ7Xf4AmrCnVlOOlD9Mw2ob1khGadgwEb8rAhG7GktwvA3jMA2OxXyMXQ/rCWPk2pa9PSjbE8YQNCSMzpxPCGoIg0cPPaC8o0hn2AvHzIoAwzvTw4v0MBCm+HDyVd25kyAH7cZstyE3CKU4Fm4wq6hybPNiWP5rSRTWVt3ujja0smmA0y6Tzes3HK9ub2ibHHV7vapNIZcVq+5kROcHHuTMrt0yGJc7GkCwV6VQXSnGdRQnf/yZ0fI5Bte4VPY+87H6E3iK0DwU55s6znJ7UW4OPLTcVE+YcUnAph16iJ5FVyO6g6ucmvVK8qNAbwoJhiTOZHgc0sMmi1IGOukNnQFE+4xRtNKzzD1K70TzV03PjtYh/kgjlIHV0uFSAmi5o44dLleBYp4N4ScLzwZEY/4HB2du9QVqgtMZiVN8kTLLcZaaRmpu00MR3fnJanRzULu/r+1adfZ8o90fHrfdbxtoy9sdlY52tRdLvPYRzg//FMJL45LbD7bcuzXY8d4tvSKpkcvpKpKhKlLtEY1j1ST56HJTTbLEkQaxed6W7PeONMmxqs8RX815aVxyewjr28vnfKT2HWnSkb0tOagmNd6Of9ya5LTXJAmBg272B8uaIcqvW5PUlIw4p0zprFf6VotxGzFlkrWMebW0qldVl5m/Vin717xKOcSW+cihh6I3/MI09P3VGxed21/VXuVzZ8q30PiIre10r7nbDiTh6T7kAsUOj7I2we3ddlYsvn2XA6H4giC8+R8=7Vtbc5s4FP41ntk+tGMQAvyYOLdNp9NOvTttn3ZkkDGNQA7It/z6FUayBcK3xNhMA34I5+iCdL5P5xwJ0gH9aHGfoMn4C/Ux6Zhdf9EBNx3TNLqmzf9kmmWucSyQK4Ik9EWljWIQvmDZUminoY/TQkVGKWHhpKj0aBxjjxV0KEnovFhtREnxqRMUYE0x8BDRtT9Cn41zrWs6G/0DDoOxfLJh9/KSCMnKYibpGPl0rqjAbQf0E0pZfhct+phkxpN2ue69/J493t0Yzy/92Xf/3v3nKviYd3Z3TJP1FBIcs9N2beZdzxCZCnuJubKlNGBCp7GPs06MDriej0OGBxPkZaVzThmuG7OIiOK1jbpcGNGYCUJwc/NC3iyMAy7CrDQkpE8JTVaPASOY/UQrRZ9fWWuW0CeslNiri5dEdIaGq+FmQ0hwGr6oMmWIKTInOFZl7IeqKHioaA60vkBphhOGFwr3BBr3mEaYJUteRZTyueVNxMoCUBBtvuEpsIVurHDUcoQSibURrPve4M9vBAWOoENPo8MPPHxAsU9w8jZiKFwwuE2uA4LSVPAkR1auTp1GRaL4CLsjr5IQnouHozoRc7sFxCwANcQMswqxugCTA1AQE3Clq2HxUZndPiJkiLynVIOQz5uVFnDBqDGNcQkBoUIkDGIuetzEnBvgOrNiyN3ulSiIQt/PHlNJjA11yn4CVjoRMWKrNmANWFyKbsVS7Dk6sOAEwBqfH8jEnz7+9yv4m4HH5+gruhe+XMWVxt/x8xSn7K8PjVmKELu+VbUUXXMIVr65LsQsGcDlUrQOXIpmXYgZGmIaTDj2r7KUJls3mdFDr4hMcVlwiyXLn6rwKxM+QSneLNTCm6WUFiH7KYMbv1dacWnTKBNkm+NgSuk08fD+OMJQEmC2w2YCC+wXcjgddAVUWIGp1CWYIBbOiplfFdDiCd9oyKercKrk3iEsdpHPW7RSU61yR3axI2Bbn3rqVew2N5PW7YqFayO8nph6jseD+oB6T5i1oX1baLfNM4b2SthARQT4OsFx6/4r3L/dvbT7tyrguktQhFu8qvA6NHOuDS89caZxn9C0xasSL/vSeNnvM73aFdH3pldWs9IroxhiofHa9Ao4uzuqOaFymkLF42i1ly6gWXTpljIy60R0sctp/Ra6cADRUqk2ySqkh/NbDnjruMr1jUJ9fpOP4KTcdf9Q7sJ3wl2zodx1zsBd/XT63wmhyG93sdt2sevIeLFdrORJe5B5YKYNL3+Q+U5PMnf5nL3xR/K8KQGodJQJwYmOMi3rNUeZbw1HcviHhiMIzxCODLPCteURqfVsVZ7t4md0hn5IdyHPdpzN93sfs1nep3sq71Pe6dvnSX+P9jfn2LoZ+pHl7YzzZ5DHsKY4nOZkv845P8+ohkw/tWxf4mwNEM6h79zqCxD68Uj7VmAHYBd/K2Dq28s/JKLbjY7ozqs/jQBbKFRzRHfgcRHdAWeI6GZjdtqnZq/7TthbTmybwt63HcdycfNZe159888B4PZ/ ================================================ FILE: src/ChunkPrinter.cpp ================================================ #include "ChunkPrinter.h" ChunkPrinter::ChunkPrinter(PsychicResponse* response, uint8_t* buffer, size_t len) : _response(response), _buffer(buffer), _length(len), _pos(0) { } ChunkPrinter::~ChunkPrinter() { flush(); } size_t ChunkPrinter::write(uint8_t c) { esp_err_t err; // if we're full, send a chunk if (_pos == _length) { _pos = 0; err = _response->sendChunk(_buffer, _length); if (err != ESP_OK) return 0; } _buffer[_pos] = c; _pos++; return 1; } size_t ChunkPrinter::write(const uint8_t* buffer, size_t size) { size_t written = 0; while (written < size) { size_t space = _length - _pos; size_t blockSize = std::min(space, size - written); memcpy(_buffer + _pos, buffer + written, blockSize); _pos += blockSize; if (_pos == _length) { _pos = 0; if (_response->sendChunk(_buffer, _length) != ESP_OK) return written; } written += blockSize; // Update if sent correctly. } return written; } void ChunkPrinter::flush() { if (_pos) { _response->sendChunk(_buffer, _pos); _pos = 0; } } size_t ChunkPrinter::copyFrom(Stream& stream) { size_t count = 0; while (stream.available()) { if (_pos == _length) { _response->sendChunk(_buffer, _length); _pos = 0; } size_t readBytes = stream.readBytes(_buffer + _pos, _length - _pos); _pos += readBytes; count += readBytes; } return count; } ================================================ FILE: src/ChunkPrinter.h ================================================ #ifndef ChunkPrinter_h #define ChunkPrinter_h #include "PsychicResponse.h" #include class ChunkPrinter : public Print { private: PsychicResponse* _response; uint8_t* _buffer; size_t _length; size_t _pos; public: ChunkPrinter(PsychicResponse* response, uint8_t* buffer, size_t len); ~ChunkPrinter(); size_t write(uint8_t c) override; size_t write(const uint8_t* buffer, size_t size) override; size_t copyFrom(Stream& stream); void flush() override; }; #endif ================================================ FILE: src/MultipartProcessor.cpp ================================================ #include "MultipartProcessor.h" #include "PsychicRequest.h" enum { EXPECT_BOUNDARY, PARSE_HEADERS, WAIT_FOR_RETURN1, EXPECT_FEED1, EXPECT_DASH1, EXPECT_DASH2, BOUNDARY_OR_DATA, DASH3_OR_RETURN2, EXPECT_FEED2, PARSING_FINISHED, PARSE_ERROR }; MultipartProcessor::MultipartProcessor(PsychicRequest* request, PsychicUploadCallback uploadCallback) : _request(request), _uploadCallback(uploadCallback), _temp(), _parsedLength(0), _multiParseState(EXPECT_BOUNDARY), _boundaryPosition(0), _itemStartIndex(0), _itemSize(0), _itemName(), _itemFilename(), _itemType(), _itemValue(), _itemBuffer(0), _itemBufferIndex(0), _itemIsFile(false) { } MultipartProcessor::~MultipartProcessor() {} esp_err_t MultipartProcessor::process() { esp_err_t err = ESP_OK; _parsedLength = 0; String value = _request->header("Content-Type"); if (value.startsWith("multipart/")) { _boundary = value.substring(value.indexOf('=') + 1); _boundary.replace("\"", ""); } else { ESP_LOGE(PH_TAG, "No multipart boundary found."); return ESP_ERR_HTTPD_INVALID_REQ; } char* buf = (char*)malloc(FILE_CHUNK_SIZE); int received; unsigned long index = 0; /* Content length of the request gives the size of the file being uploaded */ int remaining = _request->contentLength(); while (remaining > 0) { #ifdef ENABLE_ASYNC httpd_sess_update_lru_counter(request->server()->server, request->client()->socket()); #endif /* Receive the file part by part into a buffer */ if ((received = httpd_req_recv(_request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0) { /* Retry if timeout occurred */ if (received == HTTPD_SOCK_ERR_TIMEOUT) continue; // bail if we got an error else if (received == HTTPD_SOCK_ERR_FAIL) { ESP_LOGE(PH_TAG, "Socket error"); err = ESP_FAIL; break; } } // parse it 1 byte at a time. for (int i = 0; i < received; i++) { /* Keep track of remaining size of the file left to be uploaded */ remaining--; index++; // send it to our parser _parseMultipartPostByte(buf[i], !remaining); _parsedLength++; } } // dont forget to free our buffer free(buf); return err; } esp_err_t MultipartProcessor::process(const char* body) { esp_err_t err = ESP_OK; _parsedLength = 0; String value = _request->header("Content-Type"); if (value.startsWith("multipart/")) { _boundary = value.substring(value.indexOf('=') + 1); _boundary.replace("\"", ""); } else { ESP_LOGE(PH_TAG, "No multipart boundary found."); return ESP_ERR_HTTPD_INVALID_REQ; } // loop over the whole string unsigned int size = strlen(body); for (unsigned i = 0; i < size; i++) { // send it to our parser _parseMultipartPostByte(body[i], i == size - 1); _parsedLength++; } return err; } void MultipartProcessor::_handleUploadByte(uint8_t data, bool last) { _itemBuffer[_itemBufferIndex++] = data; if (last || _itemBufferIndex == FILE_CHUNK_SIZE) { if (_uploadCallback) _uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, last); _itemBufferIndex = 0; } } #define itemWriteByte(b) \ do \ { \ _itemSize++; \ if (_itemIsFile) \ _handleUploadByte(b, last); \ else \ _itemValue += (char)(b); \ } while (0) void MultipartProcessor::_parseMultipartPostByte(uint8_t data, bool last) { if (_multiParseState == PARSE_ERROR) { // not sure we can end up with an error during buffer fill, but jsut to be safe if (_itemBuffer != NULL) { free(_itemBuffer); _itemBuffer = NULL; } return; } if (!_parsedLength) { _multiParseState = EXPECT_BOUNDARY; _temp = String(); _itemName = String(); _itemFilename = String(); _itemType = String(); } if (_multiParseState == WAIT_FOR_RETURN1) { if (data != '\r') { itemWriteByte(data); } else { _multiParseState = EXPECT_FEED1; } } else if (_multiParseState == EXPECT_BOUNDARY) { if (_parsedLength < 2 && data != '-') { ESP_LOGE(PH_TAG, "Multipart: No boundary"); _multiParseState = PARSE_ERROR; return; } else if (_parsedLength - 2 < _boundary.length() && _boundary.c_str()[_parsedLength - 2] != data) { ESP_LOGE(PH_TAG, "Multipart: Multipart malformed"); _multiParseState = PARSE_ERROR; return; } else if (_parsedLength - 2 == _boundary.length() && data != '\r') { ESP_LOGE(PH_TAG, "Multipart: Multipart missing carriage return"); _multiParseState = PARSE_ERROR; return; } else if (_parsedLength - 3 == _boundary.length()) { if (data != '\n') { ESP_LOGE(PH_TAG, "Multipart: Multipart missing newline"); _multiParseState = PARSE_ERROR; return; } _multiParseState = PARSE_HEADERS; _itemIsFile = false; } } else if (_multiParseState == PARSE_HEADERS) { if ((char)data != '\r' && (char)data != '\n') _temp += (char)data; if ((char)data == '\n') { if (_temp.length()) { if (_temp.length() > 12 && _temp.substring(0, 12).equalsIgnoreCase("Content-Type")) { _itemType = _temp.substring(14); _itemIsFile = true; } else if (_temp.length() > 19 && _temp.substring(0, 19).equalsIgnoreCase("Content-Disposition")) { _temp = _temp.substring(_temp.indexOf(';') + 2); while (_temp.indexOf(';') > 0) { String name = _temp.substring(0, _temp.indexOf('=')); String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.indexOf(';') - 1); if (name == "name") { _itemName = nameVal; } else if (name == "filename") { _itemFilename = nameVal; _itemIsFile = true; } _temp = _temp.substring(_temp.indexOf(';') + 2); } String name = _temp.substring(0, _temp.indexOf('=')); String nameVal = _temp.substring(_temp.indexOf('=') + 2, _temp.length() - 1); if (name == "name") { _itemName = nameVal; } else if (name == "filename") { _itemFilename = nameVal; _itemIsFile = true; } } _temp = String(); } else { _multiParseState = WAIT_FOR_RETURN1; // value starts from here _itemSize = 0; _itemStartIndex = _parsedLength; _itemValue = String(); if (_itemIsFile) { if (_itemBuffer) free(_itemBuffer); _itemBuffer = (uint8_t*)malloc(FILE_CHUNK_SIZE); if (_itemBuffer == NULL) { ESP_LOGE(PH_TAG, "Multipart: Failed to allocate buffer"); _multiParseState = PARSE_ERROR; return; } _itemBufferIndex = 0; } } } } else if (_multiParseState == EXPECT_FEED1) { if (data != '\n') { _multiParseState = WAIT_FOR_RETURN1; itemWriteByte('\r'); _parseMultipartPostByte(data, last); } else { _multiParseState = EXPECT_DASH1; } } else if (_multiParseState == EXPECT_DASH1) { if (data != '-') { _multiParseState = WAIT_FOR_RETURN1; itemWriteByte('\r'); itemWriteByte('\n'); _parseMultipartPostByte(data, last); } else { _multiParseState = EXPECT_DASH2; } } else if (_multiParseState == EXPECT_DASH2) { if (data != '-') { _multiParseState = WAIT_FOR_RETURN1; itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); _parseMultipartPostByte(data, last); } else { _multiParseState = BOUNDARY_OR_DATA; _boundaryPosition = 0; } } else if (_multiParseState == BOUNDARY_OR_DATA) { if (_boundaryPosition < _boundary.length() && _boundary.c_str()[_boundaryPosition] != data) { _multiParseState = WAIT_FOR_RETURN1; itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); uint8_t i; for (i = 0; i < _boundaryPosition; i++) itemWriteByte(_boundary.c_str()[i]); _parseMultipartPostByte(data, last); } else if (_boundaryPosition == _boundary.length() - 1) { _multiParseState = DASH3_OR_RETURN2; if (!_itemIsFile) { // External - Add parameter! _request->addParam(_itemName, _itemValue); } else { if (_itemSize) { if (_uploadCallback) { _uploadCallback(_request, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true); } _itemBufferIndex = 0; // External - Add parameter! _request->addParam(new PsychicWebParameter(_itemName, _itemFilename, true, true, _itemSize)); } free(_itemBuffer); _itemBuffer = NULL; } } else { _boundaryPosition++; } } else if (_multiParseState == DASH3_OR_RETURN2) { if (data == '-' && (_request->contentLength() - _parsedLength - 4) != 0) { ESP_LOGE(PH_TAG, "ERROR: The parser got to the end of the POST but is expecting more bytes!"); _multiParseState = PARSE_ERROR; return; } if (data == '\r') { _multiParseState = EXPECT_FEED2; } else if (data == '-' && _request->contentLength() == (_parsedLength + 4)) { _multiParseState = PARSING_FINISHED; } else { _multiParseState = WAIT_FOR_RETURN1; itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); uint8_t i; for (i = 0; i < _boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); _parseMultipartPostByte(data, last); } } else if (_multiParseState == EXPECT_FEED2) { if (data == '\n') { _multiParseState = PARSE_HEADERS; _itemIsFile = false; } else { _multiParseState = WAIT_FOR_RETURN1; itemWriteByte('\r'); itemWriteByte('\n'); itemWriteByte('-'); itemWriteByte('-'); uint8_t i; for (i = 0; i < _boundary.length(); i++) itemWriteByte(_boundary.c_str()[i]); itemWriteByte('\r'); _parseMultipartPostByte(data, last); } } } ================================================ FILE: src/MultipartProcessor.h ================================================ #ifndef MULTIPART_PROCESSOR_H #define MULTIPART_PROCESSOR_H #include "PsychicCore.h" /* * MultipartProcessor - handle parsing and processing a multipart form. * */ class MultipartProcessor { protected: PsychicRequest* _request; PsychicUploadCallback _uploadCallback; String _temp; size_t _parsedLength; uint8_t _multiParseState; String _boundary; uint8_t _boundaryPosition; size_t _itemStartIndex; size_t _itemSize; String _itemName; String _itemFilename; String _itemType; String _itemValue; uint8_t* _itemBuffer; size_t _itemBufferIndex; bool _itemIsFile; void _handleUploadByte(uint8_t data, bool last); void _parseMultipartPostByte(uint8_t data, bool last); public: MultipartProcessor(PsychicRequest* request, PsychicUploadCallback uploadCallback = nullptr); ~MultipartProcessor(); esp_err_t process(); esp_err_t process(const char* body); }; #endif ================================================ FILE: src/PsychicClient.cpp ================================================ #include "PsychicClient.h" #include "PsychicHttpServer.h" #include PsychicClient::PsychicClient(httpd_handle_t server, int socket) : _server(server), _socket(socket), _friend(NULL), isNew(false) { } PsychicClient::~PsychicClient() { } httpd_handle_t PsychicClient::server() { return _server; } int PsychicClient::socket() { return _socket; } // I'm not sure this is entirely safe to call. I was having issues with race conditions when highly loaded using this. esp_err_t PsychicClient::close() { esp_err_t err = httpd_sess_trigger_close(_server, _socket); // PsychicHttpServer::closeCallback(_server, _socket); // call this immediately so the client is taken off the list. return err; } IPAddress PsychicClient::localIP() { IPAddress address(0, 0, 0, 0); char ipstr[INET6_ADDRSTRLEN]; struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing socklen_t addr_size = sizeof(addr); if (getsockname(_socket, (struct sockaddr*)&addr, &addr_size) < 0) { ESP_LOGE(PH_TAG, "Error getting client IP"); return address; } // Convert to IPv4 string inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr)); // ESP_LOGD(PH_TAG, "Client Local IP => %s", ipstr); address.fromString(ipstr); return address; } uint16_t PsychicClient::localPort() const { struct sockaddr_storage addr; socklen_t len = sizeof addr; getsockname(_socket, (struct sockaddr*)&addr, &len); struct sockaddr_in* s = (struct sockaddr_in*)&addr; return ntohs(s->sin_port); } IPAddress PsychicClient::remoteIP() { IPAddress address(0, 0, 0, 0); char ipstr[INET6_ADDRSTRLEN]; struct sockaddr_in6 addr; // esp_http_server uses IPv6 addressing socklen_t addr_size = sizeof(addr); if (getpeername(_socket, (struct sockaddr*)&addr, &addr_size) < 0) { ESP_LOGE(PH_TAG, "Error getting client IP"); return address; } // Convert to IPv4 string inet_ntop(AF_INET, &addr.sin6_addr.un.u32_addr[3], ipstr, sizeof(ipstr)); // ESP_LOGD(PH_TAG, "Client Remote IP => %s", ipstr); address.fromString(ipstr); return address; } uint16_t PsychicClient::remotePort() const { struct sockaddr_storage addr; socklen_t len = sizeof addr; getpeername(_socket, (struct sockaddr*)&addr, &len); struct sockaddr_in* s = (struct sockaddr_in*)&addr; return ntohs(s->sin_port); } ================================================ FILE: src/PsychicClient.h ================================================ #ifndef PsychicClient_h #define PsychicClient_h #include "PsychicCore.h" /* * PsychicClient :: Generic wrapper around the ESP-IDF socket */ class PsychicClient { protected: httpd_handle_t _server; int _socket; public: PsychicClient(httpd_handle_t server, int socket); ~PsychicClient(); // no idea if this is the right way to do it or not, but lets see. // pointer to our derived class (eg. PsychicWebSocketConnection) void* _friend; bool isNew = false; bool operator==(PsychicClient& rhs) const { return _socket == rhs.socket(); } httpd_handle_t server(); int socket(); esp_err_t close(); IPAddress localIP(); uint16_t localPort() const; IPAddress remoteIP(); uint16_t remotePort() const; }; #endif ================================================ FILE: src/PsychicCore.h ================================================ #ifndef PsychicCore_h #define PsychicCore_h #define PH_TAG "psychic" #ifndef FILE_CHUNK_SIZE #define FILE_CHUNK_SIZE 8 * 1024 #endif #ifndef STREAM_CHUNK_SIZE #define STREAM_CHUNK_SIZE 1024 #endif #ifndef MAX_UPLOAD_SIZE #define MAX_UPLOAD_SIZE (2048 * 1024) // 2MB #endif #ifndef MAX_REQUEST_BODY_SIZE #define MAX_REQUEST_BODY_SIZE (16 * 1024) // 16K #endif #ifdef ARDUINO #include #endif #include "FS.h" #include "MD5Builder.h" #include "UrlEncode.h" #include "esp_random.h" #include #include #include #include #include #ifdef PSY_DEVMODE #include "ArduinoTrace.h" #endif enum HTTPAuthMethod { BASIC_AUTH, DIGEST_AUTH }; String urlDecode(const char* encoded); class PsychicHttpServer; class PsychicRequest; class PsychicResponse; class PsychicWebSocketRequest; class PsychicClient; // filter function definition typedef std::function PsychicRequestFilterFunction; // middleware function definition typedef std::function PsychicMiddlewareNext; typedef std::function PsychicMiddlewareCallback; // client connect callback typedef std::function PsychicClientCallback; // callback definitions typedef std::function PsychicHttpRequestCallback; typedef std::function PsychicJsonRequestCallback; typedef std::function PsychicUploadCallback; struct HTTPHeader { String field; String value; }; class DefaultHeaders { std::list _headers; public: DefaultHeaders() {} void addHeader(const String& field, const String& value) { _headers.push_back({field, value}); } void addHeader(const char* field, const char* value) { _headers.push_back({field, value}); } const std::list& getHeaders() { return _headers; } // delete the copy constructor, singleton class DefaultHeaders(DefaultHeaders const&) = delete; DefaultHeaders& operator=(DefaultHeaders const&) = delete; // single static class interface static DefaultHeaders& Instance() { static DefaultHeaders instance; return instance; } }; #endif // PsychicCore_h ================================================ FILE: src/PsychicEndpoint.cpp ================================================ #include "PsychicEndpoint.h" #include "PsychicHttpServer.h" PsychicEndpoint::PsychicEndpoint() : _server(NULL), _uri(""), _method(HTTP_GET), _handler(NULL) { } PsychicEndpoint::PsychicEndpoint(PsychicHttpServer* server, int method, const char* uri) : _server(server), _uri(uri), _method(method), _handler(NULL) { } PsychicEndpoint::~PsychicEndpoint() { delete _handler; } PsychicEndpoint* PsychicEndpoint::setHandler(PsychicHandler* handler) { // clean up old / default handler if (_handler != NULL) delete _handler; // get our new pointer _handler = handler; // keep a pointer to the server _handler->_server = _server; return this; } PsychicHandler* PsychicEndpoint::handler() { return _handler; } String PsychicEndpoint::uri() { return _uri; } esp_err_t PsychicEndpoint::requestCallback(httpd_req_t* req) { #ifdef ENABLE_ASYNC if (is_on_async_worker_thread() == false) { if (submit_async_req(req, PsychicEndpoint::requestCallback) == ESP_OK) { return ESP_OK; } else { httpd_resp_set_status(req, "503 Busy"); httpd_resp_sendstr(req, "No workers available. Server busy."); return ESP_OK; } } #endif PsychicEndpoint* self = (PsychicEndpoint*)req->user_ctx; PsychicRequest request(self->_server, req); esp_err_t err = self->process(&request); if (err == HTTPD_404_NOT_FOUND) return PsychicHttpServer::requestHandler(req); if (err == ESP_ERR_HTTPD_INVALID_REQ) return request.response()->error(HTTPD_500_INTERNAL_SERVER_ERROR, "No handler registered."); return err; } bool PsychicEndpoint::matches(const char* uri) { // we only want to match the path, no GET strings char* ptr; size_t position = 0; // look for a ? and set our path length to that, ptr = strchr(uri, '?'); if (ptr != NULL) position = (size_t)(int)(ptr - uri); // or use the whole uri if not found else position = strlen(uri); // do we have a per-endpoint match function if (this->getURIMatchFunction() != NULL) { // ESP_LOGD(PH_TAG, "Match? %s == %s (%d)", _uri.c_str(), uri, position); return this->getURIMatchFunction()(_uri.c_str(), uri, (size_t)position); } // do we have a global match function if (_server->getURIMatchFunction() != NULL) { // ESP_LOGD(PH_TAG, "Match? %s == %s (%d)", _uri.c_str(), uri, position); return _server->getURIMatchFunction()(_uri.c_str(), uri, (size_t)position); } else { ESP_LOGE(PH_TAG, "No uri matching function set"); return false; } } httpd_uri_match_func_t PsychicEndpoint::getURIMatchFunction() { return _uri_match_fn; } void PsychicEndpoint::setURIMatchFunction(httpd_uri_match_func_t match_fn) { _uri_match_fn = match_fn; } PsychicEndpoint* PsychicEndpoint::addFilter(PsychicRequestFilterFunction fn) { _handler->addFilter(fn); return this; } PsychicEndpoint* PsychicEndpoint::addMiddleware(PsychicMiddleware* middleware) { _handler->addMiddleware(middleware); return this; } PsychicEndpoint* PsychicEndpoint::addMiddleware(PsychicMiddlewareCallback fn) { _handler->addMiddleware(fn); return this; } void PsychicEndpoint::removeMiddleware(PsychicMiddleware* middleware) { _handler->removeMiddleware(middleware); } esp_err_t PsychicEndpoint::process(PsychicRequest* request) { esp_err_t ret = ESP_ERR_HTTPD_INVALID_REQ; if (_handler != NULL) ret = _handler->process(request); ESP_LOGD(PH_TAG, "Endpoint %s processed %s: %s", _uri.c_str(), request->uri().c_str(), esp_err_to_name(ret)); return ret; } ================================================ FILE: src/PsychicEndpoint.h ================================================ #ifndef PsychicEndpoint_h #define PsychicEndpoint_h #include "PsychicCore.h" class PsychicHandler; class PsychicMiddleware; #ifdef ENABLE_ASYNC #include "async_worker.h" #endif class PsychicEndpoint { friend PsychicHttpServer; private: PsychicHttpServer* _server; String _uri; int _method; PsychicHandler* _handler; httpd_uri_match_func_t _uri_match_fn = nullptr; // use this change the endpoint matching function. public: PsychicEndpoint(); PsychicEndpoint(PsychicHttpServer* server, int method, const char* uri); ~PsychicEndpoint(); PsychicEndpoint* setHandler(PsychicHandler* handler); PsychicHandler* handler(); httpd_uri_match_func_t getURIMatchFunction(); void setURIMatchFunction(httpd_uri_match_func_t match_fn); bool matches(const char* uri); // called to process this endpoint with its middleware chain esp_err_t process(PsychicRequest* request); PsychicEndpoint* addFilter(PsychicRequestFilterFunction fn); PsychicEndpoint* addMiddleware(PsychicMiddleware* middleware); PsychicEndpoint* addMiddleware(PsychicMiddlewareCallback fn); void removeMiddleware(PsychicMiddleware* middleware); String uri(); static esp_err_t requestCallback(httpd_req_t* req); }; #endif // PsychicEndpoint_h ================================================ FILE: src/PsychicEventSource.cpp ================================================ /* Asynchronous WebServer library for Espressif MCUs Copyright (c) 2016 Hristo Gochkov. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "PsychicEventSource.h" #include "esp_log.h" #include /*****************************************/ // PsychicEventSource - Handler /*****************************************/ PsychicEventSource::PsychicEventSource() : PsychicHandler(), _onOpen(nullptr), _onClose(nullptr) { } PsychicEventSource::~PsychicEventSource() { } PsychicEventSourceClient* PsychicEventSource::getClient(int socket) { PsychicClient* client = PsychicHandler::getClient(socket); if (client == nullptr) return nullptr; return (PsychicEventSourceClient*)client->_friend; } PsychicEventSourceClient* PsychicEventSource::getClient(PsychicClient* client) { return getClient(client->socket()); } esp_err_t PsychicEventSource::handleRequest(PsychicRequest* request, PsychicResponse* resp) { // start our open ended HTTP response PsychicEventSourceResponse response(resp); response.addHeader("Content-Type", "text/event-stream"); response.addHeader("Cache-Control", "no-cache"); response.addHeader("Connection", "keep-alive"); esp_err_t err = response.send(); // lookup our client PsychicClient* client = checkForNewClient(request->client()); if (client->isNew) { // did we get our last id? if (request->hasHeader("Last-Event-ID")) { PsychicEventSourceClient* buddy = getClient(client); buddy->_lastId = atoi(request->header("Last-Event-ID").c_str()); } // let our handler know. openCallback(client); } return err; } PsychicEventSource* PsychicEventSource::onOpen(PsychicEventSourceClientCallback fn) { _onOpen = fn; return this; } PsychicEventSource* PsychicEventSource::onClose(PsychicEventSourceClientCallback fn) { _onClose = fn; return this; } void PsychicEventSource::addClient(PsychicClient* client) { client->_friend = new PsychicEventSourceClient(client); PsychicHandler::addClient(client); } void PsychicEventSource::removeClient(PsychicClient* client) { auto buddy = static_cast(client->_friend); if (buddy) { delete buddy; client->_friend = nullptr; } PsychicHandler::removeClient(client); } void PsychicEventSource::openCallback(PsychicClient* client) { PsychicEventSourceClient* buddy = getClient(client); if (buddy == nullptr) { return; } if (_onOpen != nullptr) _onOpen(buddy); } void PsychicEventSource::closeCallback(PsychicClient* client) { PsychicEventSourceClient* buddy = getClient(client); if (buddy == nullptr) { return; } if (_onClose != nullptr) _onClose(getClient(buddy)); } /** * @brief Sends an event to all connected clients. * * This function now safely handles client disconnections. * It iterates through all clients, attempts to send the event, and collects * any clients for whom the send fails. It then properly removes these * disconnected clients after the loop, preventing a crash from using a stale handle. */ void PsychicEventSource::send(const char* message, const char* event, uint32_t id, uint32_t reconnect) { String ev = generateEventMessage(message, event, id, reconnect); std::vector clientsToRemove; // First, iterate and send, collecting disconnected clients for (PsychicClient* c : _clients) { if (!((PsychicEventSourceClient*)c->_friend)->sendEvent(ev.c_str())) { clientsToRemove.push_back(c); } } // Second, iterate through the disconnected clients and clean them up for (PsychicClient* c : clientsToRemove) { closeCallback(c); // Let the user application know removeClient(c); // Remove from handler and clean up memory } } /*****************************************/ // PsychicEventSourceClient /*****************************************/ PsychicEventSourceClient::PsychicEventSourceClient(PsychicClient* client) : PsychicClient(client->server(), client->socket()), _lastId(0) { } PsychicEventSourceClient::~PsychicEventSourceClient() { } /** * @brief Returns a boolean indicating send success. */ bool PsychicEventSourceClient::send(const char* message, const char* event, uint32_t id, uint32_t reconnect) { String ev = generateEventMessage(message, event, id, reconnect); return sendEvent(ev.c_str()); } /** * @brief Sends data and returns true on success, false on failure. * This prevents a crash by detecting if the underlying socket is closed. */ bool PsychicEventSourceClient::sendEvent(const char* event) { int result; do { result = httpd_socket_send(this->server(), this->socket(), event, strlen(event), 0); } while (result == HTTPD_SOCK_ERR_TIMEOUT); if (result < 0) { ESP_LOGD(PH_TAG, "sendEvent to socket %d failed. Client likely disconnected.", this->socket()); return false; } return true; } /*****************************************/ // PsychicEventSourceResponse /*****************************************/ PsychicEventSourceResponse::PsychicEventSourceResponse(PsychicResponse* response) : PsychicResponseDelegate(response) { } esp_err_t PsychicEventSourceResponse::send() { // build our main header String out = String(); out.concat("HTTP/1.1 200 OK\r\n"); // now do our individual headers for (auto& header : _response->headers()) out.concat(header.field + ": " + header.value + "\r\n"); // separator out.concat("\r\n"); int result; do { result = httpd_send(request(), out.c_str(), out.length()); } while (result == HTTPD_SOCK_ERR_TIMEOUT); if (result < 0) ESP_LOGE(PH_TAG, "EventSource send failed with %s", esp_err_to_name(result)); if (result > 0) return ESP_OK; else return ESP_ERR_HTTPD_RESP_SEND; } /*****************************************/ // Event Message Generator /*****************************************/ String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect) { String ev = ""; if (reconnect) { ev += "retry: "; ev += String(reconnect); ev += "\r\n"; } if (id) { ev += "id: "; ev += String(id); ev += "\r\n"; } if (event != NULL) { ev += "event: "; ev += String(event); ev += "\r\n"; } if (message != NULL) { ev += "data: "; ev += String(message); ev += "\r\n"; } ev += "\r\n"; return ev; } ================================================ FILE: src/PsychicEventSource.h ================================================ /* Asynchronous WebServer library for Espressif MCUs Copyright (c) 2016 Hristo Gochkov. All rights reserved. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef PsychicEventSource_H_ #define PsychicEventSource_H_ #include "PsychicClient.h" #include "PsychicCore.h" #include "PsychicHandler.h" #include "PsychicResponse.h" #include class PsychicEventSource; class PsychicEventSourceResponse; class PsychicEventSourceClient; class PsychicResponse; typedef std::function PsychicEventSourceClientCallback; typedef struct { httpd_handle_t handle; int socket; char* event; size_t len; transfer_complete_cb callback; void* arg; } async_event_transfer_t; class PsychicEventSourceClient : public PsychicClient { friend PsychicEventSource; protected: uint32_t _lastId; esp_err_t _sendEventAsync(httpd_handle_t handle, int socket, const char* event, size_t len); static void _sendEventWorkCallback(void* arg); static void _sendEventSentCallback(esp_err_t err, int socket, void* arg); public: PsychicEventSourceClient(PsychicClient* client); ~PsychicEventSourceClient(); uint32_t lastId() const { return _lastId; } bool send(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0); bool sendEvent(const char *event); }; class PsychicEventSource : public PsychicHandler { private: PsychicEventSourceClientCallback _onOpen; PsychicEventSourceClientCallback _onClose; public: PsychicEventSource(); ~PsychicEventSource(); PsychicEventSourceClient* getClient(int socket) override; PsychicEventSourceClient* getClient(PsychicClient* client) override; void addClient(PsychicClient* client) override; void removeClient(PsychicClient* client) override; void openCallback(PsychicClient* client) override; void closeCallback(PsychicClient* client) override; PsychicEventSource* onOpen(PsychicEventSourceClientCallback fn); PsychicEventSource* onClose(PsychicEventSourceClientCallback fn); esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override final; void send(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0); }; class PsychicEventSourceResponse : public PsychicResponseDelegate { public: PsychicEventSourceResponse(PsychicResponse* response); esp_err_t send(); }; String generateEventMessage(const char* message, const char* event, uint32_t id, uint32_t reconnect); #endif /* PsychicEventSource_H_ */ ================================================ FILE: src/PsychicFileResponse.cpp ================================================ #include "PsychicFileResponse.h" #include "PsychicRequest.h" #include "PsychicResponse.h" #include PsychicFileResponse::PsychicFileResponse(PsychicResponse* response, FS& fs, const String& path, const String& contentType, bool download) : PsychicResponseDelegate(response) { //_code = 200; String _path(path); if (!download && !fs.exists(_path) && fs.exists(_path + ".gz")) { _path = _path + ".gz"; addHeader("Content-Encoding", "gzip"); } _content = fs.open(_path, "r"); setContentLength(_content.size()); if (contentType == "") _setContentTypeFromPath(path); else setContentType(contentType.c_str()); int filenameStart = path.lastIndexOf('/') + 1; char buf[26 + path.length() - filenameStart]; char* filename = (char*)path.c_str() + filenameStart; if (download) { // set filename and force download snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", filename); } else { // set filename and force rendering snprintf(buf, sizeof(buf), "inline; filename=\"%s\"", filename); } addHeader("Content-Disposition", buf); } PsychicFileResponse::PsychicFileResponse(PsychicResponse* response, File content, const String& path, const String& contentType, bool download) : PsychicResponseDelegate(response) { String _path(path); if (!download && String(content.name()).endsWith(".gz") && !path.endsWith(".gz")) { addHeader("Content-Encoding", "gzip"); } _content = content; setContentLength(_content.size()); if (contentType == "") _setContentTypeFromPath(path); else setContentType(contentType.c_str()); int filenameStart = path.lastIndexOf('/') + 1; char buf[26 + path.length() - filenameStart]; char* filename = (char*)path.c_str() + filenameStart; if (download) { snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", filename); } else { snprintf(buf, sizeof(buf), "inline; filename=\"%s\"", filename); } addHeader("Content-Disposition", buf); } PsychicFileResponse::~PsychicFileResponse() { if (_content) _content.close(); } void PsychicFileResponse::_setContentTypeFromPath(const String& path) { const char* _contentType; if (path.endsWith(".html")) _contentType = "text/html"; else if (path.endsWith(".htm")) _contentType = "text/html"; else if (path.endsWith(".css")) _contentType = "text/css"; else if (path.endsWith(".json")) _contentType = "application/json"; else if (path.endsWith(".js")) _contentType = "application/javascript"; else if (path.endsWith(".png")) _contentType = "image/png"; else if (path.endsWith(".gif")) _contentType = "image/gif"; else if (path.endsWith(".jpg")) _contentType = "image/jpeg"; else if (path.endsWith(".ico")) _contentType = "image/x-icon"; else if (path.endsWith(".svg")) _contentType = "image/svg+xml"; else if (path.endsWith(".eot")) _contentType = "font/eot"; else if (path.endsWith(".woff")) _contentType = "font/woff"; else if (path.endsWith(".woff2")) _contentType = "font/woff2"; else if (path.endsWith(".ttf")) _contentType = "font/ttf"; else if (path.endsWith(".xml")) _contentType = "text/xml"; else if (path.endsWith(".pdf")) _contentType = "application/pdf"; else if (path.endsWith(".zip")) _contentType = "application/zip"; else if (path.endsWith(".gz")) _contentType = "application/x-gzip"; else _contentType = "text/plain"; setContentType(_contentType); } esp_err_t PsychicFileResponse::send() { esp_err_t err = ESP_OK; // just send small files directly size_t size = getContentLength(); if (size < FILE_CHUNK_SIZE) { uint8_t* buffer = (uint8_t*)malloc(size); if (buffer == NULL && size > 0) { ESP_LOGE(PH_TAG, "Unable to allocate %zu bytes to send chunk", size); httpd_resp_send_err(request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); return ESP_FAIL; } size_t readSize = _content.readBytes((char*)buffer, size); setContent(buffer, readSize); err = _response->send(); free(buffer); } else { /* Retrieve the pointer to scratch buffer for temporary storage */ char* chunk = (char*)malloc(FILE_CHUNK_SIZE); if (chunk == NULL) { ESP_LOGE(PH_TAG, "Unable to allocate %zu bytes to send chunk", (size_t)FILE_CHUNK_SIZE); httpd_resp_send_err(request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); return ESP_FAIL; } // now the headers sendHeaders(); size_t chunksize; do { /* Read file in chunks into the scratch buffer */ chunksize = _content.readBytes(chunk, FILE_CHUNK_SIZE); if (chunksize > 0) { err = sendChunk((uint8_t*)chunk, chunksize); if (err != ESP_OK) break; } /* Keep looping till the whole file is sent */ } while (chunksize != 0); // keep track of our memory free(chunk); if (err == ESP_OK) { ESP_LOGD(PH_TAG, "File sending complete"); finishChunking(); } } return err; } ================================================ FILE: src/PsychicFileResponse.h ================================================ #ifndef PsychicFileResponse_h #define PsychicFileResponse_h #include "PsychicCore.h" #include "PsychicResponse.h" class PsychicRequest; class PsychicFileResponse : public PsychicResponseDelegate { using File = fs::File; using FS = fs::FS; protected: File _content; void _setContentTypeFromPath(const String& path); public: PsychicFileResponse(PsychicResponse* response, FS& fs, const String& path, const String& contentType = String(), bool download = false); PsychicFileResponse(PsychicResponse* response, File content, const String& path, const String& contentType = String(), bool download = false); ~PsychicFileResponse(); esp_err_t send(); }; #endif // PsychicFileResponse_h ================================================ FILE: src/PsychicHandler.cpp ================================================ #include "PsychicHandler.h" PsychicHandler::PsychicHandler() { } PsychicHandler::~PsychicHandler() { delete _chain; // actual PsychicClient deletion handled by PsychicServer // for (PsychicClient *client : _clients) // delete(client); _clients.clear(); } PsychicHandler* PsychicHandler::addFilter(PsychicRequestFilterFunction fn) { _filters.push_back(fn); return this; } bool PsychicHandler::filter(PsychicRequest* request) { // run through our filter chain. for (auto& filter : _filters) { if (!filter(request)) { ESP_LOGD(PH_TAG, "Request %s refused by filter from handler", request->uri().c_str()); return false; } } return true; } void PsychicHandler::setSubprotocol(const String& subprotocol) { this->_subprotocol = subprotocol; } const char* PsychicHandler::getSubprotocol() const { return _subprotocol.c_str(); } PsychicClient* PsychicHandler::checkForNewClient(PsychicClient* client) { PsychicClient* c = PsychicHandler::getClient(client); if (c == NULL) { c = client; addClient(c); c->isNew = true; } else c->isNew = false; return c; } void PsychicHandler::checkForClosedClient(PsychicClient* client) { if (hasClient(client)) { closeCallback(client); removeClient(client); } } void PsychicHandler::addClient(PsychicClient* client) { _clients.push_back(client); } void PsychicHandler::removeClient(PsychicClient* client) { _clients.remove(client); } PsychicClient* PsychicHandler::getClient(int socket) { // make sure the server has it too. if (!_server->hasClient(socket)) return NULL; // what about us? for (PsychicClient* client : _clients) if (client->socket() == socket) return client; // nothing found. return NULL; } PsychicClient* PsychicHandler::getClient(PsychicClient* client) { return PsychicHandler::getClient(client->socket()); } bool PsychicHandler::hasClient(PsychicClient* socket) { return PsychicHandler::getClient(socket) != NULL; } const std::list& PsychicHandler::getClientList() { return _clients; } PsychicHandler* PsychicHandler::addMiddleware(PsychicMiddleware* middleware) { if (!_chain) { _chain = new PsychicMiddlewareChain(); } _chain->addMiddleware(middleware); return this; } PsychicHandler* PsychicHandler::addMiddleware(PsychicMiddlewareCallback fn) { if (!_chain) { _chain = new PsychicMiddlewareChain(); } _chain->addMiddleware(fn); return this; } void PsychicHandler::removeMiddleware(PsychicMiddleware* middleware) { if (_chain) { _chain->removeMiddleware(middleware); } } esp_err_t PsychicHandler::process(PsychicRequest* request) { if (!filter(request)) { return HTTPD_404_NOT_FOUND; } if (!canHandle(request)) { ESP_LOGD(PH_TAG, "Request %s refused by handler", request->uri().c_str()); return HTTPD_404_NOT_FOUND; } if (_chain) { return _chain->runChain(request, [this, request]() { return handleRequest(request, request->response()); }); } else { return handleRequest(request, request->response()); } } ================================================ FILE: src/PsychicHandler.h ================================================ #ifndef PsychicHandler_h #define PsychicHandler_h #include "PsychicCore.h" #include "PsychicRequest.h" class PsychicEndpoint; class PsychicHttpServer; class PsychicMiddleware; class PsychicMiddlewareChain; /* * HANDLER :: Can be attached to any endpoint or as a generic request handler. */ class PsychicHandler { friend PsychicEndpoint; protected: PsychicHttpServer* _server = nullptr; PsychicMiddlewareChain* _chain = nullptr; std::list _filters; String _subprotocol; std::list _clients; public: PsychicHandler(); virtual ~PsychicHandler(); virtual bool isWebSocket() { return false; }; void setSubprotocol(const String& subprotocol); const char* getSubprotocol() const; PsychicClient* checkForNewClient(PsychicClient* client); void checkForClosedClient(PsychicClient* client); virtual void addClient(PsychicClient* client); virtual void removeClient(PsychicClient* client); virtual PsychicClient* getClient(int socket); virtual PsychicClient* getClient(PsychicClient* client); virtual void openCallback(PsychicClient* client) {}; virtual void closeCallback(PsychicClient* client) {}; bool hasClient(PsychicClient* client); int count() { return _clients.size(); }; const std::list& getClientList(); // called to process this handler with its middleware chain and filers esp_err_t process(PsychicRequest* request); //bool filter(PsychicRequest* request); PsychicHandler* addFilter(PsychicRequestFilterFunction fn); bool filter(PsychicRequest* request); PsychicHandler* addMiddleware(PsychicMiddleware* middleware); PsychicHandler* addMiddleware(PsychicMiddlewareCallback fn); void removeMiddleware(PsychicMiddleware *middleware); // derived classes must implement these functions virtual bool canHandle(PsychicRequest* request) { return true; }; virtual esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) { return HTTPD_404_NOT_FOUND; }; }; #endif ================================================ FILE: src/PsychicHttp.h ================================================ #ifndef PsychicHttp_h #define PsychicHttp_h // #define ENABLE_ASYNC // This is something added in ESP-IDF 5.1.x where each request can be handled in its own thread #include "PsychicEndpoint.h" #include "PsychicEventSource.h" #include "PsychicFileResponse.h" #include "PsychicHandler.h" #include "PsychicHttpServer.h" #include "PsychicJson.h" #include "PsychicMiddleware.h" #include "PsychicMiddlewareChain.h" #include "PsychicMiddlewares.h" #include "PsychicRequest.h" #include "PsychicResponse.h" #include "PsychicStaticFileHandler.h" #include "PsychicStreamResponse.h" #include "PsychicUploadHandler.h" #include "PsychicVersion.h" #include "PsychicWebSocket.h" #include #ifdef ENABLE_ASYNC #include "async_worker.h" #endif // debugging library #ifdef PSY_USE_ARDUINO_TRACE #include #endif #endif /* PsychicHttp_h */ ================================================ FILE: src/PsychicHttpServer.cpp ================================================ #include "PsychicHttpServer.h" #include "PsychicEndpoint.h" #include "PsychicHandler.h" #include "PsychicJson.h" #include "PsychicStaticFileHandler.h" #include "PsychicWebHandler.h" #include "PsychicWebSocket.h" #include "esp_idf_version.h" #include "esp_netif.h" #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #define esp_netif_next_compat(n) esp_netif_next_unsafe(n) #else #define esp_netif_next_compat(n) esp_netif_next(n) #endif PsychicHttpServer::PsychicHttpServer(uint16_t port) { maxRequestBodySize = MAX_REQUEST_BODY_SIZE; maxUploadSize = MAX_UPLOAD_SIZE; defaultEndpoint = new PsychicEndpoint(this, HTTP_GET, ""); onNotFound(PsychicHttpServer::defaultNotFoundHandler); // for a regular server config = HTTPD_DEFAULT_CONFIG(); config.open_fn = PsychicHttpServer::openCallback; config.close_fn = PsychicHttpServer::closeCallback; config.global_user_ctx = this; config.global_user_ctx_free_fn = PsychicHttpServer::destroy; config.uri_match_fn = MATCH_WILDCARD; // new internal endpoint matching - do not change this!!! config.stack_size = 8192; // file I/O via VFS/LittleFS needs a deep call chain // our internal matching function for endpoints _uri_match_fn = MATCH_WILDCARD; // use this change the endpoint matching function. #ifdef ENABLE_ASYNC // It is advisable that httpd_config_t->max_open_sockets > MAX_ASYNC_REQUESTS // Why? This leaves at least one socket still available to handle // quick synchronous requests. Otherwise, all the sockets will // get taken by the long async handlers, and your server will no // longer be responsive. config.max_open_sockets = ASYNC_WORKER_COUNT + 1; config.lru_purge_enable = true; #endif setPort(port); } PsychicHttpServer::~PsychicHttpServer() { _esp_idf_endpoints.clear(); for (auto* client : _clients) delete (client); _clients.clear(); for (auto* endpoint : _endpoints) delete (endpoint); _endpoints.clear(); for (auto* handler : _handlers) delete (handler); _handlers.clear(); for (auto* rewrite : _rewrites) delete (rewrite); _rewrites.clear(); delete defaultEndpoint; delete _chain; } void PsychicHttpServer::destroy(void* ctx) { // do not release any resource for PsychicHttpServer in order to be able to restart it after stopping } void PsychicHttpServer::setPort(uint16_t port) { this->config.server_port = port; } uint16_t PsychicHttpServer::getPort() { return this->config.server_port; } static bool _netif_is_connected(esp_netif_t* netif) { if (!esp_netif_is_netif_up(netif)) return false; esp_netif_ip_info_t ip; if (esp_netif_get_ip_info(netif, &ip) != ESP_OK) return false; return ip.ip.addr != 0 && (ip.ip.addr & 0xFF) != 127; } bool PsychicHttpServer::isConnected() { for (esp_netif_t* netif = esp_netif_next_compat(nullptr); netif != nullptr; netif = esp_netif_next_compat(netif)) if (_netif_is_connected(netif)) return true; return false; } esp_err_t PsychicHttpServer::start() { if (_running) return ESP_OK; // starting without network will crash us if (!isConnected()) { ESP_LOGE(PH_TAG, "Server start failed - no network interface available."); return ESP_FAIL; } esp_err_t ret; #ifdef ENABLE_ASYNC // start workers start_async_req_workers(); #endif // one URI handler for each http_method config.max_uri_handlers = supported_methods.size() + _esp_idf_endpoints.size(); // fire it up. ret = _startServer(); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "Server start failed (%s)", esp_err_to_name(ret)); return ret; } // some handlers (aka websockets) need actual endpoints in esp-idf http_server for (auto& endpoint : _esp_idf_endpoints) { ESP_LOGD(PH_TAG, "Adding endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method)); // Register endpoint with ESP-IDF server esp_err_t ret = httpd_register_uri_handler(this->server, &endpoint); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret)); } // Register a handler for each http_method method - it will match all requests with that URI/method for (auto& method : supported_methods) { ESP_LOGD(PH_TAG, "Adding %s meta endpoint", http_method_str((http_method)method)); httpd_uri_t my_uri; my_uri.uri = "*"; my_uri.method = method; my_uri.handler = PsychicHttpServer::requestHandler; my_uri.is_websocket = false; my_uri.supported_subprotocol = ""; // Register endpoint with ESP-IDF server esp_err_t ret = httpd_register_uri_handler(this->server, &my_uri); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add endpoint failed (%s)", esp_err_to_name(ret)); } // Register handler ret = httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, PsychicHttpServer::notFoundHandler); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Add 404 handler failed (%s)", esp_err_to_name(ret)); ESP_LOGI(PH_TAG, "Server started on port %" PRIu16, getPort()); _running = true; return ret; } esp_err_t PsychicHttpServer::_startServer() { return httpd_start(&this->server, &this->config); } esp_err_t PsychicHttpServer::stop() { if (!_running) return ESP_OK; // some handlers (aka websockets) need actual endpoints in esp-idf http_server for (auto& endpoint : _esp_idf_endpoints) { ESP_LOGD(PH_TAG, "Removing endpoint %s | %s", endpoint.uri, http_method_str((http_method)endpoint.method)); // Unregister endpoint with ESP-IDF server esp_err_t ret = httpd_unregister_uri_handler(this->server, endpoint.uri, endpoint.method); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Removal of endpoint failed (%s)", esp_err_to_name(ret)); } // Unregister a handler for each http_method method - it will match all requests with that URI/method for (auto& method : supported_methods) { ESP_LOGD(PH_TAG, "Removing %s meta endpoint", http_method_str((http_method)method)); // Unregister endpoint with ESP-IDF server esp_err_t ret = httpd_unregister_uri_handler(this->server, "*", method); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Removal of endpoint failed (%s)", esp_err_to_name(ret)); } esp_err_t ret = _stopServer(); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "Server stop failed (%s)", esp_err_to_name(ret)); return ret; } ESP_LOGI(PH_TAG, "Server stopped"); _running = false; return ret; } esp_err_t PsychicHttpServer::_stopServer() { return httpd_stop(this->server); } void PsychicHttpServer::reset() { if (_running) stop(); for (auto* client : _clients) delete (client); _clients.clear(); for (auto* endpoint : _endpoints) delete (endpoint); _endpoints.clear(); for (auto* handler : _handlers) delete (handler); _handlers.clear(); for (auto* rewrite : _rewrites) delete (rewrite); _rewrites.clear(); _esp_idf_endpoints.clear(); delete _chain; _chain = nullptr; onNotFound(PsychicHttpServer::defaultNotFoundHandler); _onOpen = nullptr; _onClose = nullptr; } esp_err_t PsychicHttpServer::restart() { esp_err_t ret = ESP_OK; if (_running) { ret = stop(); if (ret != ESP_OK) return ret; } return start(); } httpd_uri_match_func_t PsychicHttpServer::getURIMatchFunction() { return _uri_match_fn; } void PsychicHttpServer::setURIMatchFunction(httpd_uri_match_func_t match_fn) { _uri_match_fn = match_fn; } PsychicHandler* PsychicHttpServer::addHandler(PsychicHandler* handler) { _handlers.push_back(handler); return handler; } void PsychicHttpServer::removeHandler(PsychicHandler* handler) { _handlers.remove(handler); delete handler; } PsychicRewrite* PsychicHttpServer::addRewrite(PsychicRewrite* rewrite) { _rewrites.push_back(rewrite); return rewrite; } void PsychicHttpServer::removeRewrite(PsychicRewrite* rewrite) { _rewrites.remove(rewrite); delete rewrite; } PsychicRewrite* PsychicHttpServer::rewrite(const char* from, const char* to) { return addRewrite(new PsychicRewrite(from, to)); } PsychicEndpoint* PsychicHttpServer::on(const char* uri) { return on(uri, HTTP_GET); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method) { PsychicWebHandler* handler = new PsychicWebHandler(); return on(uri, method, handler); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHandler* handler) { return on(uri, HTTP_GET, handler); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicHandler* handler) { // make our endpoint PsychicEndpoint* endpoint = new PsychicEndpoint(this, method, uri); // set our handler endpoint->setHandler(handler); // websockets need a real endpoint in esp-idf if (handler->isWebSocket()) { if (_running) ESP_LOGW(PH_TAG, "WebSocket handler for '%s' registered after server started — it will not work. Call server.on() before server.start().", uri); // URI handler structure httpd_uri_t my_uri; my_uri.uri = uri; my_uri.method = HTTP_GET; my_uri.handler = PsychicEndpoint::requestCallback; my_uri.user_ctx = endpoint; my_uri.is_websocket = handler->isWebSocket(); my_uri.supported_subprotocol = handler->getSubprotocol(); // save it to our 'real' handlers for later. _esp_idf_endpoints.push_back(my_uri); } // if this is a method we haven't added yet, do it. if (method != HTTP_ANY) { if (!(std::find(supported_methods.begin(), supported_methods.end(), (http_method)method) != supported_methods.end())) { ESP_LOGD(PH_TAG, "Adding %s to server.supported_methods", http_method_str((http_method)method)); supported_methods.push_back((http_method)method); } } // add it to our meta endpoints _endpoints.push_back(endpoint); return endpoint; } PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicHttpRequestCallback fn) { return on(uri, HTTP_GET, fn); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicHttpRequestCallback fn) { // these basic requests need a basic web handler PsychicWebHandler* handler = new PsychicWebHandler(); handler->onRequest(fn); return on(uri, method, handler); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, PsychicJsonRequestCallback fn) { return on(uri, HTTP_GET, fn); } PsychicEndpoint* PsychicHttpServer::on(const char* uri, int method, PsychicJsonRequestCallback fn) { // these basic requests need a basic web handler PsychicJsonHandler* handler = new PsychicJsonHandler(); handler->onRequest(fn); return on(uri, method, handler); } bool PsychicHttpServer::removeEndpoint(const char* uri, int method) { for (auto* endpoint : _endpoints) { if (endpoint->uri().equals(uri) && method == endpoint->_method) return removeEndpoint(endpoint); } return false; } bool PsychicHttpServer::removeEndpoint(PsychicEndpoint* endpoint) { // unregister any ESP-IDF native handler (e.g. WebSocket) for this endpoint for (auto it = _esp_idf_endpoints.begin(); it != _esp_idf_endpoints.end();) { if (endpoint->uri().equals(it->uri) && endpoint->_method == (int)it->method) { ESP_LOGD(PH_TAG, "Unregistering endpoint %s | %s", it->uri, http_method_str((http_method)it->method)); if (_running) { esp_err_t ret = httpd_unregister_uri_handler(this->server, it->uri, it->method); if (ret != ESP_OK) ESP_LOGE(PH_TAG, "Remove endpoint failed (%s)", esp_err_to_name(ret)); } it = _esp_idf_endpoints.erase(it); } else { ++it; } } _endpoints.remove(endpoint); delete endpoint; return true; } PsychicHttpServer* PsychicHttpServer::addFilter(PsychicRequestFilterFunction fn) { _filters.push_back(fn); return this; } bool PsychicHttpServer::_filter(PsychicRequest* request) { // run through our filter chain. for (auto& filter : _filters) { if (!filter(request)) return false; } return true; } PsychicHttpServer* PsychicHttpServer::addMiddleware(PsychicMiddleware* middleware) { if (!_chain) { _chain = new PsychicMiddlewareChain(); } _chain->addMiddleware(middleware); return this; } PsychicHttpServer* PsychicHttpServer::addMiddleware(PsychicMiddlewareCallback fn) { if (!_chain) { _chain = new PsychicMiddlewareChain(); } _chain->addMiddleware(fn); return this; } void PsychicHttpServer::removeMiddleware(PsychicMiddleware* middleware) { if (_chain) { _chain->removeMiddleware(middleware); } } void PsychicHttpServer::onNotFound(PsychicHttpRequestCallback fn) { PsychicWebHandler* handler = new PsychicWebHandler(); handler->onRequest(fn == nullptr ? PsychicHttpServer::defaultNotFoundHandler : fn); this->defaultEndpoint->setHandler(handler); } bool PsychicHttpServer::_rewriteRequest(PsychicRequest* request) { for (auto* r : _rewrites) { if (r->match(request)) { request->_setUri(r->toUrl().c_str()); return true; } } return false; } esp_err_t PsychicHttpServer::requestHandler(httpd_req_t* req) { PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle); PsychicRequest request(server, req); // process any URL rewrites server->_rewriteRequest(&request); // run it through our global server filter list if (!server->_filter(&request)) { ESP_LOGD(PH_TAG, "Request %s refused by global filter", request.uri().c_str()); return request.response()->send(400); } // then runs the request through the filter chain esp_err_t ret; if (server->_chain) { ret = server->_chain->runChain(&request, [server, &request]() { return server->_process(&request); }); } else { ret = server->_process(&request); } ESP_LOGD(PH_TAG, "Request %s processed by global middleware: %s", request.uri().c_str(), esp_err_to_name(ret)); if (ret == HTTPD_404_NOT_FOUND) { return PsychicHttpServer::notFoundHandler(req, HTTPD_404_NOT_FOUND); } return ret; } esp_err_t PsychicHttpServer::_process(PsychicRequest* request) { // loop through our endpoints and see if anyone wants it. for (auto* endpoint : _endpoints) { if (endpoint->matches(request->uri().c_str())) { if (endpoint->_method == request->method() || endpoint->_method == HTTP_ANY) { request->setEndpoint(endpoint); return endpoint->process(request); } } } // loop through our global handlers and see if anyone wants it for (auto* handler : _handlers) { esp_err_t ret = handler->process(request); if (ret != HTTPD_404_NOT_FOUND) return ret; } return HTTPD_404_NOT_FOUND; } esp_err_t PsychicHttpServer::notFoundHandler(httpd_req_t* req, httpd_err_code_t err) { PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(req->handle); PsychicRequest request(server, req); // pull up our default handler / endpoint PsychicHandler* handler = server->defaultEndpoint->handler(); if (!handler) return request.response()->send(404); esp_err_t ret = handler->process(&request); if (ret != HTTPD_404_NOT_FOUND) return ret; // not sure how we got this far. return request.response()->send(404); } esp_err_t PsychicHttpServer::defaultNotFoundHandler(PsychicRequest* request, PsychicResponse* response) { return response->send(404, "text/html", "That URI does not exist."); } void PsychicHttpServer::onOpen(PsychicClientCallback handler) { this->_onOpen = handler; } esp_err_t PsychicHttpServer::openCallback(httpd_handle_t hd, int sockfd) { ESP_LOGD(PH_TAG, "New client connected %d", sockfd); // get our global server reference PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); // lookup our client PsychicClient* client = server->getClient(sockfd); if (client == NULL) { client = new PsychicClient(hd, sockfd); server->addClient(client); } // user callback if (server->_onOpen != NULL) server->_onOpen(client); return ESP_OK; } void PsychicHttpServer::onClose(PsychicClientCallback handler) { this->_onClose = handler; } void PsychicHttpServer::closeCallback(httpd_handle_t hd, int sockfd) { ESP_LOGD(PH_TAG, "Client disconnected %d", sockfd); PsychicHttpServer* server = (PsychicHttpServer*)httpd_get_global_user_ctx(hd); // lookup our client PsychicClient* client = server->getClient(sockfd); if (client != NULL) { // give our handlers a chance to handle a disconnect first for (PsychicEndpoint* endpoint : server->_endpoints) { PsychicHandler* handler = endpoint->handler(); if (handler != nullptr) handler->checkForClosedClient(client); } // do we have a callback attached? if (server->_onClose != NULL) server->_onClose(client); // remove it from our list server->removeClient(client); } else ESP_LOGE(PH_TAG, "No client record %d", sockfd); // finally close it out. close(sockfd); } PsychicStaticFileHandler* PsychicHttpServer::serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control) { PsychicStaticFileHandler* handler = new PsychicStaticFileHandler(uri, fs, path, cache_control); this->addHandler(handler); return handler; } void PsychicHttpServer::addClient(PsychicClient* client) { _clients.push_back(client); } void PsychicHttpServer::removeClient(PsychicClient* client) { _clients.remove(client); delete client; } PsychicClient* PsychicHttpServer::getClient(int socket) { for (PsychicClient* client : _clients) if (client->socket() == socket) return client; return NULL; } PsychicClient* PsychicHttpServer::getClient(httpd_req_t* req) { return getClient(httpd_req_to_sockfd(req)); } bool PsychicHttpServer::hasClient(int socket) { return getClient(socket) != NULL; } const std::list& PsychicHttpServer::getClientList() { return _clients; } static esp_netif_t* _find_netif_by_ip(const IPAddress& addr) { for (esp_netif_t* netif = esp_netif_next_compat(nullptr); netif != nullptr; netif = esp_netif_next_compat(netif)) { esp_netif_ip_info_t ip; if (esp_netif_get_ip_info(netif, &ip) != ESP_OK) continue; if (IPAddress(ip.ip.addr) == addr) return netif; } return nullptr; } bool ON_STA_FILTER(PsychicRequest* request) { esp_netif_t* netif = _find_netif_by_ip(request->client()->localIP()); if (netif == nullptr) return false; return !(esp_netif_get_flags(netif) & ESP_NETIF_DHCP_SERVER); } bool ON_AP_FILTER(PsychicRequest* request) { esp_netif_t* netif = _find_netif_by_ip(request->client()->localIP()); if (netif == nullptr) return false; return (esp_netif_get_flags(netif) & ESP_NETIF_DHCP_SERVER) != 0; } String urlDecode(const char* encoded) { size_t length = strlen(encoded); char* decoded = (char*)malloc(length + 1); if (!decoded) { return ""; } size_t i, j = 0; for (i = 0; i < length; ++i) { if (encoded[i] == '%' && i + 2 < length && isxdigit(encoded[i + 1]) && isxdigit(encoded[i + 2])) { // Valid percent-encoded sequence int hex; sscanf(encoded + i + 1, "%2x", &hex); decoded[j++] = (char)hex; i += 2; // Skip the two hexadecimal characters } else if (encoded[i] == '+') { // Convert '+' to space decoded[j++] = ' '; } else { // Copy other characters as they are decoded[j++] = encoded[i]; } } decoded[j] = '\0'; // Null-terminate the decoded string String output(decoded); free(decoded); return output; } bool psychic_uri_match_simple(const char* uri1, const char* uri2, size_t len2) { return strlen(uri1) == len2 && // First match lengths (strncmp(uri1, uri2, len2) == 0); // Then match actual URIs } #ifdef PSY_ENABLE_REGEX bool psychic_uri_match_regex(const char* uri1, const char* uri2, size_t len2) { try { std::regex pattern(uri1); std::smatch matches; std::string s(uri2, len2); return std::regex_search(s, matches, pattern); } catch (const std::regex_error& e) { ESP_LOGE(PH_TAG, "Invalid regex pattern '%s': %s", uri1, e.what()); return false; } } #endif ================================================ FILE: src/PsychicHttpServer.h ================================================ #ifndef PsychicHttpServer_h #define PsychicHttpServer_h #include "PsychicClient.h" #include "PsychicCore.h" #include "PsychicHandler.h" #include "PsychicMiddleware.h" #include "PsychicMiddlewareChain.h" #include "PsychicRewrite.h" #ifdef PSY_ENABLE_REGEX #include #endif #ifndef HTTP_ANY #define HTTP_ANY INT_MAX #endif class PsychicEndpoint; class PsychicHandler; class PsychicStaticFileHandler; class PsychicHttpServer { protected: std::list _esp_idf_endpoints; std::list _endpoints; std::list _handlers; std::list _clients; std::list _rewrites; std::list _filters; PsychicClientCallback _onOpen = nullptr; PsychicClientCallback _onClose = nullptr; PsychicMiddlewareChain* _chain = nullptr; esp_err_t _start(); virtual esp_err_t _startServer(); virtual esp_err_t _stopServer(); bool _running = false; httpd_uri_match_func_t _uri_match_fn = nullptr; bool _rewriteRequest(PsychicRequest* request); esp_err_t _process(PsychicRequest* request); bool _filter(PsychicRequest* request); public: PsychicHttpServer(uint16_t port = 80); virtual ~PsychicHttpServer(); // what methods to support std::list supported_methods = { HTTP_GET, HTTP_POST, HTTP_DELETE, HTTP_HEAD, HTTP_PUT, HTTP_OPTIONS}; // esp-idf specific stuff httpd_handle_t server; httpd_config_t config; // some limits on what we will accept unsigned long maxUploadSize; unsigned long maxRequestBodySize; PsychicEndpoint* defaultEndpoint; static void destroy(void* ctx); virtual void setPort(uint16_t port); virtual uint16_t getPort(); // stub functions to allow us to use a pointer to PsychicHttpServer that is really a PsychicHttpsServer // for runtime selection of ssl or no ssl virtual void setCertificate(const char* cert, const char* private_key) {} virtual void setCertificate(const uint8_t* cert, size_t cert_size, const uint8_t* private_key, size_t private_key_size) {} bool isConnected(); bool isRunning() { return _running; } esp_err_t begin() { return start(); } esp_err_t end() { return stop(); } esp_err_t start(); esp_err_t stop(); esp_err_t restart(); void reset(); httpd_uri_match_func_t getURIMatchFunction(); void setURIMatchFunction(httpd_uri_match_func_t match_fn); PsychicRewrite* addRewrite(PsychicRewrite* rewrite); void removeRewrite(PsychicRewrite* rewrite); PsychicRewrite* rewrite(const char* from, const char* to); PsychicHandler* addHandler(PsychicHandler* handler); void removeHandler(PsychicHandler* handler); void addClient(PsychicClient* client); void removeClient(PsychicClient* client); PsychicClient* getClient(int socket); PsychicClient* getClient(httpd_req_t* req); bool hasClient(int socket); int count() { return _clients.size(); }; const std::list& getClientList(); PsychicEndpoint* on(const char* uri); PsychicEndpoint* on(const char* uri, int method); PsychicEndpoint* on(const char* uri, PsychicHandler* handler); PsychicEndpoint* on(const char* uri, int method, PsychicHandler* handler); PsychicEndpoint* on(const char* uri, PsychicHttpRequestCallback onRequest); PsychicEndpoint* on(const char* uri, int method, PsychicHttpRequestCallback onRequest); PsychicEndpoint* on(const char* uri, PsychicJsonRequestCallback onRequest); PsychicEndpoint* on(const char* uri, int method, PsychicJsonRequestCallback onRequest); bool removeEndpoint(const char* uri, int method); bool removeEndpoint(PsychicEndpoint* endpoint); PsychicHttpServer* addFilter(PsychicRequestFilterFunction fn); PsychicHttpServer* addMiddleware(PsychicMiddleware* middleware); PsychicHttpServer* addMiddleware(PsychicMiddlewareCallback fn); void removeMiddleware(PsychicMiddleware* middleware); static esp_err_t requestHandler(httpd_req_t* req); static esp_err_t notFoundHandler(httpd_req_t* req, httpd_err_code_t err); static esp_err_t defaultNotFoundHandler(PsychicRequest* request, PsychicResponse* response); static esp_err_t openCallback(httpd_handle_t hd, int sockfd); static void closeCallback(httpd_handle_t hd, int sockfd); void onNotFound(PsychicHttpRequestCallback fn); void onOpen(PsychicClientCallback handler); void onClose(PsychicClientCallback handler); PsychicStaticFileHandler* serveStatic(const char* uri, fs::FS& fs, const char* path, const char* cache_control = NULL); }; bool ON_STA_FILTER(PsychicRequest* request); bool ON_AP_FILTER(PsychicRequest* request); // URI matching functions bool psychic_uri_match_simple(const char* uri1, const char* uri2, size_t len2); #define MATCH_SIMPLE psychic_uri_match_simple #define MATCH_WILDCARD httpd_uri_match_wildcard #ifdef PSY_ENABLE_REGEX bool psychic_uri_match_regex(const char* uri1, const char* uri2, size_t len2); #define MATCH_REGEX psychic_uri_match_regex #endif #endif // PsychicHttpServer_h ================================================ FILE: src/PsychicHttpsServer.cpp ================================================ #include "PsychicHttpsServer.h" #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE PsychicHttpsServer::PsychicHttpsServer(uint16_t port) : PsychicHttpServer(port) { // for a SSL server ssl_config = HTTPD_SSL_CONFIG_DEFAULT(); ssl_config.httpd.open_fn = PsychicHttpServer::openCallback; ssl_config.httpd.close_fn = PsychicHttpServer::closeCallback; ssl_config.httpd.uri_match_fn = httpd_uri_match_wildcard; ssl_config.httpd.global_user_ctx = this; ssl_config.httpd.global_user_ctx_free_fn = destroy; ssl_config.httpd.max_uri_handlers = 20; // each SSL connection takes about 45kb of heap // a barebones sketch with PsychicHttp has ~150kb of heap available // if we set it higher than 2 and use all the connections, we get lots of memory errors. // not to mention there is no heap left over for the program itself. ssl_config.httpd.max_open_sockets = 2; setPort(port); } PsychicHttpsServer::~PsychicHttpsServer() {} void PsychicHttpsServer::setPort(uint16_t port) { this->ssl_config.port_secure = port; } uint16_t PsychicHttpsServer::getPort() { return this->ssl_config.port_secure; } void PsychicHttpsServer::setCertificate(const uint8_t* cert, size_t cert_size, const uint8_t* private_key, size_t private_key_size) { if (cert) { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2) this->ssl_config.servercert = cert; this->ssl_config.servercert_len = cert_size; #else this->ssl_config.cacert_pem = cert; this->ssl_config.cacert_len = cert_size; #endif } if (private_key) { this->ssl_config.prvtkey_pem = private_key; this->ssl_config.prvtkey_len = private_key_size; } } esp_err_t PsychicHttpsServer::_startServer() { // sync fields that start() calculated into our ssl_config.httpd before handing off ssl_config.httpd.max_uri_handlers = config.max_uri_handlers; ssl_config.httpd.stack_size = config.stack_size; return httpd_ssl_start(&this->server, &this->ssl_config); } esp_err_t PsychicHttpsServer::_stopServer() { #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 2) return httpd_ssl_stop(this->server); #else httpd_ssl_stop(this->server); return ESP_OK; #endif } #endif // CONFIG_ESP_HTTPS_SERVER_ENABLE ================================================ FILE: src/PsychicHttpsServer.h ================================================ #ifndef PsychicHttpsServer_h #define PsychicHttpsServer_h #include #ifdef CONFIG_ESP_HTTPS_SERVER_ENABLE #include "PsychicCore.h" #include "PsychicHttpServer.h" #include #if !CONFIG_HTTPD_WS_SUPPORT #error PsychicHttpsServer cannot be used unless HTTPD_WS_SUPPORT is enabled in esp-http-server component configuration #endif #ifndef PSY_ENABLE_SSL #define PSY_ENABLE_SSL // you can use this define in your code to enable/disable these features #endif class PsychicHttpsServer : public PsychicHttpServer { protected: virtual esp_err_t _startServer() override final; virtual esp_err_t _stopServer() override final; public: PsychicHttpsServer(uint16_t port = 443); ~PsychicHttpsServer(); httpd_ssl_config_t ssl_config; // using PsychicHttpServer::listen; // keep the regular version virtual void setPort(uint16_t port) override final; virtual uint16_t getPort() override final; // Pointer to certificate data in PEM format void setCertificate(const char* cert, const char* private_key) override final { setCertificate((const uint8_t*)cert, strlen(cert) + 1, (const uint8_t*)private_key, private_key ? strlen(private_key) + 1 : 0); } // Pointer to certificate data in PEM or DER format. PEM-format must have a terminating NULL-character. DER-format requires the length to be passed in certSize and keySize. void setCertificate(const uint8_t* cert, size_t cert_size, const uint8_t* private_key, size_t private_key_size) override final; }; #else #warning ESP-IDF https server support not enabled. #endif // CONFIG_ESP_HTTPS_SERVER_ENABLE #endif // PsychicHttpsServer_h ================================================ FILE: src/PsychicJson.cpp ================================================ #include "PsychicJson.h" #ifdef ARDUINOJSON_6_COMPATIBILITY PsychicJsonResponse::PsychicJsonResponse(PsychicResponse* response, bool isArray, size_t maxJsonBufferSize) : __response(response), _jsonBuffer(maxJsonBufferSize) { response->setContentType(JSON_MIMETYPE); if (isArray) _root = _jsonBuffer.createNestedArray(); else _root = _jsonBuffer.createNestedObject(); } #else PsychicJsonResponse::PsychicJsonResponse(PsychicResponse* response, bool isArray) : PsychicResponseDelegate(response) { setContentType(JSON_MIMETYPE); if (isArray) _root = _jsonBuffer.add(); else _root = _jsonBuffer.add(); } #endif JsonVariant& PsychicJsonResponse::getRoot() { return _root; } size_t PsychicJsonResponse::getLength() { return measureJson(_root); } esp_err_t PsychicJsonResponse::send() { esp_err_t err = ESP_OK; size_t length = getLength(); size_t buffer_size; char* buffer; // how big of a buffer do we want? if (length < JSON_BUFFER_SIZE) buffer_size = length + 1; else buffer_size = JSON_BUFFER_SIZE; buffer = (char*)malloc(buffer_size); if (buffer == NULL) { return error(HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); } // send it in one shot or no? if (length < JSON_BUFFER_SIZE) { serializeJson(_root, buffer, buffer_size); setContent((uint8_t*)buffer, length); setContentType(JSON_MIMETYPE); err = PsychicResponseDelegate::send(); } else { // helper class that acts as a stream to print chunked responses ChunkPrinter dest(_response, (uint8_t*)buffer, buffer_size); // keep our headers sendHeaders(); serializeJson(_root, dest); // send the last bits dest.flush(); // done with our chunked response too err = finishChunking(); } // let the buffer go free(buffer); return err; } #ifdef ARDUINOJSON_6_COMPATIBILITY PsychicJsonHandler::PsychicJsonHandler(size_t maxJsonBufferSize) : _onRequest(NULL), _maxJsonBufferSize(maxJsonBufferSize) {}; PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize) : _onRequest(onRequest), _maxJsonBufferSize(maxJsonBufferSize) { } #else PsychicJsonHandler::PsychicJsonHandler() : _onRequest(NULL) {}; PsychicJsonHandler::PsychicJsonHandler(PsychicJsonRequestCallback onRequest) : _onRequest(onRequest) { } #endif void PsychicJsonHandler::onRequest(PsychicJsonRequestCallback fn) { _onRequest = fn; } esp_err_t PsychicJsonHandler::handleRequest(PsychicRequest* request, PsychicResponse* response) { // process basic stuff PsychicWebHandler::handleRequest(request, response); if (_onRequest) { #ifdef ARDUINOJSON_6_COMPATIBILITY DynamicJsonDocument jsonBuffer(this->_maxJsonBufferSize); DeserializationError error = deserializeJson(jsonBuffer, request->body()); if (error) return response->send(400); JsonVariant json = jsonBuffer.as(); #else JsonDocument jsonBuffer; DeserializationError error = deserializeJson(jsonBuffer, request->body()); if (error) return response->send(400); JsonVariant json = jsonBuffer.as(); #endif return _onRequest(request, response, json); } else return response->send(500); } ================================================ FILE: src/PsychicJson.h ================================================ // PsychicJson.h /* Async Response to use with ArduinoJson and AsyncWebServer Written by Andrew Melvin (SticilFace) with help from me-no-dev and BBlanchon. Ported to PsychicHttp by Zach Hoeken */ #ifndef PSYCHIC_JSON_H_ #define PSYCHIC_JSON_H_ #include "ChunkPrinter.h" #include "PsychicRequest.h" #include "PsychicWebHandler.h" #include #if ARDUINOJSON_VERSION_MAJOR == 6 #define ARDUINOJSON_6_COMPATIBILITY #ifndef DYNAMIC_JSON_DOCUMENT_SIZE #define DYNAMIC_JSON_DOCUMENT_SIZE 4096 #endif #endif #ifndef JSON_BUFFER_SIZE #define JSON_BUFFER_SIZE 4 * 1024 #endif constexpr const char* JSON_MIMETYPE = "application/json"; /* * Json Response * */ class PsychicJsonResponse : public PsychicResponseDelegate { protected: #ifdef ARDUINOJSON_5_COMPATIBILITY DynamicJsonBuffer _jsonBuffer; #elif ARDUINOJSON_VERSION_MAJOR == 6 DynamicJsonDocument _jsonBuffer; #else JsonDocument _jsonBuffer; #endif JsonVariant _root; size_t _contentLength; public: #ifdef ARDUINOJSON_5_COMPATIBILITY PsychicJsonResponse(PsychicResponse* response, bool isArray = false); #elif ARDUINOJSON_VERSION_MAJOR == 6 PsychicJsonResponse(PsychicResponse* response, bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); #else PsychicJsonResponse(PsychicResponse* response, bool isArray = false); #endif ~PsychicJsonResponse() { } JsonVariant& getRoot(); size_t getLength(); esp_err_t send(); }; class PsychicJsonHandler : public PsychicWebHandler { protected: PsychicJsonRequestCallback _onRequest; #if ARDUINOJSON_VERSION_MAJOR == 6 const size_t _maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE; #endif public: #ifdef ARDUINOJSON_5_COMPATIBILITY PsychicJsonHandler(); PsychicJsonHandler(PsychicJsonRequestCallback onRequest); #elif ARDUINOJSON_VERSION_MAJOR == 6 PsychicJsonHandler(size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); PsychicJsonHandler(PsychicJsonRequestCallback onRequest, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE); #else PsychicJsonHandler(); PsychicJsonHandler(PsychicJsonRequestCallback onRequest); #endif void onRequest(PsychicJsonRequestCallback fn); virtual esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override; }; #endif ================================================ FILE: src/PsychicMiddleware.cpp ================================================ #include "PsychicMiddleware.h" esp_err_t PsychicMiddlewareFunction::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) { return _fn(request, request->response(), next); } ================================================ FILE: src/PsychicMiddleware.h ================================================ #ifndef PsychicMiddleware_h #define PsychicMiddleware_h #include "PsychicCore.h" #include "PsychicRequest.h" #include "PsychicResponse.h" class PsychicMiddlewareChain; /* * PsychicMiddleware :: fancy callback wrapper for handling requests and responses. * */ class PsychicMiddleware { public: virtual ~PsychicMiddleware() {} virtual esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) { return next(); } private: friend PsychicMiddlewareChain; bool _freeOnRemoval = false; }; class PsychicMiddlewareFunction : public PsychicMiddleware { public: PsychicMiddlewareFunction(PsychicMiddlewareCallback fn) : _fn(fn) { assert(_fn); } esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override; protected: PsychicMiddlewareCallback _fn; }; #endif ================================================ FILE: src/PsychicMiddlewareChain.cpp ================================================ #include "PsychicMiddlewareChain.h" PsychicMiddlewareChain::~PsychicMiddlewareChain() { for (auto middleware : _middleware) if (middleware->_freeOnRemoval) delete middleware; _middleware.clear(); } void PsychicMiddlewareChain::addMiddleware(PsychicMiddleware* middleware) { _middleware.push_back(middleware); } void PsychicMiddlewareChain::addMiddleware(PsychicMiddlewareCallback fn) { PsychicMiddlewareFunction* closure = new PsychicMiddlewareFunction(fn); closure->_freeOnRemoval = true; _middleware.push_back(closure); } void PsychicMiddlewareChain::removeMiddleware(PsychicMiddleware* middleware) { _middleware.remove(middleware); if (middleware->_freeOnRemoval) delete middleware; } esp_err_t PsychicMiddlewareChain::runChain(PsychicRequest* request, PsychicMiddlewareNext finalizer) { if (_middleware.size() == 0) return finalizer(); PsychicMiddlewareNext next; std::list::iterator it = _middleware.begin(); next = [this, &next, &it, request, finalizer]() { if (it == _middleware.end()) return finalizer(); PsychicMiddleware* m = *it; it++; return m->run(request, request->response(), next); }; return next(); } ================================================ FILE: src/PsychicMiddlewareChain.h ================================================ #ifndef PsychicMiddlewareChain_h #define PsychicMiddlewareChain_h #include "PsychicCore.h" #include "PsychicMiddleware.h" #include "PsychicRequest.h" #include "PsychicResponse.h" /* * PsychicMiddlewareChain - handle tracking and executing our chain of middleware objects * */ class PsychicMiddlewareChain { public: virtual ~PsychicMiddlewareChain(); void addMiddleware(PsychicMiddleware* middleware); void addMiddleware(PsychicMiddlewareCallback fn); void removeMiddleware(PsychicMiddleware* middleware); esp_err_t runChain(PsychicRequest* request, PsychicMiddlewareNext finalizer); protected: std::list _middleware; }; #endif ================================================ FILE: src/PsychicMiddlewares.cpp ================================================ #include "PsychicMiddlewares.h" void LoggingMiddleware::setOutput(Print &output) { _out = &output; } esp_err_t LoggingMiddleware::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) { _out->print("* Connection from "); _out->print(request->client()->remoteIP().toString()); _out->print(":"); _out->println(request->client()->remotePort()); _out->print("> "); _out->print(request->methodStr()); _out->print(" "); _out->print(request->uri()); _out->print(" "); _out->println(request->version()); // TODO: find a way to collect all headers // int n = request->headerCount(); // for (int i = 0; i < n; i++) { // String v = server.header(i); // if (!v.isEmpty()) { // // because these 2 are always there, eventually empty: "Authorization", "If-None-Match" // _out->print("< "); // _out->print(server.headerName(i)); // _out->print(": "); // _out->println(server.header(i)); // } // } _out->println(">"); esp_err_t ret = next(); if (ret != HTTPD_404_NOT_FOUND) { _out->println("* Processed!"); _out->print("< "); _out->print(response->version()); _out->print(" "); _out->print(response->getCode()); _out->print(" "); _out->println(http_status_reason(response->getCode())); // iterate over response->headers() std::list::iterator it = response->headers().begin(); while (it != response->headers().end()) { HTTPHeader h = *it; _out->print("< "); _out->print(h.field); _out->print(": "); _out->println(h.value); it++; } _out->println("<"); } else { _out->println("* Not processed!"); } return ret; } AuthenticationMiddleware& AuthenticationMiddleware::setUsername(const char* username) { _username = username; return *this; } AuthenticationMiddleware& AuthenticationMiddleware::setPassword(const char* password) { _password = password; return *this; } AuthenticationMiddleware& AuthenticationMiddleware::setRealm(const char* realm) { _realm = realm; return *this; } AuthenticationMiddleware& AuthenticationMiddleware::setAuthMethod(HTTPAuthMethod method) { _method = method; return *this; } AuthenticationMiddleware& AuthenticationMiddleware::setAuthFailureMessage(const char* message) { _authFailMsg = message; return *this; } bool AuthenticationMiddleware::isAllowed(PsychicRequest* request) const { if (!_username.isEmpty() && !_password.isEmpty()) { return request->authenticate(_username.c_str(), _password.c_str()); } return true; } esp_err_t AuthenticationMiddleware::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) { bool authenticationRequired = false; if (!_username.isEmpty() && !_password.isEmpty()) { authenticationRequired = !request->authenticate(_username.c_str(), _password.c_str()); } if (authenticationRequired) { return request->requestAuthentication(_method, _realm.c_str(), _authFailMsg.c_str()); } else { return next(); } } CorsMiddleware& CorsMiddleware::setOrigin(const char* origin) { _origin = origin; return *this; } CorsMiddleware& CorsMiddleware::setMethods(const char* methods) { _methods = methods; return *this; } CorsMiddleware& CorsMiddleware::setHeaders(const char* headers) { _headers = headers; return *this; } CorsMiddleware& CorsMiddleware::setAllowCredentials(bool credentials) { _credentials = credentials; return *this; } CorsMiddleware& CorsMiddleware::setMaxAge(uint32_t seconds) { _maxAge = seconds; return *this; } void CorsMiddleware::addCORSHeaders(PsychicResponse* response) { response->addHeader("Access-Control-Allow-Origin", _origin.c_str()); response->addHeader("Access-Control-Allow-Methods", _methods.c_str()); response->addHeader("Access-Control-Allow-Headers", _headers.c_str()); response->addHeader("Access-Control-Allow-Credentials", _credentials ? "true" : "false"); response->addHeader("Access-Control-Max-Age", String(_maxAge).c_str()); } esp_err_t CorsMiddleware::run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) { if (request->hasHeader("Origin")) { addCORSHeaders(response); if (request->method() == HTTP_OPTIONS) { return response->send(200); } } return next(); } ================================================ FILE: src/PsychicMiddlewares.h ================================================ #ifndef PsychicMiddlewares_h #define PsychicMiddlewares_h #include "PsychicMiddleware.h" #include #include // curl-like logging middleware class LoggingMiddleware : public PsychicMiddleware { public: void setOutput(Print& output); esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override; private: Print* _out; }; class AuthenticationMiddleware : public PsychicMiddleware { public: AuthenticationMiddleware& setUsername(const char* username); AuthenticationMiddleware& setPassword(const char* password); AuthenticationMiddleware& setRealm(const char* realm); AuthenticationMiddleware& setAuthMethod(HTTPAuthMethod method); AuthenticationMiddleware& setAuthFailureMessage(const char* message); const String& getUsername() const { return _username; } const String& getPassword() const { return _password; } const String& getRealm() const { return _realm; } HTTPAuthMethod getAuthMethod() const { return _method; } const String& getAuthFailureMessage() const { return _authFailMsg; } bool isAllowed(PsychicRequest* request) const; esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override; private: String _username; String _password; String _realm; HTTPAuthMethod _method = BASIC_AUTH; String _authFailMsg; }; class CorsMiddleware : public PsychicMiddleware { public: CorsMiddleware& setOrigin(const char* origin); CorsMiddleware& setMethods(const char* methods); CorsMiddleware& setHeaders(const char* headers); CorsMiddleware& setAllowCredentials(bool credentials); CorsMiddleware& setMaxAge(uint32_t seconds); const String& getOrigin() const { return _origin; } const String& getMethods() const { return _methods; } const String& getHeaders() const { return _headers; } bool getAllowCredentials() const { return _credentials; } uint32_t getMaxAge() const { return _maxAge; } void addCORSHeaders(PsychicResponse* response); esp_err_t run(PsychicRequest* request, PsychicResponse* response, PsychicMiddlewareNext next) override; private: String _origin = "*"; String _methods = "*"; String _headers = "*"; bool _credentials = true; uint32_t _maxAge = 86400; }; #endif ================================================ FILE: src/PsychicRequest.cpp ================================================ #include "PsychicRequest.h" #include "MultipartProcessor.h" #include "PsychicHttpServer.h" #include "http_status.h" PsychicRequest::PsychicRequest(PsychicHttpServer* server, httpd_req_t* req) : _server(server), _req(req), _endpoint(nullptr), _method(HTTP_GET), _uri(""), _query(""), _body(""), _tempObject(nullptr) { // load up our client. this->_client = server->getClient(req); // handle our session data if (req->sess_ctx != NULL) this->_session = (SessionData*)req->sess_ctx; else { this->_session = new SessionData(); req->sess_ctx = this->_session; } // callback for freeing the session later req->free_ctx = this->freeSession; // load and parse our uri. this->_setUri(this->_req->uri); _response = new PsychicResponse(this); } PsychicRequest::~PsychicRequest() { // temorary user object if (_tempObject != NULL) free(_tempObject); // our web parameters for (auto* param : _params) delete (param); _params.clear(); delete _response; } void PsychicRequest::freeSession(void* ctx) { if (ctx != NULL) { SessionData* session = (SessionData*)ctx; delete session; } } PsychicHttpServer* PsychicRequest::server() { return _server; } httpd_req_t* PsychicRequest::request() { return _req; } PsychicClient* PsychicRequest::client() { return _client; } PsychicEndpoint* PsychicRequest::endpoint() { return _endpoint; } void PsychicRequest::setEndpoint(PsychicEndpoint* endpoint) { _endpoint = endpoint; } #ifdef PSY_ENABLE_REGEX bool PsychicRequest::getRegexMatches(std::smatch& matches, bool use_full_uri) { if (_endpoint != nullptr) { std::regex pattern(_endpoint->uri().c_str()); std::string s(this->path().c_str()); if (use_full_uri) s = this->uri().c_str(); return std::regex_search(s, matches, pattern); } return false; } #endif const String PsychicRequest::getFilename() { // parse the content-disposition header if (this->hasHeader("Content-Disposition")) { ContentDisposition cd = this->getContentDisposition(); if (cd.filename != "") return cd.filename; } // fall back to passed in query string PsychicWebParameter* param = getParam("_filename"); if (param != NULL) return param->name(); // fall back to parsing it from url (useful for wildcard uploads) String uri = this->uri(); int filenameStart = uri.lastIndexOf('/') + 1; String filename = uri.substring(filenameStart); if (filename != "") return filename; // finally, unknown. ESP_LOGE(PH_TAG, "Did not get a valid filename from the upload."); return "unknown.txt"; } const ContentDisposition PsychicRequest::getContentDisposition() { ContentDisposition cd; String header = this->header("Content-Disposition"); int start; int end; if (header.indexOf("form-data") == 0) cd.disposition = FORM_DATA; else if (header.indexOf("attachment") == 0) cd.disposition = ATTACHMENT; else if (header.indexOf("inline") == 0) cd.disposition = INLINE; else cd.disposition = NONE; start = header.indexOf("filename="); if (start >= 0) { end = header.indexOf('"', start + 10); cd.filename = header.substring(start + 10, end - 1); } start = header.indexOf("name="); if (start >= 0) { end = header.indexOf('"', start + 6); cd.name = header.substring(start + 6, end - 1); } return cd; } esp_err_t PsychicRequest::loadBody() { if (_bodyParsed != ESP_ERR_NOT_FINISHED) return _bodyParsed; // quick size check. if (contentLength() > server()->maxRequestBodySize) { ESP_LOGE(PH_TAG, "Body size larger than maxRequestBodySize"); return _bodyParsed = ESP_ERR_INVALID_SIZE; } this->_body = String(); size_t remaining = this->_req->content_len; size_t actuallyReceived = 0; char* buf = (char*)malloc(remaining + 1); if (buf == NULL) { ESP_LOGE(PH_TAG, "Failed to allocate memory for body"); return _bodyParsed = ESP_FAIL; } while (remaining > 0) { int received = httpd_req_recv(this->_req, buf + actuallyReceived, remaining); if (received == HTTPD_SOCK_ERR_TIMEOUT) { continue; } else if (received == HTTPD_SOCK_ERR_FAIL) { ESP_LOGE(PH_TAG, "Failed to receive data."); _bodyParsed = ESP_FAIL; break; } remaining -= received; actuallyReceived += received; } buf[actuallyReceived] = '\0'; this->_body = String(buf); free(buf); _bodyParsed = ESP_OK; return _bodyParsed; } http_method PsychicRequest::method() { return (http_method)this->_req->method; } const String PsychicRequest::methodStr() { return String(http_method_str((http_method)this->_req->method)); } const String PsychicRequest::path() { int index = _uri.indexOf("?"); if (index == -1) return _uri; else return _uri.substring(0, index); } const String& PsychicRequest::uri() { return this->_uri; } const String& PsychicRequest::query() { return this->_query; } // no way to get list of headers yet.... // int PsychicRequest::headers() // { // } const String PsychicRequest::header(const char* name) { size_t header_len = httpd_req_get_hdr_value_len(this->_req, name); // if we've got one, allocated it and load it if (header_len) { char header[header_len + 1]; httpd_req_get_hdr_value_str(this->_req, name, header, sizeof(header)); return String(header); } else return ""; } bool PsychicRequest::hasHeader(const char* name) { return httpd_req_get_hdr_value_len(this->_req, name) > 0; } const String PsychicRequest::host() { return this->header("Host"); } const String PsychicRequest::contentType() { return header("Content-Type"); } size_t PsychicRequest::contentLength() { return this->_req->content_len; } const String& PsychicRequest::body() { return this->_body; } bool PsychicRequest::isMultipart() { const String& type = this->contentType(); return (this->contentType().indexOf("multipart/form-data") >= 0); } bool PsychicRequest::hasCookie(const char* key, size_t* size) { char buffer; // this keeps our size for the user. if (size != nullptr) { *size = 1; return getCookie(key, &buffer, size) != ESP_ERR_NOT_FOUND; } // this just checks that it exists. else { size_t mysize = 1; return getCookie(key, &buffer, &mysize) != ESP_ERR_NOT_FOUND; } } esp_err_t PsychicRequest::getCookie(const char* key, char* buffer, size_t* size) { return httpd_req_get_cookie_val(this->_req, key, buffer, size); } String PsychicRequest::getCookie(const char* key) { String cookie = ""; // how big is our cookie? size_t size; if (!hasCookie(key, &size)) return cookie; // allocate cookie buffer... keep it on the stack char buf[size]; // load it up. esp_err_t err = getCookie(key, buf, &size); if (err == ESP_OK) cookie.concat(buf); return cookie; } void PsychicRequest::replaceResponse(PsychicResponse* response) { delete _response; _response = response; } void PsychicRequest::addResponseHeader(const char* key, const char* value) { _response->addHeader(key, value); } std::list& PsychicRequest::getResponseHeaders() { return _response->headers(); } void PsychicRequest::loadParams() { if (_paramsParsed != ESP_ERR_NOT_FINISHED) return; // convenience shortcut to allow calling loadParams() if (_bodyParsed == ESP_ERR_NOT_FINISHED) loadBody(); // various form data as parameters if (this->method() == HTTP_POST) { if (this->contentType().startsWith("application/x-www-form-urlencoded")) _addParams(_body, true); if (this->isMultipart()) { MultipartProcessor mpp(this); _paramsParsed = mpp.process(_body.c_str()); return; } } _paramsParsed = ESP_OK; } void PsychicRequest::_setUri(const char* uri) { // save it _uri = String(uri); // look for our query separator int index = _uri.indexOf('?', 0); if (index >= 0) { // parse them. _query = _uri.substring(index + 1); _addParams(_query, false); } } void PsychicRequest::_addParams(const String& params, bool post) { size_t start = 0; while (start < params.length()) { int end = params.indexOf('&', start); if (end < 0) end = params.length(); int equal = params.indexOf('=', start); if (equal < 0 || equal > end) equal = end; String name = params.substring(start, equal); String value = equal + 1 < end ? params.substring(equal + 1, end) : String(); addParam(name, value, true, post); start = end + 1; } } PsychicWebParameter* PsychicRequest::addParam(const String& name, const String& value, bool decode, bool post) { if (decode) return addParam(new PsychicWebParameter(urlDecode(name.c_str()), urlDecode(value.c_str()), post)); else return addParam(new PsychicWebParameter(name, value, post)); } PsychicWebParameter* PsychicRequest::addParam(PsychicWebParameter* param) { // ESP_LOGD(PH_TAG, "Adding param: '%s' = '%s'", param->name().c_str(), param->value().c_str()); _params.push_back(param); return param; } bool PsychicRequest::hasParam(const char* key) { return getParam(key) != NULL; } bool PsychicRequest::hasParam(const char* key, bool isPost, bool isFile) { return getParam(key, isPost, isFile) != NULL; } PsychicWebParameter* PsychicRequest::getParam(const char* key) { for (auto* param : _params) if (param->name().equals(key)) return param; return NULL; } PsychicWebParameter* PsychicRequest::getParam(const char* key, bool isPost, bool isFile) { for (auto* param : _params) if (param->name().equals(key) && isPost == param->isPost() && isFile == param->isFile()) return param; return NULL; } bool PsychicRequest::hasSessionKey(const String& key) { return this->_session->find(key) != this->_session->end(); } const String PsychicRequest::getSessionKey(const String& key) { auto it = this->_session->find(key); if (it != this->_session->end()) return it->second; else return ""; } void PsychicRequest::setSessionKey(const String& key, const String& value) { this->_session->insert(std::pair(key, value)); } static const String md5str(const String& in) { MD5Builder md5 = MD5Builder(); md5.begin(); md5.add(in); md5.calculate(); return md5.toString(); } bool PsychicRequest::authenticate(const char * username, const char * password, bool passwordIsHashed) { if (hasHeader("Authorization")) { String authReq = header("Authorization"); if (authReq.startsWith("Basic")) { authReq = authReq.substring(6); authReq.trim(); int toencodeLen = strlen(username) + strlen(password) + 1; char* toencode = new char[toencodeLen + 1]; if (toencode == NULL) { authReq = ""; return false; } char* encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; if (encoded == NULL) { authReq = ""; delete[] toencode; return false; } sprintf(toencode, "%s:%s", username, password); if (base64_encode_chars(toencode, toencodeLen, encoded) > 0 && authReq.equalsConstantTime(encoded)) { authReq = ""; delete[] toencode; delete[] encoded; return true; } delete[] toencode; delete[] encoded; } else if (authReq.startsWith(F("Digest"))) { authReq = authReq.substring(7); String _username = _extractParam(authReq, F("username=\""), '\"'); if (!_username.length() || _username != String(username)) { authReq = ""; return false; } // extracting required parameters for RFC 2069 simpler Digest String _realm = _extractParam(authReq, F("realm=\""), '\"'); String _nonce = _extractParam(authReq, F("nonce=\""), '\"'); String _url = _extractParam(authReq, F("uri=\""), '\"'); String _resp = _extractParam(authReq, F("response=\""), '\"'); String _opaque = _extractParam(authReq, F("opaque=\""), '\"'); if ((!_realm.length()) || (!_nonce.length()) || (!_url.length()) || (!_resp.length()) || (!_opaque.length())) { authReq = ""; return false; } if ((_opaque != this->getSessionKey("opaque")) || (_nonce != this->getSessionKey("nonce")) || (_realm != this->getSessionKey("realm"))) { authReq = ""; return false; } // parameters for the RFC 2617 newer Digest String _nc, _cnonce; if (authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) { _nc = _extractParam(authReq, F("nc="), ','); _cnonce = _extractParam(authReq, F("cnonce=\""), '\"'); } String _H1 = passwordIsHashed ? String(password) : md5str(String(username) + ':' + _realm + ':' + String(password)); //ESP_LOGD(PH_TAG, "Hash of user:realm:pass=%s", _H1.c_str()); String _H2 = ""; switch(method()) { case HTTP_GET: _H2 = md5str(String(F("GET:")) + _uri); break; case HTTP_POST: _H2 = md5str(String(F("POST:")) + _uri); break; case HTTP_PUT: _H2 = md5str(String(F("PUT:")) + _uri); break; case HTTP_DELETE: _H2 = md5str(String(F("DELETE:")) + _uri); break; default: _H2 = md5str(String(F("GET:")) + _uri); break; } //ESP_LOGD(PH_TAG, "Hash of GET:uri=%s", _H2.c_str()); String _responsecheck = ""; if (authReq.indexOf("qop=auth") != -1 || authReq.indexOf("qop=\"auth\"") != -1) { _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _nc + ':' + _cnonce + F(":auth:") + _H2); } else { _responsecheck = md5str(_H1 + ':' + _nonce + ':' + _H2); } // ESP_LOGD(PH_TAG, "The Proper response=%s", _responsecheck.c_str()); if (_resp == _responsecheck) { authReq = ""; return true; } } authReq = ""; } return false; } const String PsychicRequest::_extractParam(const String& authReq, const String& param, const char delimit) { int _begin = authReq.indexOf(param); if (_begin == -1) return ""; return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); } const String PsychicRequest::_getRandomHexString() { char buffer[33]; // buffer to hold 32 Hex Digit + /0 int i; for (i = 0; i < 4; i++) { sprintf(buffer + (i * 8), "%08lx", (unsigned long int)esp_random()); } return String(buffer); } esp_err_t PsychicRequest::requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg) { // what is thy realm, sire? if (!strcmp(realm, "")) this->setSessionKey("realm", "Login Required"); else this->setSessionKey("realm", realm); PsychicResponse response(this); String authStr; // what kind of auth? if (mode == BASIC_AUTH) { authStr = "Basic realm=\"" + this->getSessionKey("realm") + "\""; response.addHeader("WWW-Authenticate", authStr.c_str()); } else { // only make new ones if we havent sent them yet if (this->getSessionKey("nonce").isEmpty()) this->setSessionKey("nonce", _getRandomHexString()); if (this->getSessionKey("opaque").isEmpty()) this->setSessionKey("opaque", _getRandomHexString()); authStr = "Digest realm=\"" + this->getSessionKey("realm") + "\", qop=\"auth\", nonce=\"" + this->getSessionKey("nonce") + "\", opaque=\"" + this->getSessionKey("opaque") + "\""; response.addHeader("WWW-Authenticate", authStr.c_str()); } response.setCode(401); response.setContentType("text/html"); response.setContent(authFailMsg); return response.send(); } ================================================ FILE: src/PsychicRequest.h ================================================ #ifndef PsychicRequest_h #define PsychicRequest_h #include "PsychicClient.h" #include "PsychicCore.h" #include "PsychicEndpoint.h" #include "PsychicHttpServer.h" #include "PsychicWebParameter.h" #ifdef PSY_ENABLE_REGEX #include #endif typedef std::map SessionData; enum Disposition { NONE, INLINE, ATTACHMENT, FORM_DATA }; struct ContentDisposition { Disposition disposition; String filename; String name; }; class PsychicRequest { friend PsychicHttpServer; friend PsychicResponse; protected: PsychicHttpServer* _server; httpd_req_t* _req; SessionData* _session; PsychicClient* _client; PsychicEndpoint* _endpoint; http_method _method; String _uri; String _query; String _body; esp_err_t _bodyParsed = ESP_ERR_NOT_FINISHED; esp_err_t _paramsParsed = ESP_ERR_NOT_FINISHED; std::list _params; PsychicResponse* _response; void _setUri(const char* uri); void _addParams(const String& params, bool post); void _parseGETParams(); void _parsePOSTParams(); const String _extractParam(const String& authReq, const String& param, const char delimit); const String _getRandomHexString(); public: PsychicRequest(PsychicHttpServer* server, httpd_req_t* req); virtual ~PsychicRequest(); void* _tempObject; PsychicHttpServer* server(); httpd_req_t* request(); virtual PsychicClient* client(); PsychicEndpoint* endpoint(); void setEndpoint(PsychicEndpoint* endpoint); #ifdef PSY_ENABLE_REGEX bool getRegexMatches(std::smatch& matches, bool use_full_uri = false); #endif bool isMultipart(); esp_err_t loadBody(); const String header(const char* name); bool hasHeader(const char* name); static void freeSession(void* ctx); bool hasSessionKey(const String& key); const String getSessionKey(const String& key); void setSessionKey(const String& key, const String& value); bool hasCookie(const char* key, size_t* size = nullptr); PsychicResponse* response() { return _response; } void replaceResponse(PsychicResponse* response); void addResponseHeader(const char* key, const char* value); std::list& getResponseHeaders(); /** * @brief Get the value string of a cookie value from the "Cookie" request headers by cookie name. * * @param[in] key The cookie name to be searched in the request * @param[out] buffer Pointer to the buffer into which the value of cookie will be copied if the cookie is found * @param[inout] size Pointer to size of the user buffer "val". This variable will contain cookie length if * ESP_OK is returned and required buffer length in case ESP_ERR_HTTPD_RESULT_TRUNC is returned. * * @return * - ESP_OK : Key is found in the cookie string and copied to buffer. The value is null-terminated. * - ESP_ERR_NOT_FOUND : Key not found * - ESP_ERR_INVALID_ARG : Null arguments * - ESP_ERR_HTTPD_RESULT_TRUNC : Value string truncated * - ESP_ERR_NO_MEM : Memory allocation failure */ esp_err_t getCookie(const char* key, char* buffer, size_t* size); // convenience / lazy function for getting cookies. String getCookie(const char* key); http_method method(); // returns the HTTP method used as enum value (eg. HTTP_GET) const String methodStr(); // returns the HTTP method used as a string (eg. "GET") const String path(); // returns the request path (eg /page?foo=bar returns "/page") const String& uri(); // returns the full request uri (eg /page?foo=bar) const String& query(); // returns the request query data (eg /page?foo=bar returns "foo=bar") const String host(); // returns the requested host (request to http://psychic.local/foo will return "psychic.local") const String contentType(); // returns the Content-Type header value size_t contentLength(); // returns the Content-Length header value const String& body(); // returns the body of the request const ContentDisposition getContentDisposition(); const char* version() { return "HTTP/1.1"; } const String& queryString() { return query(); } // compatability function. same as query() const String& url() { return uri(); } // compatability function. same as uri() void loadParams(); PsychicWebParameter* addParam(PsychicWebParameter* param); PsychicWebParameter* addParam(const String& name, const String& value, bool decode = true, bool post = false); bool hasParam(const char* key); bool hasParam(const char* key, bool isPost, bool isFile = false); PsychicWebParameter* getParam(const char* name); PsychicWebParameter* getParam(const char* name, bool isPost, bool isFile = false); const std::list& getParams() { return _params; } const String getFilename(); bool authenticate(const char * username, const char * password, bool passwordIsHashed = false); esp_err_t requestAuthentication(HTTPAuthMethod mode, const char* realm, const char* authFailMsg); }; #endif // PsychicRequest_h ================================================ FILE: src/PsychicResponse.cpp ================================================ #include "PsychicResponse.h" #include "PsychicRequest.h" #include PsychicResponse::PsychicResponse(PsychicRequest* request) : _request(request), _code(0), _status(""), _contentType(emptyString), _contentLength(0), _body("") { // get our global headers out of the way for (auto& header : DefaultHeaders::Instance().getHeaders()) addHeader(header.field.c_str(), header.value.c_str()); } PsychicResponse::~PsychicResponse() { _headers.clear(); } void PsychicResponse::addHeader(const char* field, const char* value) { // erase any existing ones. for (auto itr = _headers.begin(); itr != _headers.end();) { if (itr->field.equalsIgnoreCase(field)) itr = _headers.erase(itr); else itr++; } // now add it. _headers.push_back({field, value}); } void PsychicResponse::setCookie(const char* name, const char* value, unsigned long secondsFromNow, const char* extras) { time_t now = time(nullptr); String output; output = urlEncode(name) + "=" + urlEncode(value); // if current time isn't modern, default to using max age if (now < 1700000000) output += "; Max-Age=" + String(secondsFromNow); // otherwise, set an expiration date else { time_t expirationTimestamp = now + secondsFromNow; // Convert the expiration timestamp to a formatted string for the "expires" attribute struct tm* tmInfo = gmtime(&expirationTimestamp); char expires[30]; strftime(expires, sizeof(expires), "%a, %d %b %Y %H:%M:%S GMT", tmInfo); output += "; Expires=" + String(expires); } // did we get any extras? if (strlen(extras)) output += "; " + String(extras); // okay, add it in. addHeader("Set-Cookie", output.c_str()); } void PsychicResponse::setCode(int code) { _code = code; } void PsychicResponse::setContentType(const char* contentType) { _contentType = contentType; } void PsychicResponse::setContent(const char* content) { _body = content; setContentLength(strlen(content)); } void PsychicResponse::setContent(const uint8_t* content, size_t len) { _body = (char*)content; setContentLength(len); } const char* PsychicResponse::getContent() { return _body; } size_t PsychicResponse::getContentLength() { return _contentLength; } esp_err_t PsychicResponse::send() { if (!_code) setCode(200); // our headers too this->sendHeaders(); // now send it off esp_err_t err = httpd_resp_send(_request->request(), getContent(), getContentLength()); // did something happen? if (err != ESP_OK) ESP_LOGE(PH_TAG, "Send response failed (%s)", esp_err_to_name(err)); return err; } void PsychicResponse::sendHeaders() { // esp-idf makes you set the whole status. sprintf(_status, "%d %s", _code, http_status_reason(_code)); httpd_resp_set_status(_request->request(), _status); // set the content type httpd_resp_set_type(_request->request(), _contentType.c_str()); // now do our individual headers for (auto& header : _headers) httpd_resp_set_hdr(this->_request->request(), header.field.c_str(), header.value.c_str()); } esp_err_t PsychicResponse::sendChunk(uint8_t* chunk, size_t chunksize) { /* Send the buffer contents as HTTP response chunk */ ESP_LOGD(PH_TAG, "Sending chunk: %d", chunksize); esp_err_t err = httpd_resp_send_chunk(request(), (char*)chunk, chunksize); if (err != ESP_OK) { ESP_LOGE(PH_TAG, "File sending failed (%s)", esp_err_to_name(err)); /* Abort sending file */ httpd_resp_sendstr_chunk(this->_request->request(), NULL); } return err; } esp_err_t PsychicResponse::finishChunking() { /* Respond with an empty chunk to signal HTTP response completion */ return httpd_resp_send_chunk(this->_request->request(), NULL, 0); } esp_err_t PsychicResponse::redirect(const char* url) { if (!_code) setCode(301); addHeader("Location", url); return send(); } esp_err_t PsychicResponse::send(int code) { setCode(code); return send(); } esp_err_t PsychicResponse::send(const char* content) { if (!_code) setCode(200); if (_contentType.isEmpty()) setContentType("text/html"); setContent(content); return send(); } esp_err_t PsychicResponse::send(const char* contentType, const char* content) { if (!_code) setCode(200); setContentType(contentType); setContent(content); return send(); } esp_err_t PsychicResponse::send(int code, const char* contentType, const char* content) { setCode(code); setContentType(contentType); setContent(content); return send(); } esp_err_t PsychicResponse::send(int code, const char* contentType, const uint8_t* content, size_t len) { setCode(code); setContentType(contentType); setContent(content, len); return send(); } esp_err_t PsychicResponse::error(httpd_err_code_t code, const char* message) { return httpd_resp_send_err(_request->_req, code, message); } httpd_req_t* PsychicResponse::request() { return _request->_req; } ================================================ FILE: src/PsychicResponse.h ================================================ #ifndef PsychicResponse_h #define PsychicResponse_h #include "PsychicCore.h" #include "time.h" class PsychicRequest; class PsychicResponse { protected: PsychicRequest* _request; int _code; char _status[60]; std::list _headers; String _contentType; int64_t _contentLength; const char* _body; public: PsychicResponse(PsychicRequest* request); virtual ~PsychicResponse(); const char* version() { return "HTTP/1.1"; } void setCode(int code); int getCode() { return _code; } void setContentType(const char* contentType); String& getContentType() { return _contentType; } void setContentLength(int64_t contentLength) { _contentLength = contentLength; } int64_t getContentLength(int64_t contentLength) { return _contentLength; } void addHeader(const char* field, const char* value); std::list& headers() { return _headers; } void setCookie(const char* key, const char* value, unsigned long max_age = 60 * 60 * 24 * 30, const char* extras = ""); void setContent(const char* content); void setContent(const uint8_t* content, size_t len); const char* getContent(); size_t getContentLength(); virtual esp_err_t send(); void sendHeaders(); esp_err_t sendChunk(uint8_t* chunk, size_t chunksize); esp_err_t finishChunking(); esp_err_t redirect(const char* url); esp_err_t send(int code); esp_err_t send(const char* content); esp_err_t send(const char* contentType, const char* content); esp_err_t send(int code, const char* contentType, const char* content); esp_err_t send(int code, const char* contentType, const uint8_t* content, size_t len); esp_err_t error(httpd_err_code_t code, const char* message); httpd_req_t* request(); }; class PsychicResponseDelegate { protected: PsychicResponse* _response; public: PsychicResponseDelegate(PsychicResponse* response) : _response(response) {} virtual ~PsychicResponseDelegate() {} const char* version() { return _response->version(); } void setCode(int code) { _response->setCode(code); } void setContentType(const char* contentType) { _response->setContentType(contentType); } String& getContentType() { return _response->getContentType(); } void setContentLength(int64_t contentLength) { _response->setContentLength(contentLength); } int64_t getContentLength(int64_t contentLength) { return _response->getContentLength(); } void addHeader(const char* field, const char* value) { _response->addHeader(field, value); } void setCookie(const char* key, const char* value, unsigned long max_age = 60 * 60 * 24 * 30, const char* extras = "") { _response->setCookie(key, value, max_age, extras); } void setContent(const char* content) { _response->setContent(content); } void setContent(const uint8_t* content, size_t len) { _response->setContent(content, len); } const char* getContent() { return _response->getContent(); } size_t getContentLength() { return _response->getContentLength(); } esp_err_t send() { return _response->send(); } void sendHeaders() { _response->sendHeaders(); } esp_err_t sendChunk(uint8_t* chunk, size_t chunksize) { return _response->sendChunk(chunk, chunksize); } esp_err_t finishChunking() { return _response->finishChunking(); } esp_err_t redirect(const char* url) { return _response->redirect(url); } esp_err_t send(int code) { return _response->send(code); } esp_err_t send(const char* content) { return _response->send(content); } esp_err_t send(const char* contentType, const char* content) { return _response->send(contentType, content); } esp_err_t send(int code, const char* contentType, const char* content) { return _response->send(code, contentType, content); } esp_err_t send(int code, const char* contentType, const uint8_t* content, size_t len) { return _response->send(code, contentType, content, len); } esp_err_t error(httpd_err_code_t code, const char* message) { return _response->error(code, message); } httpd_req_t* request() { return _response->request(); } }; #endif // PsychicResponse_h ================================================ FILE: src/PsychicRewrite.cpp ================================================ #include "PsychicRewrite.h" #include "PsychicRequest.h" PsychicRewrite::PsychicRewrite(const char* from, const char* to): _fromPath(from), _toUri(to), _toPath(String()), _toParams(String()), _filter(nullptr) { int index = _toUri.indexOf('?'); if (index > 0) { _toParams = _toUri.substring(index + 1); _toPath = _toUri.substring(0, index); } else _toPath = _toUri; } PsychicRewrite::~PsychicRewrite() { } PsychicRewrite* PsychicRewrite::setFilter(PsychicRequestFilterFunction fn) { _filter = fn; return this; } bool PsychicRewrite::filter(PsychicRequest *request) const { return _filter == nullptr || _filter(request); } const String& PsychicRewrite::from(void) const { return _fromPath; } const String& PsychicRewrite::toUrl(void) const { return _toUri; } const String& PsychicRewrite::params(void) const { return _toParams; } bool PsychicRewrite::match(PsychicRequest *request) { if (!filter(request)) return false; return _fromPath == request->path(); } ================================================ FILE: src/PsychicRewrite.h ================================================ #ifndef PsychicRewrite_h #define PsychicRewrite_h #include "PsychicCore.h" /* * REWRITE :: One instance can be handle any Request (done by the Server) * */ class PsychicRewrite { protected: String _fromPath; String _toUri; String _toPath; String _toParams; PsychicRequestFilterFunction _filter; public: PsychicRewrite(const char* from, const char* to); virtual ~PsychicRewrite(); PsychicRewrite* setFilter(PsychicRequestFilterFunction fn); bool filter(PsychicRequest *request) const; const String& from(void) const; const String& toUrl(void) const; const String& params(void) const; virtual bool match(PsychicRequest *request); }; #endif ================================================ FILE: src/PsychicStaticFileHander.cpp ================================================ #include "PsychicStaticFileHandler.h" /*************************************/ /* PsychicStaticFileHandler */ /*************************************/ PsychicStaticFileHandler::PsychicStaticFileHandler(const char* uri, FS& fs, const char* path, const char* cache_control) : _fs(fs), _uri(uri), _path(path), _default_file("index.html"), _cache_control(cache_control), _last_modified("") { // Ensure leading '/' if (_uri.length() == 0 || _uri[0] != '/') _uri = "/" + _uri; if (_path.length() == 0 || _path[0] != '/') _path = "/" + _path; // If path ends with '/' we assume a hint that this is a directory to improve performance. // However - if it does not end with '/' we, can't assume a file, path can still be a directory. _isDir = _path[_path.length() - 1] == '/'; // Remove the trailing '/' so we can handle default file // Notice that root will be "" not "/" if (_uri[_uri.length() - 1] == '/') _uri = _uri.substring(0, _uri.length() - 1); if (_path[_path.length() - 1] == '/') _path = _path.substring(0, _path.length() - 1); // Reset stats _gzipFirst = false; _gzipStats = 0xF8; } PsychicStaticFileHandler* PsychicStaticFileHandler::setIsDir(bool isDir) { _isDir = isDir; return this; } PsychicStaticFileHandler* PsychicStaticFileHandler::setDefaultFile(const char* filename) { _default_file = filename; return this; } PsychicStaticFileHandler* PsychicStaticFileHandler::setCacheControl(const char* cache_control) { _cache_control = cache_control; return this; } PsychicStaticFileHandler* PsychicStaticFileHandler::setLastModified(const char* last_modified) { _last_modified = String(last_modified); return this; } PsychicStaticFileHandler* PsychicStaticFileHandler::setLastModified(struct tm* last_modified) { char result[30]; strftime(result, 30, "%a, %d %b %Y %H:%M:%S %Z", last_modified); return setLastModified((const char*)result); } bool PsychicStaticFileHandler::canHandle(PsychicRequest* request) { if (request->method() != HTTP_GET) { ESP_LOGD(PH_TAG, "Request %s refused by PsychicStaticFileHandler: %s", request->uri().c_str(), request->methodStr().c_str()); return false; } if (!request->uri().startsWith(_uri)) { ESP_LOGD(PH_TAG, "Request %s refused by PsychicStaticFileHandler: does not start with %s", request->uri().c_str(), _uri.c_str()); return false; } if (_getFile(request)) { return true; } ESP_LOGD(PH_TAG, "Request %s refused by PsychicStaticFileHandler: file not found", request->uri().c_str()); return false; } bool PsychicStaticFileHandler::_getFile(PsychicRequest* request) { // Remove the found uri String path = request->uri().substring(_uri.length()); // Reject any path that contains directory traversal sequences if (path.indexOf("..") >= 0) return false; // We can skip the file check and look for default if request is to the root of a directory or that request path ends with '/' bool canSkipFileCheck = (_isDir && path.length() == 0) || (path.length() && path[path.length() - 1] == '/'); path = _path + path; // Do we have a file or .gz file if (!canSkipFileCheck && _fileExists(path)) return true; // Can't handle if not default file if (_default_file.length() == 0) return false; // Try to add default file, ensure there is a trailing '/' ot the path. if (path.length() == 0 || path[path.length() - 1] != '/') path += "/"; path += _default_file; return _fileExists(path); } #define FILE_IS_REAL(f) (f == true && !f.isDirectory()) bool PsychicStaticFileHandler::_fileExists(const String& path) { bool fileFound = false; bool gzipFound = false; String gzip = path + ".gz"; if (_gzipFirst) { _file = _fs.open(gzip, "r"); gzipFound = FILE_IS_REAL(_file); if (!gzipFound) { _file = _fs.open(path, "r"); fileFound = FILE_IS_REAL(_file); } } else { _file = _fs.open(path, "r"); fileFound = FILE_IS_REAL(_file); if (!fileFound) { _file = _fs.open(gzip, "r"); gzipFound = FILE_IS_REAL(_file); } } bool found = fileFound || gzipFound; if (found) { _filename = path; // Calculate gzip statistic _gzipStats = (_gzipStats << 1) + (gzipFound ? 1 : 0); if (_gzipStats == 0x00) _gzipFirst = false; // All files are not gzip else if (_gzipStats == 0xFF) _gzipFirst = true; // All files are gzip else _gzipFirst = _countBits(_gzipStats) > 4; // IF we have more gzip files - try gzip first } ESP_LOGD(PH_TAG, "PsychicStaticFileHandler _fileExists(%s): %d", path.c_str(), found); return found; } uint8_t PsychicStaticFileHandler::_countBits(const uint8_t value) const { uint8_t w = value; uint8_t n; for (n = 0; w != 0; n++) w &= w - 1; return n; } esp_err_t PsychicStaticFileHandler::handleRequest(PsychicRequest* request, PsychicResponse* res) { if (_file == true) { // is it not modified? String etag = String(_file.size()); if (_last_modified.length() && _last_modified == request->header("If-Modified-Since")) { _file.close(); res->send(304); // Not modified } // does our Etag match? else if (_cache_control.length() && request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag)) { _file.close(); res->addHeader("Cache-Control", _cache_control.c_str()); res->addHeader("ETag", etag.c_str()); res->setCode(304); res->send(); } // nope, send them the full file. else { _file.close(); PsychicFileResponse response(res, _fs, _filename); if (_last_modified.length()) response.addHeader("Last-Modified", _last_modified.c_str()); if (_cache_control.length()) { response.addHeader("Cache-Control", _cache_control.c_str()); response.addHeader("ETag", etag.c_str()); } return response.send(); } } else { return res->send(404); } return ESP_OK; } ================================================ FILE: src/PsychicStaticFileHandler.h ================================================ #ifndef PsychicStaticFileHandler_h #define PsychicStaticFileHandler_h #include "PsychicCore.h" #include "PsychicFileResponse.h" #include "PsychicRequest.h" #include "PsychicResponse.h" #include "PsychicWebHandler.h" class PsychicStaticFileHandler : public PsychicWebHandler { using File = fs::File; using FS = fs::FS; private: bool _getFile(PsychicRequest* request); bool _fileExists(const String& path); uint8_t _countBits(const uint8_t value) const; protected: FS _fs; File _file; String _filename; String _uri; String _path; String _default_file; String _cache_control; String _last_modified; bool _isDir; bool _gzipFirst; uint8_t _gzipStats; public: PsychicStaticFileHandler(const char* uri, FS& fs, const char* path, const char* cache_control); bool canHandle(PsychicRequest* request) override; esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override; PsychicStaticFileHandler* setIsDir(bool isDir); PsychicStaticFileHandler* setDefaultFile(const char* filename); PsychicStaticFileHandler* setCacheControl(const char* cache_control); PsychicStaticFileHandler* setLastModified(const char* last_modified); PsychicStaticFileHandler* setLastModified(struct tm* last_modified); // PsychicStaticFileHandler* setTemplateProcessor(AwsTemplateProcessor newCallback) {_callback = newCallback; return *this;} }; #endif /* PsychicHttp_h */ ================================================ FILE: src/PsychicStreamResponse.cpp ================================================ #include "PsychicStreamResponse.h" #include "PsychicRequest.h" #include "PsychicResponse.h" PsychicStreamResponse::PsychicStreamResponse(PsychicResponse* response, const String& contentType) : PsychicResponseDelegate(response), _buffer(NULL) { setContentType(contentType.c_str()); addHeader("Content-Disposition", "inline"); } PsychicStreamResponse::PsychicStreamResponse(PsychicResponse* response, const String& contentType, const String& name) : PsychicResponseDelegate(response), _buffer(NULL) { setContentType(contentType.c_str()); char buf[26 + name.length()]; snprintf(buf, sizeof(buf), "attachment; filename=\"%s\"", name.c_str()); addHeader("Content-Disposition", buf); } PsychicStreamResponse::~PsychicStreamResponse() { endSend(); } esp_err_t PsychicStreamResponse::beginSend() { if (_buffer) return ESP_OK; // Buffer to hold ChunkPrinter and stream buffer. Using placement new will keep us at a single allocation. _buffer = (uint8_t*)malloc(STREAM_CHUNK_SIZE + sizeof(ChunkPrinter)); if (!_buffer) { /* Respond with 500 Internal Server Error */ ESP_LOGE(PH_TAG, "Unable to allocate %zu bytes to send chunk", STREAM_CHUNK_SIZE + sizeof(ChunkPrinter)); httpd_resp_send_err(request(), HTTPD_500_INTERNAL_SERVER_ERROR, "Unable to allocate memory."); return ESP_FAIL; } _printer = new (_buffer) ChunkPrinter(_response, _buffer + sizeof(ChunkPrinter), STREAM_CHUNK_SIZE); sendHeaders(); return ESP_OK; } esp_err_t PsychicStreamResponse::endSend() { esp_err_t err = ESP_OK; if (!_buffer) err = ESP_FAIL; else { _printer->~ChunkPrinter(); // flushed on destruct err = finishChunking(); free(_buffer); _buffer = NULL; } return err; } void PsychicStreamResponse::flush() { if (_buffer) _printer->flush(); } size_t PsychicStreamResponse::write(uint8_t data) { return _buffer ? _printer->write(data) : 0; } size_t PsychicStreamResponse::write(const uint8_t* buffer, size_t size) { return _buffer ? _printer->write(buffer, size) : 0; } size_t PsychicStreamResponse::copyFrom(Stream& stream) { if (_buffer) return _printer->copyFrom(stream); return 0; } ================================================ FILE: src/PsychicStreamResponse.h ================================================ #ifndef PsychicStreamResponse_h #define PsychicStreamResponse_h #include "ChunkPrinter.h" #include "PsychicCore.h" #include "PsychicResponse.h" class PsychicRequest; class PsychicStreamResponse : public PsychicResponseDelegate, public Print { private: ChunkPrinter* _printer; uint8_t* _buffer; public: PsychicStreamResponse(PsychicResponse* response, const String& contentType); PsychicStreamResponse(PsychicResponse* response, const String& contentType, const String& name); // Download ~PsychicStreamResponse(); esp_err_t beginSend(); esp_err_t endSend(); void flush() override; size_t write(uint8_t data) override; size_t write(const uint8_t* buffer, size_t size) override; size_t copyFrom(Stream& stream); using Print::write; }; #endif // PsychicStreamResponse_h ================================================ FILE: src/PsychicUploadHandler.cpp ================================================ #include "PsychicUploadHandler.h" PsychicUploadHandler::PsychicUploadHandler() : PsychicWebHandler(), _uploadCallback(nullptr) { } PsychicUploadHandler::~PsychicUploadHandler() {} bool PsychicUploadHandler::canHandle(PsychicRequest* request) { return true; } esp_err_t PsychicUploadHandler::handleRequest(PsychicRequest* request, PsychicResponse* response) { esp_err_t err = ESP_OK; /* File cannot be larger than a limit */ if (request->contentLength() > request->server()->maxUploadSize) { ESP_LOGE(PH_TAG, "File too large : %zu bytes", request->contentLength()); /* Respond with 400 Bad Request */ char error[50]; sprintf(error, "File size must be less than %lu bytes!", request->server()->maxUploadSize); return httpd_resp_send_err(request->request(), HTTPD_400_BAD_REQUEST, error); } // TODO: support for the 100 header. not sure if we can do it. // if (request->header("Expect").equals("100-continue")) // { // char response[] = "100 Continue"; // httpd_socket_send(self->server, httpd_req_to_sockfd(req), response, strlen(response), 0); // } // 2 types of upload requests if (request->isMultipart()) err = _multipartUploadHandler(request); else err = _basicUploadHandler(request); // we can also call onRequest for some final processing and response if (err == ESP_OK) { if (_requestCallback != NULL) err = _requestCallback(request, response); else err = response->send("Upload Successful."); } else if (err == ESP_ERR_HTTPD_INVALID_REQ) response->send(400, "text/html", "No multipart boundary found."); else response->send(500, "text/html", "Error processing upload."); return err; } esp_err_t PsychicUploadHandler::_basicUploadHandler(PsychicRequest* request) { esp_err_t err = ESP_OK; String filename = request->getFilename(); /* Retrieve the pointer to scratch buffer for temporary storage */ char* buf = (char*)malloc(FILE_CHUNK_SIZE); int received; unsigned long index = 0; /* Content length of the request gives the size of the file being uploaded */ int remaining = request->contentLength(); while (remaining > 0) { #ifdef ENABLE_ASYNC httpd_sess_update_lru_counter(request->server()->server, request->client()->socket()); #endif // ESP_LOGD(PH_TAG, "Remaining size : %d", remaining); /* Receive the file part by part into a buffer */ if ((received = httpd_req_recv(request->request(), buf, min(remaining, FILE_CHUNK_SIZE))) <= 0) { /* Retry if timeout occurred */ if (received == HTTPD_SOCK_ERR_TIMEOUT) continue; // bail if we got an error else if (received == HTTPD_SOCK_ERR_FAIL) { ESP_LOGE(PH_TAG, "Socket error"); err = ESP_FAIL; break; } } // call our upload callback here. if (_uploadCallback != NULL) { err = _uploadCallback(request, filename, index, (uint8_t*)buf, received, (remaining - received == 0)); if (err != ESP_OK) break; } else { ESP_LOGE(PH_TAG, "No upload callback specified!"); err = ESP_FAIL; break; } /* Keep track of remaining size of the file left to be uploaded */ if (received > 0) { remaining -= received; index += received; } } // dont forget to free our buffer free(buf); return err; } esp_err_t PsychicUploadHandler::_multipartUploadHandler(PsychicRequest* request) { MultipartProcessor mpp(request, _uploadCallback); return mpp.process(); } PsychicUploadHandler* PsychicUploadHandler::onUpload(PsychicUploadCallback fn) { _uploadCallback = fn; return this; } ================================================ FILE: src/PsychicUploadHandler.h ================================================ #ifndef PsychicUploadHandler_h #define PsychicUploadHandler_h #include "MultipartProcessor.h" #include "PsychicCore.h" #include "PsychicHttpServer.h" #include "PsychicRequest.h" #include "PsychicWebHandler.h" /* * HANDLER :: Can be attached to any endpoint or as a generic request handler. */ class PsychicUploadHandler : public PsychicWebHandler { protected: esp_err_t _basicUploadHandler(PsychicRequest* request); esp_err_t _multipartUploadHandler(PsychicRequest* request); PsychicUploadCallback _uploadCallback; public: PsychicUploadHandler(); ~PsychicUploadHandler(); bool canHandle(PsychicRequest* request) override; esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override; PsychicUploadHandler* onUpload(PsychicUploadCallback fn); }; #endif // PsychicUploadHandler_h ================================================ FILE: src/PsychicVersion.h ================================================ // Copyright 2019 Espressif Systems (Shanghai) PTE LTD // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #pragma once /** Major version number (X.x.x) */ #define PSYCHIC_VERSION_MAJOR 2 /** Minor version number (x.X.x) */ #define PSYCHIC_VERSION_MINOR 2 /** Patch version number (x.x.X) */ #define PSYCHIC_VERSION_PATCH 0 /** * Macro to convert PsychicHttp version number into an integer * * To be used in comparisons, such as PSYCHIC_VERSION >= PSYCHIC_VERSION_VAL(2, 0, 0) */ #define PSYCHIC_VERSION_VAL(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) /** * Current PsychicHttp version, as an integer * * To be used in comparisons, such as PSYCHIC_VERSION >= PSYCHIC_VERSION_VAL(2, 0, 0) */ #define PSYCHIC_VERSION PSYCHIC_VERSION_VAL(PSYCHIC_VERSION_MAJOR, PSYCHIC_VERSION_MINOR, PSYCHIC_VERSION_PATCH) /** * Current PsychicHttp version, as string */ #ifndef PSYCHIC_df2xstr #define PSYCHIC_df2xstr(s) #s #endif #ifndef PSYCHIC_df2str #define PSYCHIC_df2str(s) PSYCHIC_df2xstr(s) #endif #define PSYCHIC_VERSION_STR PSYCHIC_df2str(PSYCHIC_VERSION_MAJOR) "." PSYCHIC_df2str(PSYCHIC_VERSION_MINOR) "." PSYCHIC_df2str(PSYCHIC_VERSION_PATCH) ================================================ FILE: src/PsychicWebHandler.cpp ================================================ #include "PsychicWebHandler.h" PsychicWebHandler::PsychicWebHandler() : PsychicHandler(), _requestCallback(NULL), _onOpen(NULL), _onClose(NULL) { } PsychicWebHandler::~PsychicWebHandler() {} bool PsychicWebHandler::canHandle(PsychicRequest* request) { return true; } esp_err_t PsychicWebHandler::handleRequest(PsychicRequest* request, PsychicResponse* response) { // lookup our client PsychicClient* client = checkForNewClient(request->client()); if (client->isNew) openCallback(client); /* Request body cannot be larger than a limit */ if (request->contentLength() > request->server()->maxRequestBodySize) { ESP_LOGE(PH_TAG, "Request body too large : %zu bytes", request->contentLength()); /* Respond with 400 Bad Request */ char error[60]; sprintf(error, "Request body must be less than %lu bytes!", request->server()->maxRequestBodySize); response->send(400, "text/html", error); /* Return failure to close underlying connection else the incoming file content will keep the socket busy */ return ESP_FAIL; } // get our body loaded up. esp_err_t err = request->loadBody(); if (err != ESP_OK) return response->send(400, "text/html", "Error loading request body."); // load our params in. request->loadParams(); // okay, pass on to our callback. if (this->_requestCallback != NULL) err = this->_requestCallback(request, response); return err; } PsychicWebHandler* PsychicWebHandler::onRequest(PsychicHttpRequestCallback fn) { _requestCallback = fn; return this; } void PsychicWebHandler::openCallback(PsychicClient* client) { if (_onOpen != NULL) _onOpen(client); } void PsychicWebHandler::closeCallback(PsychicClient* client) { if (_onClose != NULL) _onClose(getClient(client)); } PsychicWebHandler* PsychicWebHandler::onOpen(PsychicClientCallback fn) { _onOpen = fn; return this; } PsychicWebHandler* PsychicWebHandler::onClose(PsychicClientCallback fn) { _onClose = fn; return this; } ================================================ FILE: src/PsychicWebHandler.h ================================================ #ifndef PsychicWebHandler_h #define PsychicWebHandler_h // #include "PsychicCore.h" // #include "PsychicHttpServer.h" // #include "PsychicRequest.h" #include "PsychicHandler.h" /* * HANDLER :: Can be attached to any endpoint or as a generic request handler. */ class PsychicWebHandler : public PsychicHandler { protected: PsychicHttpRequestCallback _requestCallback; PsychicClientCallback _onOpen; PsychicClientCallback _onClose; public: PsychicWebHandler(); ~PsychicWebHandler(); virtual bool canHandle(PsychicRequest* request) override; virtual esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override; PsychicWebHandler* onRequest(PsychicHttpRequestCallback fn); virtual void openCallback(PsychicClient* client); virtual void closeCallback(PsychicClient* client); PsychicWebHandler* onOpen(PsychicClientCallback fn); PsychicWebHandler* onClose(PsychicClientCallback fn); }; #endif ================================================ FILE: src/PsychicWebParameter.h ================================================ #ifndef PsychicWebParameter_h #define PsychicWebParameter_h /* * PARAMETER :: Chainable object to hold GET/POST and FILE parameters * */ class PsychicWebParameter { private: String _name; String _value; size_t _size; bool _isForm; bool _isFile; public: PsychicWebParameter(const String& name, const String& value, bool form = false, bool file = false, size_t size = 0) : _name(name), _value(value), _size(size), _isForm(form), _isFile(file) {} const String& name() const { return _name; } const String& value() const { return _value; } size_t size() const { return _size; } bool isPost() const { return _isForm; } bool isFile() const { return _isFile; } }; #endif // PsychicWebParameter_h ================================================ FILE: src/PsychicWebSocket.cpp ================================================ #include "PsychicWebSocket.h" /*************************************/ /* PsychicWebSocketRequest */ /*************************************/ PsychicWebSocketRequest::PsychicWebSocketRequest(PsychicRequest* req) : PsychicRequest(req->server(), req->request()), _client(req->client()) { } PsychicWebSocketRequest::~PsychicWebSocketRequest() { } PsychicWebSocketClient* PsychicWebSocketRequest::client() { return &_client; } esp_err_t PsychicWebSocketRequest::reply(httpd_ws_frame_t* ws_pkt) { return httpd_ws_send_frame(this->_req, ws_pkt); } esp_err_t PsychicWebSocketRequest::reply(httpd_ws_type_t op, const void* data, size_t len) { httpd_ws_frame_t ws_pkt; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.payload = (uint8_t*)data; ws_pkt.len = len; ws_pkt.type = op; return this->reply(&ws_pkt); } esp_err_t PsychicWebSocketRequest::reply(const char* buf) { return this->reply(HTTPD_WS_TYPE_TEXT, buf, strlen(buf)); } /*************************************/ /* PsychicWebSocketClient */ /*************************************/ PsychicWebSocketClient::PsychicWebSocketClient(PsychicClient* client) : PsychicClient(client->server(), client->socket()) { } PsychicWebSocketClient::~PsychicWebSocketClient() { } void PsychicWebSocketClient::_sendMessageCallback(esp_err_t err, int socket, void* arg) { // free our frame. httpd_ws_frame_t* ws_pkt = (httpd_ws_frame_t*)arg; free(ws_pkt->payload); free(ws_pkt); if (err == ESP_OK) return; else if (err == ESP_FAIL) ESP_LOGE(PH_TAG, "Websocket: send - socket error (#%d)", socket); else if (err == ESP_ERR_INVALID_STATE) ESP_LOGE(PH_TAG, "Websocket: Handshake was already done beforehand (#%d)", socket); else if (err == ESP_ERR_INVALID_ARG) ESP_LOGE(PH_TAG, "Websocket: Argument is invalid (null or non-WebSocket) (#%d)", socket); else ESP_LOGE(PH_TAG, "Websocket: Send message unknown error. (#%d)", socket); } esp_err_t PsychicWebSocketClient::sendMessage(httpd_ws_frame_t* ws_pkt) { return sendMessage(ws_pkt->type, ws_pkt->payload, ws_pkt->len); } esp_err_t PsychicWebSocketClient::sendMessage(httpd_ws_type_t op, const void* data, size_t len) { // init our frame. httpd_ws_frame_t* ws_pkt = (httpd_ws_frame_t*)malloc(sizeof(httpd_ws_frame_t)); if (ws_pkt == NULL) { ESP_LOGE(PH_TAG, "Websocket: out of memory"); return ESP_ERR_NO_MEM; } memset(ws_pkt, 0, sizeof(httpd_ws_frame_t)); // zero the datastructure out // allocate for event text ws_pkt->payload = (uint8_t*)malloc(len); if (ws_pkt->payload == NULL) { ESP_LOGE(PH_TAG, "Websocket: out of memory"); free(ws_pkt); // free our other memory return ESP_ERR_NO_MEM; } memcpy(ws_pkt->payload, data, len); ws_pkt->len = len; ws_pkt->type = op; esp_err_t err = httpd_ws_send_data_async(server(), socket(), ws_pkt, PsychicWebSocketClient::_sendMessageCallback, ws_pkt); // take care of memory if (err != ESP_OK) { free(ws_pkt->payload); free(ws_pkt); } return err; } esp_err_t PsychicWebSocketClient::sendMessage(const char* buf) { return this->sendMessage(HTTPD_WS_TYPE_TEXT, buf, strlen(buf)); } PsychicWebSocketHandler::PsychicWebSocketHandler() : PsychicHandler(), _onOpen(NULL), _onFrame(NULL), _onClose(NULL) { } PsychicWebSocketHandler::~PsychicWebSocketHandler() { } PsychicWebSocketClient* PsychicWebSocketHandler::getClient(int socket) { PsychicClient* client = PsychicHandler::getClient(socket); if (client == NULL) return NULL; if (client->_friend == NULL) { return NULL; } return (PsychicWebSocketClient*)client->_friend; } PsychicWebSocketClient* PsychicWebSocketHandler::getClient(PsychicClient* client) { return getClient(client->socket()); } void PsychicWebSocketHandler::addClient(PsychicClient* client) { client->_friend = new PsychicWebSocketClient(client); PsychicHandler::addClient(client); } void PsychicWebSocketHandler::removeClient(PsychicClient* client) { PsychicHandler::removeClient(client); delete (PsychicWebSocketClient*)client->_friend; client->_friend = NULL; } void PsychicWebSocketHandler::openCallback(PsychicClient* client) { PsychicWebSocketClient* buddy = getClient(client); if (buddy == NULL) { return; } if (_onOpen != NULL) _onOpen(getClient(buddy)); } void PsychicWebSocketHandler::closeCallback(PsychicClient* client) { PsychicWebSocketClient* buddy = getClient(client); if (buddy == NULL) { return; } if (_onClose != NULL) _onClose(getClient(buddy)); } bool PsychicWebSocketHandler::isWebSocket() { return true; } esp_err_t PsychicWebSocketHandler::handleRequest(PsychicRequest* request, PsychicResponse* response) { // lookup our client PsychicClient* client = checkForNewClient(request->client()); // beginning of the ws URI handler and our onConnect hook if (request->method() == HTTP_GET) { if (client->isNew) openCallback(client); return ESP_OK; } // prep our request PsychicWebSocketRequest wsRequest(request); // init our memory for storing the packet httpd_ws_frame_t ws_pkt; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.type = HTTPD_WS_TYPE_TEXT; uint8_t* buf = NULL; /* Set max_len = 0 to get the frame len */ esp_err_t ret = httpd_ws_recv_frame(wsRequest.request(), &ws_pkt, 0); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "httpd_ws_recv_frame failed to get frame len with %s", esp_err_to_name(ret)); return ret; } // okay, now try to load the packet // ESP_LOGD(PH_TAG, "frame len is %d", ws_pkt.len); if (ws_pkt.len) { /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */ buf = (uint8_t*)calloc(1, ws_pkt.len + 1); if (buf == NULL) { ESP_LOGE(PH_TAG, "Failed to calloc memory for buf"); return ESP_ERR_NO_MEM; } ws_pkt.payload = buf; /* Set max_len = ws_pkt.len to get the frame payload */ ret = httpd_ws_recv_frame(wsRequest.request(), &ws_pkt, ws_pkt.len); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "httpd_ws_recv_frame failed with %s", esp_err_to_name(ret)); free(buf); return ret; } // ESP_LOGD(PH_TAG, "Got packet with message: %s", ws_pkt.payload); } if (ws_pkt.type == HTTPD_WS_TYPE_PING) { // Respond to ping with pong using the same payload ret = wsRequest.reply(HTTPD_WS_TYPE_PONG, ws_pkt.payload, ws_pkt.len); if (ret != ESP_OK) { ESP_LOGE(PH_TAG, "Failed to send pong response: %s", esp_err_to_name(ret)); } } // Text messages are our payload. if (ws_pkt.type == HTTPD_WS_TYPE_TEXT || ws_pkt.type == HTTPD_WS_TYPE_BINARY) { if (this->_onFrame != NULL) ret = this->_onFrame(&wsRequest, &ws_pkt); } // logging housekeeping if (ret != ESP_OK) ESP_LOGE(PH_TAG, "httpd_ws_send_frame failed with %s", esp_err_to_name(ret)); // ESP_LOGD(PH_TAG, "ws_handler: httpd_handle_t=%p, sockfd=%d, client_info:%d", // request->server(), // httpd_req_to_sockfd(request->request()), // httpd_ws_get_fd_info(request->server()->server, httpd_req_to_sockfd(request->request()))); // dont forget to release our buffer memory free(buf); return ret; } PsychicWebSocketHandler* PsychicWebSocketHandler::onOpen(PsychicWebSocketClientCallback fn) { _onOpen = fn; return this; } PsychicWebSocketHandler* PsychicWebSocketHandler::onFrame(PsychicWebSocketFrameCallback fn) { _onFrame = fn; return this; } PsychicWebSocketHandler* PsychicWebSocketHandler::onClose(PsychicWebSocketClientCallback fn) { _onClose = fn; return this; } void PsychicWebSocketHandler::sendAll(httpd_ws_frame_t* ws_pkt) { for (PsychicClient* client : _clients) { // ESP_LOGD(PH_TAG, "Active client (fd=%d) -> sending async message", client->socket()); if (client->_friend == NULL) { continue; } if (((PsychicWebSocketClient*)client->_friend)->sendMessage(ws_pkt) != ESP_OK) break; } } void PsychicWebSocketHandler::sendAll(httpd_ws_type_t op, const void* data, size_t len) { httpd_ws_frame_t ws_pkt; memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); ws_pkt.payload = (uint8_t*)data; ws_pkt.len = len; ws_pkt.type = op; this->sendAll(&ws_pkt); } void PsychicWebSocketHandler::sendAll(const char* buf) { this->sendAll(HTTPD_WS_TYPE_TEXT, buf, strlen(buf)); } ================================================ FILE: src/PsychicWebSocket.h ================================================ #ifndef PsychicWebSocket_h #define PsychicWebSocket_h #include "PsychicCore.h" #include "PsychicRequest.h" class PsychicWebSocketRequest; class PsychicWebSocketClient; // callback function definitions typedef std::function PsychicWebSocketClientCallback; typedef std::function PsychicWebSocketFrameCallback; class PsychicWebSocketClient : public PsychicClient { protected: static void _sendMessageCallback(esp_err_t err, int socket, void* arg); public: PsychicWebSocketClient(PsychicClient* client); ~PsychicWebSocketClient(); esp_err_t sendMessage(httpd_ws_frame_t* ws_pkt); esp_err_t sendMessage(httpd_ws_type_t op, const void* data, size_t len); esp_err_t sendMessage(const char* buf); }; class PsychicWebSocketRequest : public PsychicRequest { private: PsychicWebSocketClient _client; public: PsychicWebSocketRequest(PsychicRequest* req); virtual ~PsychicWebSocketRequest(); PsychicWebSocketClient* client() override; esp_err_t reply(httpd_ws_frame_t* ws_pkt); esp_err_t reply(httpd_ws_type_t op, const void* data, size_t len); esp_err_t reply(const char* buf); }; class PsychicWebSocketHandler : public PsychicHandler { protected: PsychicWebSocketClientCallback _onOpen; PsychicWebSocketFrameCallback _onFrame; PsychicWebSocketClientCallback _onClose; public: PsychicWebSocketHandler(); ~PsychicWebSocketHandler(); PsychicWebSocketClient* getClient(int socket) override; PsychicWebSocketClient* getClient(PsychicClient* client) override; void addClient(PsychicClient* client) override; void removeClient(PsychicClient* client) override; void openCallback(PsychicClient* client) override; void closeCallback(PsychicClient* client) override; bool isWebSocket() override final; esp_err_t handleRequest(PsychicRequest* request, PsychicResponse* response) override; PsychicWebSocketHandler* onOpen(PsychicWebSocketClientCallback fn); PsychicWebSocketHandler* onFrame(PsychicWebSocketFrameCallback fn); PsychicWebSocketHandler* onClose(PsychicWebSocketClientCallback fn); void sendAll(httpd_ws_frame_t* ws_pkt); void sendAll(httpd_ws_type_t op, const void* data, size_t len); void sendAll(const char* buf); }; #endif // PsychicWebSocket_h ================================================ FILE: src/TemplatePrinter.cpp ================================================ /************************************************************ TemplatePrinter Class A basic templating engine for a stream of text. This wraps the Arduino Print interface and writes to any Print interface. Written by Christopher Andrews (https://github.com/Chris--A) ************************************************************/ #include "TemplatePrinter.h" void TemplatePrinter::resetParam(bool flush) { if (flush && _inParam) { _stream.write(_delimiter); if (_paramPos) _stream.print(_paramBuffer); } memset(_paramBuffer, 0, sizeof(_paramBuffer)); _paramPos = 0; _inParam = false; } void TemplatePrinter::flush() { resetParam(true); _stream.flush(); } size_t TemplatePrinter::write(uint8_t data) { if (data == _delimiter) { // End of parameter, send to callback if (_inParam) { // On false, return the parameter place holder as is: not a parameter // Bug fix: ignore parameters that are zero length. if (!_paramPos || !_cb(_stream, _paramBuffer)) { resetParam(true); _stream.write(data); } else { resetParam(false); } // Start collecting parameter } else { _inParam = true; } } else { // Are we collecting if (_inParam) { // Is param still valid if (isalnum(data) || data == '_') { // Total param len must be 63, 1 for null. if (_paramPos < sizeof(_paramBuffer) - 1) { _paramBuffer[_paramPos++] = data; // Not a valid param } else { resetParam(true); } } else { resetParam(true); _stream.write(data); } // Just output } else { _stream.write(data); } } return 1; } size_t TemplatePrinter::copyFrom(Stream& stream) { size_t count = 0; while (stream.available()) count += this->write(stream.read()); return count; } ================================================ FILE: src/TemplatePrinter.h ================================================ #ifndef TemplatePrinter_h #define TemplatePrinter_h #include "PsychicCore.h" #include /************************************************************ TemplatePrinter Class A basic templating engine for a stream of text. This wraps the Arduino Print interface and writes to any Print interface. Written by Christopher Andrews (https://github.com/Chris--A) ************************************************************/ class TemplatePrinter; typedef std::function TemplateCallback; typedef std::function TemplateSourceCallback; class TemplatePrinter : public Print { private: bool _inParam; char _paramBuffer[64]; uint8_t _paramPos; Print& _stream; TemplateCallback _cb; char _delimiter; void resetParam(bool flush); public: using Print::write; static void start(Print& stream, TemplateCallback cb, TemplateSourceCallback entry) { TemplatePrinter printer(stream, cb); entry(printer); } TemplatePrinter(Print& stream, TemplateCallback cb, const char delimeter = '%') : _stream(stream), _cb(cb), _delimiter(delimeter) { resetParam(false); } ~TemplatePrinter() { flush(); } void flush() override; size_t write(uint8_t data) override; size_t copyFrom(Stream& stream); }; #endif ================================================ FILE: src/UrlEncode.cpp ================================================ // MIT License // Copyright (c) Masayuki Sugahara // https://github.com/plageoj/urlencode #include "UrlEncode.h" String urlEncode(const char *msg) { const char *hex = "0123456789ABCDEF"; String encodedMsg = ""; while (*msg != '\0') { if ( ('a' <= *msg && *msg <= 'z') || ('A' <= *msg && *msg <= 'Z') || ('0' <= *msg && *msg <= '9') || *msg == '-' || *msg == '_' || *msg == '.' || *msg == '~') { encodedMsg += *msg; } else { encodedMsg += '%'; encodedMsg += hex[(unsigned char)*msg >> 4]; encodedMsg += hex[*msg & 0xf]; } msg++; } return encodedMsg; } String urlEncode(String msg) { return urlEncode(msg.c_str()); } ================================================ FILE: src/UrlEncode.h ================================================ // MIT License // Copyright (c) Masayuki Sugahara // https://github.com/plageoj/urlencode #ifndef _PLAGEOJ_URLENCODE_H #define _PLAGEOJ_URLENCODE_H #include /** * Percent-encodes a string. * @param msg UTF-8 string to encode. * @returns Percent-encoded string. */ String urlEncode(const char *msg); /** * Percent-encodes a string. * @param msg UTF-8 string to encode. * @returns Percent-encoded string. */ String urlEncode(String msg); #endif ================================================ FILE: src/async_worker.cpp ================================================ #include "async_worker.h" bool is_on_async_worker_thread(void) { // is our handle one of the known async handles? TaskHandle_t handle = xTaskGetCurrentTaskHandle(); for (int i = 0; i < ASYNC_WORKER_COUNT; i++) { if (worker_handles[i] == handle) { return true; } } return false; } // Submit an HTTP req to the async worker queue esp_err_t submit_async_req(httpd_req_t* req, httpd_req_handler_t handler) { // must create a copy of the request that we own httpd_req_t* copy = NULL; esp_err_t err = httpd_req_async_handler_begin(req, ©); if (err != ESP_OK) { return err; } httpd_async_req_t async_req = { .req = copy, .handler = handler, }; // How should we handle resource exhaustion? // In this example, we immediately respond with an // http error if no workers are available. int ticks = 0; // counting semaphore: if success, we know 1 or // more asyncReqTaskWorkers are available. if (xSemaphoreTake(worker_ready_count, ticks) == false) { ESP_LOGE(PH_TAG, "No workers are available"); httpd_req_async_handler_complete(copy); // cleanup return ESP_FAIL; } // Since worker_ready_count > 0 the queue should already have space. // But lets wait up to 100ms just to be safe. if (xQueueSend(async_req_queue, &async_req, pdMS_TO_TICKS(100)) == false) { ESP_LOGE(PH_TAG, "worker queue is full"); httpd_req_async_handler_complete(copy); // cleanup return ESP_FAIL; } return ESP_OK; } void async_req_worker_task(void* p) { ESP_LOGI(PH_TAG, "starting async req task worker"); while (true) { // counting semaphore - this signals that a worker // is ready to accept work xSemaphoreGive(worker_ready_count); // wait for a request httpd_async_req_t async_req; if (xQueueReceive(async_req_queue, &async_req, portMAX_DELAY)) { ESP_LOGI(PH_TAG, "invoking %s", async_req.req->uri); // call the handler async_req.handler(async_req.req); // Inform the server that it can purge the socket used for // this request, if needed. if (httpd_req_async_handler_complete(async_req.req) != ESP_OK) { ESP_LOGE(PH_TAG, "failed to complete async req"); } } } ESP_LOGW(PH_TAG, "worker stopped"); vTaskDelete(NULL); } void start_async_req_workers(void) { // counting semaphore keeps track of available workers worker_ready_count = xSemaphoreCreateCounting( ASYNC_WORKER_COUNT, // Max Count 0); // Initial Count if (worker_ready_count == NULL) { ESP_LOGE(PH_TAG, "Failed to create workers counting Semaphore"); return; } // create queue async_req_queue = xQueueCreate(1, sizeof(httpd_async_req_t)); if (async_req_queue == NULL) { ESP_LOGE(PH_TAG, "Failed to create async_req_queue"); vSemaphoreDelete(worker_ready_count); return; } // start worker tasks for (int i = 0; i < ASYNC_WORKER_COUNT; i++) { bool success = xTaskCreate(async_req_worker_task, "async_req_worker", ASYNC_WORKER_TASK_STACK_SIZE, // stack size (void*)0, // argument ASYNC_WORKER_TASK_PRIORITY, // priority &worker_handles[i]); if (!success) { ESP_LOGE(PH_TAG, "Failed to start asyncReqWorker"); continue; } } } /**** * * This code is backported from the 5.1.x branch * ****/ #ifndef ESP_HTTPD_HAS_ASYNC_API #ifndef MAX #define MAX(a, b) (((a) > (b)) ? (a) : (b)) #endif /* Calculate the maximum size needed for the scratch buffer */ #if ESP_ARDUINO_VERSION_MAJOR < 3 #define HTTPD_SCRATCH_BUF MAX(HTTPD_MAX_REQ_HDR_LEN, HTTPD_MAX_URI_LEN) #else #define HTTPD_SCRATCH_BUF MAX(CONFIG_HTTPD_MAX_REQ_HDR_LEN, CONFIG_HTTPD_MAX_URI_LEN) #endif /** * @brief Auxiliary data structure for use during reception and processing * of requests and temporarily keeping responses */ struct httpd_req_aux { struct sock_db* sd; /*!< Pointer to socket database */ char scratch[HTTPD_SCRATCH_BUF + 1]; /*!< Temporary buffer for our operations (1 byte extra for null termination) */ size_t remaining_len; /*!< Amount of data remaining to be fetched */ char* status; /*!< HTTP response's status code */ char* content_type; /*!< HTTP response's content type */ bool first_chunk_sent; /*!< Used to indicate if first chunk sent */ unsigned req_hdrs_count; /*!< Count of total headers in request packet */ unsigned resp_hdrs_count; /*!< Count of additional headers in response packet */ struct resp_hdr { const char* field; const char* value; }* resp_hdrs; /*!< Additional headers in response packet */ struct http_parser_url url_parse_res; /*!< URL parsing result, used for retrieving URL elements */ #ifdef CONFIG_HTTPD_WS_SUPPORT bool ws_handshake_detect; /*!< WebSocket handshake detection flag */ httpd_ws_type_t ws_type; /*!< WebSocket frame type */ bool ws_final; /*!< WebSocket FIN bit (final frame or not) */ uint8_t mask_key[4]; /*!< WebSocket mask key for this payload */ #endif }; #if ESP_ARDUINO_VERSION_MAJOR < 3 esp_err_t httpd_req_async_handler_begin(httpd_req_t *r, httpd_req_t **out) { if (r == NULL || out == NULL) { return ESP_ERR_INVALID_ARG; } // alloc async req httpd_req_t* async = (httpd_req_t*)malloc(sizeof(httpd_req_t)); if (async == NULL) { return ESP_ERR_NO_MEM; } memcpy((void*)async, (void*)r, sizeof(httpd_req_t)); // alloc async aux async->aux = (httpd_req_aux*)malloc(sizeof(struct httpd_req_aux)); if (async->aux == NULL) { free(async); return ESP_ERR_NO_MEM; } memcpy(async->aux, r->aux, sizeof(struct httpd_req_aux)); // not available in 4.4.x // mark socket as "in use" // struct httpd_req_aux *ra = r->aux; // ra->sd->for_async_req = true; *out = async; return ESP_OK; } esp_err_t httpd_req_async_handler_complete(httpd_req_t* r) { if (r == NULL) { return ESP_ERR_INVALID_ARG; } // not available in 4.4.x // struct httpd_req_aux *ra = (httpd_req_aux *)r->aux; // ra->sd->for_async_req = false; free(r->aux); free(r); return ESP_OK; } #endif #endif ================================================ FILE: src/async_worker.h ================================================ #ifndef async_worker_h #define async_worker_h #include "PsychicCore.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #define ASYNC_WORKER_TASK_PRIORITY 5 #define ASYNC_WORKER_TASK_STACK_SIZE (4 * 1024) #define ASYNC_WORKER_COUNT 8 // Detect presence of async API in your ESP-IDF #if defined(ESP_IDF_VERSION_MAJOR) #if (ESP_IDF_VERSION_MAJOR > 5) || (ESP_IDF_VERSION_MAJOR == 5 && ESP_IDF_VERSION_MINOR >= 1) #define ESP_HTTPD_HAS_ASYNC_API 1 #endif #endif // Async requests are queued here while they wait to be processed by the workers static QueueHandle_t async_req_queue; // Track the number of free workers at any given time static SemaphoreHandle_t worker_ready_count; // Each worker has its own thread static TaskHandle_t worker_handles[ASYNC_WORKER_COUNT]; typedef esp_err_t (*httpd_req_handler_t)(httpd_req_t* req); typedef struct { httpd_req_t* req; httpd_req_handler_t handler; } httpd_async_req_t; bool is_on_async_worker_thread(void); esp_err_t submit_async_req(httpd_req_t* req, httpd_req_handler_t handler); void async_req_worker_task(void* p); void start_async_req_workers(void); /**** * * This code is backported from the 5.1.x branch * ****/ #ifndef ESP_HTTPD_HAS_ASYNC_API esp_err_t httpd_req_async_handler_begin(httpd_req_t* r, httpd_req_t** out); esp_err_t httpd_req_async_handler_complete(httpd_req_t* r); #endif #endif // async_worker_h ================================================ FILE: src/http_status.cpp ================================================ #include "http_status.h" bool http_informational(int code) { return code >= 100 && code < 200; } bool http_success(int code) { return code >= 200 && code < 300; } bool http_redirection(int code) { return code >= 300 && code < 400; } bool http_client_error(int code) { return code >= 400 && code < 500; } bool http_server_error(int code) { return code >= 500 && code < 600; } bool http_failure(int code) { return code >= 400 && code < 600; } const char* http_status_group(int code) { if (http_informational(code)) return "Informational"; if (http_success(code)) return "Success"; if (http_redirection(code)) return "Redirection"; if (http_client_error(code)) return "Client Error"; if (http_server_error(code)) return "Server Error"; return "Unknown"; } const char* http_status_reason(int code) { switch (code) { /*####### 1xx - Informational #######*/ case 100: return "Continue"; case 101: return "Switching Protocols"; case 102: return "Processing"; case 103: return "Early Hints"; /*####### 2xx - Successful #######*/ case 200: return "OK"; case 201: return "Created"; case 202: return "Accepted"; case 203: return "Non-Authoritative Information"; case 204: return "No Content"; case 205: return "Reset Content"; case 206: return "Partial Content"; case 207: return "Multi-Status"; case 208: return "Already Reported"; case 226: return "IM Used"; /*####### 3xx - Redirection #######*/ case 300: return "Multiple Choices"; case 301: return "Moved Permanently"; case 302: return "Found"; case 303: return "See Other"; case 304: return "Not Modified"; case 305: return "Use Proxy"; case 307: return "Temporary Redirect"; case 308: return "Permanent Redirect"; /*####### 4xx - Client Error #######*/ case 400: return "Bad Request"; case 401: return "Unauthorized"; case 402: return "Payment Required"; case 403: return "Forbidden"; case 404: return "Not Found"; case 405: return "Method Not Allowed"; case 406: return "Not Acceptable"; case 407: return "Proxy Authentication Required"; case 408: return "Request Timeout"; case 409: return "Conflict"; case 410: return "Gone"; case 411: return "Length Required"; case 412: return "Precondition Failed"; case 413: return "Content Too Large"; case 414: return "URI Too Long"; case 415: return "Unsupported Media Type"; case 416: return "Range Not Satisfiable"; case 417: return "Expectation Failed"; case 418: return "I'm a teapot"; case 421: return "Misdirected Request"; case 422: return "Unprocessable Content"; case 423: return "Locked"; case 424: return "Failed Dependency"; case 425: return "Too Early"; case 426: return "Upgrade Required"; case 428: return "Precondition Required"; case 429: return "Too Many Requests"; case 431: return "Request Header Fields Too Large"; case 451: return "Unavailable For Legal Reasons"; /*####### 5xx - Server Error #######*/ case 500: return "Internal Server Error"; case 501: return "Not Implemented"; case 502: return "Bad Gateway"; case 503: return "Service Unavailable"; case 504: return "Gateway Timeout"; case 505: return "HTTP Version Not Supported"; case 506: return "Variant Also Negotiates"; case 507: return "Insufficient Storage"; case 508: return "Loop Detected"; case 510: return "Not Extended"; case 511: return "Network Authentication Required"; default: return "Unknown"; } } ================================================ FILE: src/http_status.h ================================================ #ifndef MICRO_HTTP_STATUS_H #define MICRO_HTTP_STATUS_H #include bool http_informational(int code); bool http_success(int code); bool http_redirection(int code); bool http_client_error(int code); bool http_server_error(int code); bool http_failure(int code); const char* http_status_group(int code); const char* http_status_reason(int code); #endif // MICRO_HTTP_STATUS_H