{"id":21068928,"url":"https://github.com/kamchatka-volcano/asyncgi","last_synced_at":"2025-05-16T03:33:45.628Z","repository":{"id":58071644,"uuid":"494768889","full_name":"kamchatka-volcano/asyncgi","owner":"kamchatka-volcano","description":"An asynchronous FastCGI web microframework for C++","archived":false,"fork":false,"pushed_at":"2024-08-24T14:12:17.000Z","size":276,"stargazers_count":36,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-30T02:03:28.906Z","etag":null,"topics":["asio","async","asynchronous","cpp17","fastcgi","microframework","webframework"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"ms-pl","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kamchatka-volcano.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-21T11:52:14.000Z","updated_at":"2024-06-12T13:48:59.000Z","dependencies_parsed_at":"2024-01-03T10:25:24.934Z","dependency_job_id":"53a43628-fa8b-4fee-a3e8-1fcde33a9d56","html_url":"https://github.com/kamchatka-volcano/asyncgi","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamchatka-volcano%2Fasyncgi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamchatka-volcano%2Fasyncgi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamchatka-volcano%2Fasyncgi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kamchatka-volcano%2Fasyncgi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kamchatka-volcano","download_url":"https://codeload.github.com/kamchatka-volcano/asyncgi/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225405230,"owners_count":17469309,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asio","async","asynchronous","cpp17","fastcgi","microframework","webframework"],"created_at":"2024-11-19T18:29:30.605Z","updated_at":"2024-11-19T18:29:31.235Z","avatar_url":"https://github.com/kamchatka-volcano.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg height=\"128\" src=\"doc/logo.png\"/\u003e\n\u003c/p\u003e\n\n[![build \u0026 test (clang, gcc, MSVC)](https://github.com/kamchatka-volcano/asyncgi/actions/workflows/build_and_test.yml/badge.svg?branch=master)](https://github.com/kamchatka-volcano/asyncgi/actions/workflows/build_and_test.yml)\n\n**asyncgi** - is a C++17 asynchronous microframework for creating web applications interfacing with any HTTP server\nsupporting [FastCGI](https://en.wikipedia.org/wiki/FastCGI) protocol. It aims to provide a modern way of\nusing [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface), with a custom performant FastCGI implementation in\nC++, multithreading support and a clean and simple API:\n\n```c++\n#include \u003casyncgi/asyncgi.h\u003e\n\nnamespace http = asyncgi::http;\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto router = asyncgi::Router{io};\n    router.route(\"/\", http::RequestMethod::Get).process(\n        [](const asyncgi::Request\u0026)\n        {\n            return http::Response{\"Hello world\"};\n        });\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n## Table of Contents\n*    [Usage](#usage)\n     * [Connection](#connection)\n     * [Request processor](#request-processor)\n     * [Router](#router)\n     * [Route parameters](#route-parameters)\n     * [Route context](#route-context)\n     * [Route matchers](#route-matchers)\n     * [Complete guest book example](#complete-guest-book-example)\n     * [Timer](#timer)\n     * [Client](#client)\n     * [Executing an asio task](#executing-an-asio-task)\n* [Showcase](#showcase)\n* [Development status](#development-status)\n*    [Installation](#installation)\n* [Building examples](#building-examples)\n* [Running functional tests](#running-functional-tests)\n* [License](#license)\n\n## Usage\n\n### Connection\nWeb applications developed with `asyncgi` require to establish a FastCGI connection with a web server handling HTTP requests. Most popular servers provide this functionality, for example `NGINX` can be used with a following configuration:\n\n```\nserver {\n\tlisten 8088;\n\tserver_name localhost;\n\t\n\tlocation / {\n\t\ttry_files $uri @fcgi;\n\t}\n\t\n\tlocation @fcgi {\t\n\t\tfastcgi_pass  unix:/tmp/fcgi.sock;\t\t\n\t\t#or using a TCP socket\n\t\t#fastcgi_pass localhost:9000;\n\t\tinclude fastcgi_params;\n\t}\n}\n\n```\n\n`asyncgi` supports both `UNIX domain` and `TCP` sockets for opening `FastCGI` connections.\n\n### Request processor\n\nIn order to process requests, it's necessary to provide a function or function object that fulfills\nthe `RequestProcessor` requirement. This means that the function must be invocable with one of the following signatures:\n\n* `http::Response (const asyncgi::Request\u0026)`\n* `void (const asyncgi::Request\u0026, asyncgi::Responder\u0026)`.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_request_processor.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n\nnamespace http = asyncgi::http;\n\nhttp::Response guestBookPage(const asyncgi::Request\u0026 request)\n{\n    if (request.path() == \"/\")\n        return {R\"(\n                \u003ch1\u003eGuest book\u003c/h1\u003e\n                \u003cp\u003eNo messages\u003c/p\u003e\n            )\"};\n\n    return http::ResponseStatus::_404_Not_Found;\n}\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto server = asyncgi::Server{io, guestBookPage};\n    //Listen for FastCGI connections on UNIX domain socket\n    server.listen(\"/tmp/fcgi.sock\");\n    //or over TCP\n    //server.listen(\"127.0.0.1\", 9088);\n    io.run();\n    return 0;\n}\n```\n\nHere, the `guestBookPage` function serves as the request processor. Another way to implement it is by accepting a\nreference to the `asyncgi::Responder` object, which can be used for sending responses manually:\n\n```c++\nvoid guestBookPage(const asyncgi::Request\u0026 request, asyncgi::Responder\u0026 responder)\n{\n    if (request.path() == \"/\")\n        responder.send(R\"(\n                \u003ch1\u003eGuest book\u003c/h1\u003e\n                \u003cp\u003eNo messages\u003c/p\u003e\n            )\");\n\n    return responder.send(http::ResponseStatus::_404_Not_Found);\n}\n```\n\nThis approach tends to be more verbose and error-prone, therefore it is preferable to use it only when access to\nasyncgi::Responder is required for initiating asynchronous operations from the request processor. These cases are\ncovered in the later parts of this document.\n\n\u003c/details\u003e\n\n### Router\n\nMultiple request processors can be registered in the `asyncgi::Router` object, where they are matched to the paths\nspecified in requests. The `asyncgi::Router` itself satisfies the `RequestProcessor` requirement.\n\nIf multiple threads are required for request processing, the desired number of workers can be passed to\nthe `asyncgi::IO` object's constructor. In such cases, the user must ensure that any shared data in the request\nprocessors is protected from concurrent read/write access.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_router.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cmutex\u003e\n\nnamespace http = asyncgi::http;\nusing namespace std::string_literals;\n\nclass GuestBookState {\npublic:\n    std::vector\u003cstd::string\u003e messages()\n    {\n        auto lock = std::scoped_lock{mutex_};\n        return messages_;\n    }\n\n    void addMessage(const std::string\u0026 msg)\n    {\n        auto lock = std::scoped_lock{mutex_};\n        messages_.emplace_back(msg);\n    }\n\nprivate:\n    std::vector\u003cstd::string\u003e messages_;\n    std::mutex mutex_;\n};\n\nclass GuestBookPage {\npublic:\n    GuestBookPage(GuestBookState\u0026 state)\n        : state_(\u0026state)\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026)\n    {\n        auto messages = state_-\u003emessages();\n        auto page = \"\u003ch1\u003eGuest book\u003c/h1\u003e\"s;\n        if (messages.empty())\n            page += \"\u003cp\u003eNo messages\u003c/p\u003e\";\n        else\n            for (const auto\u0026 msg : messages)\n                page += \"\u003cp\u003e\" + msg + \"\u003c/p\u003e\";\n\n        page += \"\u003chr\u003e\";\n        page += \"\u003cform method=\\\"post\\\" enctype=\\\"multipart/form-data\\\"\u003e\"\n                \"\u003clabel for=\\\"msg\\\"\u003eMessage:\u003c/label\u003e\"\n                \"\u003cinput id=\\\"msg\\\" name=\\\"msg\\\" value=\\\"\\\"\u003e\"\n                \"\u003cinput value=\\\"Submit\\\" data-popup=\\\"true\\\" type=\\\"submit\\\"\u003e\"\n                \"\u003c/form\u003e\";\n        return page;\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nclass GuestBookAddMessage {\npublic:\n    GuestBookAddMessage(GuestBookState\u0026 state)\n        : state_(\u0026state)\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026 request)\n    {\n        state_-\u003eaddMessage(std::string{request.formField(\"msg\")});\n        return http::Redirect{\"/\"};\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nint main()\n{\n    auto io = asyncgi::IO{4}; //4 threads processing requests\n    auto state = GuestBookState{};\n    auto router = asyncgi::Router{io};\n    router.route(\"/\", http::RequestMethod::Get).process\u003cGuestBookPage\u003e(state);\n    router.route(\"/\", http::RequestMethod::Post).process\u003cGuestBookAddMessage\u003e(state);\n    router.route().set(http::Response{http::ResponseStatus::_404_Not_Found, \"Page not found\"});\n    //Alternatively, it's possible to pass arguments for creation of http::Response object to the set() method.\n    //router.route().set(http::ResponseStatus::Code_404_Not_Found, \"Page not found\");\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n### Route parameters\n\nWhen using `asyncgi::Router` with regular expressions, request processors must satisfy\nthe `ParametrizedRequestProcessor` requirement. That means that a function object must be invocable with one of the\nfollowing signatures:\n\n* `http::Response void(const TRouteParams\u0026..., const asyncgi::Request\u0026)`\n* `void (const TRouteParams\u0026..., const asyncgi::Request\u0026, asyncgi::Responder\u0026)`\n\nThe `TRouteParams` represents zero or more parameters generated from the capturing groups of the regular expression. For\nexample, `http::Response (int age, string name, const asyncgi::Request\u0026)` signature can be used to process\nrequests matched by `asyncgi::rx{\"/person/(\\\\w+)/age/(\\\\d+)\"}`.\n\nIn the following example a `ParametrizedRequestProcessor` named `GuestBookRemoveMessage` is added to remove the stored\nguest book messages:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_route_params.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cmutex\u003e\n\nusing namespace asyncgi;\nusing namespace std::string_literals;\n\nclass GuestBookState {\npublic:\n    std::vector\u003cstd::string\u003e messages()\n    {\n        auto lock = std::scoped_lock{mutex_};\n        return messages_;\n    }\n\n    void addMessage(const std::string\u0026 msg)\n    {\n        auto lock = std::scoped_lock{mutex_};\n        messages_.emplace_back(msg);\n    }\n\n    void removeMessage(int index)\n    {\n        auto lock = std::scoped_lock{mutex_};\n        if (index \u003c 0 || index \u003e= static_cast\u003cint\u003e(messages_.size()))\n            return;\n        messages_.erase(std::next(messages_.begin(), index));\n    }\n\nprivate:\n    std::vector\u003cstd::string\u003e messages_;\n    std::mutex mutex_;\n};\n\nstd::string makeMessage(int index, const std::string\u0026 msg)\n{\n    return msg + R\"(\u003cform action=\"/delete/)\" + std::to_string(index) +\n            R\"(\" method=\"post\"\u003e \u003cinput value=\"Delete\" type=\"submit\"\u003e \u003c/form\u003e\u003c/div\u003e)\";\n}\n\nclass GuestBookPage {\npublic:\n    explicit GuestBookPage(GuestBookState\u0026 state)\n        : state_{\u0026state}\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026)\n    {\n        auto messages = state_-\u003emessages();\n        auto page = \"\u003ch1\u003eGuest book\u003c/h1\u003e\"s;\n        if (messages.empty())\n            page += \"\u003cp\u003eNo messages\u003c/p\u003e\";\n        else\n            for (auto i = 0; i \u003c static_cast\u003cint\u003e(messages.size()); ++i)\n                page += \"\u003cp\u003e\" + makeMessage(i, messages.at(i)) + \"\u003c/p\u003e\";\n\n        page += \"\u003chr\u003e\";\n        page += \"\u003cform method=\\\"post\\\" enctype=\\\"multipart/form-data\\\"\u003e\"\n                \"\u003clabel for=\\\"msg\\\"\u003eMessage:\u003c/label\u003e\"\n                \"\u003cinput id=\\\"msg\\\" name=\\\"msg\\\" value=\\\"\\\"\u003e\"\n                \"\u003cinput value=\\\"Submit\\\" data-popup=\\\"true\\\" type=\\\"submit\\\"\u003e\"\n                \"\u003c/form\u003e\";\n        return page;\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nclass GuestBookAddMessage {\npublic:\n    explicit GuestBookAddMessage(GuestBookState\u0026 state)\n        : state_{\u0026state}\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026 request)\n    {\n        state_-\u003eaddMessage(std::string{request.formField(\"msg\")});\n        return http::Redirect{\"/\"};\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nclass GuestBookRemoveMessage {\npublic:\n    explicit GuestBookRemoveMessage(GuestBookState\u0026 state)\n        : state_{\u0026state}\n    {\n    }\n\n    http::Response operator()(int index, const asyncgi::Request\u0026)\n    {\n        state_-\u003eremoveMessage(index);\n        return http::Redirect{\"/\"};\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nint main()\n{\n    auto io = asyncgi::IO{4};\n    auto state = GuestBookState{};\n    auto router = asyncgi::Router{io};\n    router.route(\"/\", http::RequestMethod::Get).process\u003cGuestBookPage\u003e(state);\n    router.route(\"/\", http::RequestMethod::Post).process\u003cGuestBookAddMessage\u003e(state);\n    router.route(asyncgi::rx{\"/delete/(.+)\"}, http::RequestMethod::Post).process\u003cGuestBookRemoveMessage\u003e(state);\n    router.route().set(http::ResponseStatus::_404_Not_Found, \"Page not found\");\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\nRegular expression capture groups are transformed into request processor arguments using `std::stringstream`. In order\nto support request processors with user-defined parameter types, it is necessary to provide a specialization\nof `asyncgi::config::StringConverter` class template. The previous example has been modified to reformat\nthe `GuestBookRemoveMessage` request processor to use the `MessageNumber` structure as a request processor argument:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_route_params_user_defined_types.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cmutex\u003e\n\nusing namespace asyncgi;\nusing namespace std::string_literals;\n\nstruct MessageNumber {\n    int value;\n};\n\ntemplate\u003c\u003e\nstruct asyncgi::config::StringConverter\u003cMessageNumber\u003e {\n    static std::optional\u003cMessageNumber\u003e fromString(const std::string\u0026 data)\n    {\n        return MessageNumber{std::stoi(data)};\n    }\n};\n\nclass GuestBookState {\npublic:\n    std::vector\u003cstd::string\u003e messages()\n    {\n        auto lock = std::scoped_lock{mutex_};\n        return messages_;\n    }\n\n    void addMessage(const std::string\u0026 msg)\n    {\n        auto lock = std::scoped_lock{mutex_};\n        messages_.emplace_back(msg);\n    }\n\n    void removeMessage(int index)\n    {\n        auto lock = std::scoped_lock{mutex_};\n        if (index \u003c 0 || index \u003e= static_cast\u003cint\u003e(messages_.size()))\n            return;\n        messages_.erase(std::next(messages_.begin(), index));\n    }\n\nprivate:\n    std::vector\u003cstd::string\u003e messages_;\n    std::mutex mutex_;\n};\n\nstd::string makeMessage(int index, const std::string\u0026 msg)\n{\n    return msg + R\"(\u003cform action=\"/delete/)\" + std::to_string(index) +\n            R\"(\" method=\"post\"\u003e \u003cinput value=\"Delete\" type=\"submit\"\u003e \u003c/form\u003e\u003c/div\u003e)\";\n}\n\nclass GuestBookPage {\npublic:\n    explicit GuestBookPage(GuestBookState\u0026 state)\n        : state_{\u0026state}\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026)\n    {\n        auto messages = state_-\u003emessages();\n        auto page = \"\u003ch1\u003eGuest book\u003c/h1\u003e\"s;\n        if (messages.empty())\n            page += \"\u003cp\u003eNo messages\u003c/p\u003e\";\n        else\n            for (auto i = 0; i \u003c static_cast\u003cint\u003e(messages.size()); ++i)\n                page += \"\u003cp\u003e\" + makeMessage(i, messages.at(i)) + \"\u003c/p\u003e\";\n\n        page += \"\u003chr\u003e\";\n        page += \"\u003cform method=\\\"post\\\" enctype=\\\"multipart/form-data\\\"\u003e\"\n                \"\u003clabel for=\\\"msg\\\"\u003eMessage:\u003c/label\u003e\"\n                \"\u003cinput id=\\\"msg\\\" name=\\\"msg\\\" value=\\\"\\\"\u003e\"\n                \"\u003cinput value=\\\"Submit\\\" data-popup=\\\"true\\\" type=\\\"submit\\\"\u003e\"\n                \"\u003c/form\u003e\";\n        return page;\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nclass GuestBookAddMessage {\npublic:\n    explicit GuestBookAddMessage(GuestBookState\u0026 state)\n        : state_{\u0026state}\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026 request)\n    {\n        state_-\u003eaddMessage(std::string{request.formField(\"msg\")});\n        return http::Redirect{\"/\"};\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nclass GuestBookRemoveMessage {\npublic:\n    explicit GuestBookRemoveMessage(GuestBookState\u0026 state)\n        : state_{\u0026state}\n    {\n    }\n\n    http::Response operator()(MessageNumber msgNumber, const asyncgi::Request\u0026)\n    {\n        state_-\u003eremoveMessage(msgNumber.value);\n        return http::Redirect{\"/\"};\n    }\n\nprivate:\n    GuestBookState* state_;\n};\n\nint main()\n{\n    auto io = asyncgi::IO{4};\n    auto state = GuestBookState{};\n    auto router = asyncgi::Router{io};\n    router.route(\"/\", http::RequestMethod::Get).process\u003cGuestBookPage\u003e(state);\n    router.route(\"/\", http::RequestMethod::Post).process\u003cGuestBookAddMessage\u003e(state);\n    router.route(asyncgi::rx{\"/delete/(.+)\"}, http::RequestMethod::Post).process\u003cGuestBookRemoveMessage\u003e(state);\n    router.route().set(http::ResponseStatus::_404_Not_Found, \"Page not found\");\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n### Route context\n\nWhen using `asyncgi::Router`, it is possible to specify a template argument for a context structure type. This\nstructure is then passed to the `ContextualRequestProcessor` functions and can be accessed and modified throughout the\nrequest processing for multiple routes. The `ContextualRequestProcessor` is a `RequestProcessor` that takes an\nadditional argument referring to the context object.  \nA single request can match multiple routes, as long as all preceding request processors do not send any response. To\navoid sending responses request processors can use the `std::optional\u003chttp::Response\u003e` signature and return empty\nvalues. This\nallows using `asyncgi::Router` to register middleware-like processors, which primarily modify the route context for\nsubsequent processors.\n\nThe next example demonstrates how a route context can be used for storing authorization information:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_route_context.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cmutex\u003e\n#include \u003coptional\u003e\n\nnamespace http = asyncgi::http;\nusing namespace std::string_literals;\n\nenum class AccessRole {\n    Admin,\n    Guest\n};\n\nstruct RouteContext {\n    AccessRole role = AccessRole::Guest;\n};\n\nstruct AdminAuthorizer {\n    std::optional\u003chttp::Response\u003e operator()(const asyncgi::Request\u0026 request, RouteContext\u0026 context)\n    {\n        if (request.cookie(\"admin_id\") == \"ADMIN_SECRET\")\n            context.role = AccessRole::Admin;\n\n        return std::nullopt;\n    }\n};\n\nstruct LoginPage {\n    http::Response operator()(const asyncgi::Request\u0026, RouteContext\u0026 context)\n    {\n        if (context.role == AccessRole::Guest)\n            return {R\"(\n                    \u003chtml\u003e\n                    \u003cform method=\"post\" enctype=\"multipart/form-data\"\u003e\n                    \u003clabel for=\"msg\"\u003eLogin:\u003c/label\u003e\n                    \u003cinput id=\"login\" name=\"login\" value=\"\"\u003e\n                    \u003clabel for=\"msg\"\u003ePassword:\u003c/label\u003e\n                    \u003cinput id=\"passwd\" name=\"passwd\" value=\"\"\u003e\n                    \u003cinput value=\"Submit\" data-popup=\"true\" type=\"submit\"\u003e\n                    \u003c/form\u003e\u003c/html\u003e)\"};\n        else //We are already logged in as the administrator\n            return http::Redirect{\"/\"};\n    }\n};\n\nstruct LoginPageAuthorize {\n    http::Response operator()(const asyncgi::Request\u0026 request, RouteContext\u0026 context)\n    {\n        if (context.role == AccessRole::Guest) {\n            if (request.formField(\"login\") == \"admin\" \u0026\u0026 request.formField(\"passwd\") == \"12345\")\n                return {http::Redirect{\"/\"}, {asyncgi::http::Cookie(\"admin_id\", \"ADMIN_SECRET\")}};\n            else\n                return http::Redirect{\"/login\"};\n        }\n        else //We are already logged in as the administrator\n            return http::Redirect{\"/\"};\n    }\n};\n\nint main()\n{\n    auto io = asyncgi::IO{4}; //4 threads processing requests\n    auto router = asyncgi::Router\u003cRouteContext\u003e{io};\n    router.route(asyncgi::rx{\".*\"}).process\u003cAdminAuthorizer\u003e();\n    router.route(\"/\").process(\n            [](const asyncgi::Request\u0026, asyncgi::Responder\u0026 response, RouteContext\u0026 context)\n            {\n                if (context.role == AccessRole::Admin)\n                    response.send(\"\u003cp\u003eHello admin\u003c/p\u003e\");\n                else\n                    response.send(R\"(\u003cp\u003eHello guest\u003c/p\u003e\u003cp\u003e\u003ca href=\"/login\"\u003elogin\u003c/a\u003e)\");\n            });\n\n    router.route(\"/login\", http::RequestMethod::Get).process\u003cLoginPage\u003e();\n    router.route(\"/login\", http::RequestMethod::Post).process\u003cLoginPageAuthorize\u003e();\n    router.route().set(http::ResponseStatus::_404_Not_Found, \"Page not found\");\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n### Route matchers\n\nAny parameter of request or context objects can be registered for route matching\nin `asyncgi::Router::route()` method. To achieve this, it is required to provide a specialization of\nthe `asyncgi::config::RouteMatcher` class template and implement a comparator bool operator() within it. Let's see how\nto register the enum class `Access` from the previous example as a route matcher:\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_route_matcher.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cmutex\u003e\n#include \u003coptional\u003e\n\nnamespace http = asyncgi::http;\nusing namespace std::string_literals;\n\nenum class AccessRole {\n    Admin,\n    Guest\n};\n\nstruct RouteContext {\n    AccessRole role = AccessRole::Guest;\n};\n\nstruct AdminAuthorizer {\n    std::optional\u003chttp::Response\u003e operator()(const asyncgi::Request\u0026 request, RouteContext\u0026 context)\n    {\n        if (request.cookie(\"admin_id\") == \"ADMIN_SECRET\")\n            context.role = AccessRole::Admin;\n\n        return std::nullopt;\n    }\n};\n\nstruct LoginPage {\n    http::Response operator()(const asyncgi::Request\u0026)\n    {\n        return {R\"(\n                \u003chtml\u003e\n                \u003cform method=\"post\" enctype=\"multipart/form-data\"\u003e\n                \u003clabel for=\"msg\"\u003eLogin:\u003c/label\u003e\n                \u003cinput id=\"login\" name=\"login\" value=\"\"\u003e\n                \u003clabel for=\"msg\"\u003ePassword:\u003c/label\u003e\n                \u003cinput id=\"passwd\" name=\"passwd\" value=\"\"\u003e\n                \u003cinput value=\"Submit\" data-popup=\"true\" type=\"submit\"\u003e\n                \u003c/form\u003e\u003c/html\u003e)\"};\n    }\n};\n\nstruct LoginPageAuthorize {\n    http::Response operator()(const asyncgi::Request\u0026 request)\n    {\n        if (request.formField(\"login\") == \"admin\" \u0026\u0026 request.formField(\"passwd\") == \"12345\")\n            return {http::Redirect{\"/\"}, {asyncgi::http::Cookie(\"admin_id\", \"ADMIN_SECRET\")}};\n\n        return http::Redirect{\"/login\"};\n    }\n};\n\ntemplate\u003c\u003e\nstruct asyncgi::config::RouteMatcher\u003cAccessRole, RouteContext\u003e {\n    bool operator()(AccessRole value, const asyncgi::Request\u0026, const RouteContext\u0026 context) const\n    {\n        return value == context.role;\n    }\n};\n\nint main()\n{\n    auto io = asyncgi::IO{4};\n    auto router = asyncgi::Router\u003cRouteContext\u003e{io};\n    router.route(asyncgi::rx{\".*\"}).process\u003cAdminAuthorizer\u003e();\n    router.route(\"/\").process(\n            [](const asyncgi::Request\u0026, RouteContext\u0026 context) -\u003e http::Response\n            {\n                if (context.role == AccessRole::Admin)\n                    return {\"\u003cp\u003eHello admin\u003c/p\u003e\"};\n                else\n                    return {R\"(\u003cp\u003eHello guest\u003c/p\u003e\u003cp\u003e\u003ca href=\"/login\"\u003elogin\u003c/a\u003e)\"};\n            });\n\n    router.route(\"/login\", http::RequestMethod::Get, AccessRole::Guest).process\u003cLoginPage\u003e();\n    router.route(\"/login\", http::RequestMethod::Post, AccessRole::Guest).process\u003cLoginPageAuthorize\u003e();\n    router.route(\"/login\", http::RequestMethod::Get, AccessRole::Admin).set(\"/\", http::RedirectType::Found);\n    router.route(\"/login\", http::RequestMethod::Post, AccessRole::Admin).set(\"/\", http::RedirectType::Found);\n    router.route().set(http::ResponseStatus::_404_Not_Found, \"Page not found\");\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n### Complete guest book example\n\nLet's combine all the previous examples to create a simple guest book.  \nThe messages in the guest book will only persist during the application runtime, as they are stored in a `std::vector`.\nThe admin credentials for logging in are as follows: login: `admin`, password: `12345`. The admin account has the\nability to delete posts.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_guestbook.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cmutex\u003e\n#include \u003coptional\u003e\n#include \u003cregex\u003e\n\nnamespace http = asyncgi::http;\nusing namespace std::string_literals;\n\nenum class AccessRole {\n    Admin,\n    Guest\n};\n\nstruct RouteContext {\n    AccessRole role = AccessRole::Guest;\n};\n\ntemplate\u003c\u003e\nstruct asyncgi::config::RouteMatcher\u003cAccessRole, RouteContext\u003e {\n    bool operator()(AccessRole value, const asyncgi::Request\u0026, const RouteContext\u0026 context) const\n    {\n        return value == context.role;\n    }\n};\n\nstd::optional\u003chttp::Response\u003e authorizeAdmin(const asyncgi::Request\u0026 request, RouteContext\u0026 context)\n{\n    if (request.cookie(\"admin_id\") == \"ADMIN_SECRET\")\n        context.role = AccessRole::Admin;\n\n    return std::nullopt;\n}\n\nhttp::Response showLoginPage(const asyncgi::Request\u0026)\n{\n    return {R\"(\n            \u003chead\u003e\u003clink rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\"\u003e\u003c/head\u003e\n            \u003cform method=\"post\" enctype=\"multipart/form-data\"\u003e\n                \u003clabel for=\"msg\"\u003eLogin:\u003c/label\u003e\n                \u003cinput id=\"login\" name=\"login\" value=\"\"\u003e\n                \u003clabel for=\"msg\"\u003ePassword:\u003c/label\u003e\n                \u003cinput id=\"passwd\" name=\"passwd\" value=\"\"\u003e\n                \u003cinput value=\"Submit\" data-popup=\"true\" type=\"submit\"\u003e\n            \u003c/form\u003e)\"};\n}\n\nhttp::Response loginAdmin(const asyncgi::Request\u0026 request)\n{\n    if (request.formField(\"login\") == \"admin\" \u0026\u0026 request.formField(\"passwd\") == \"12345\")\n        return {http::Redirect{\"/\"}, {http::Cookie(\"admin_id\", \"ADMIN_SECRET\")}};\n    else\n        return http::Redirect{\"/login\"};\n}\n\nhttp::Response logoutAdmin(const asyncgi::Request\u0026)\n{\n    return {http::Redirect{\"/\"}, {http::Cookie(\"admin_id\", \"\")}};\n}\n\nstruct GuestBookMessage {\n    std::string name;\n    std::string text;\n};\n\nclass GuestBookState {\npublic:\n    std::vector\u003cGuestBookMessage\u003e messages()\n    {\n        auto lock = std::scoped_lock{mutex_};\n        return messages_;\n    }\n\n    void addMessage(std::string name, std::string msg)\n    {\n        name = std::regex_replace(name, std::regex{\"\u003c\"}, \"\u0026lt;\");\n        name = std::regex_replace(name, std::regex{\"\u003e\"}, \"\u0026gt;\");\n        msg = std::regex_replace(msg, std::regex{\"\u003c\"}, \"\u0026lt;\");\n        msg = std::regex_replace(msg, std::regex{\"\u003e\"}, \"\u0026gt;\");\n        auto lock = std::scoped_lock{mutex_};\n        messages_.emplace_back(GuestBookMessage{name.empty() ? \"Guest\" : name, msg});\n    }\n\n    void removeMessage(int index)\n    {\n        auto lock = std::scoped_lock{mutex_};\n        if (index \u003c 0 || index \u003e= static_cast\u003cint\u003e(messages_.size()))\n            return;\n        messages_.erase(std::next(messages_.begin(), index));\n    }\n\nprivate:\n    std::vector\u003cGuestBookMessage\u003e messages_;\n    std::mutex mutex_;\n};\n\nstd::string makeMessagesDiv(const std::vector\u003cGuestBookMessage\u003e\u0026 messages, AccessRole role)\n{\n    if (messages.empty())\n        return \"\u003cdiv\u003eNo messages\u003c/div\u003e\";\n\n    auto messagesDiv = std::string{};\n    for (auto i = 0; i \u003c static_cast\u003cint\u003e(messages.size()); ++i) {\n        messagesDiv += \"\u003ch4\u003e\" + messages.at(i).name + \" says:\u003c/hr\u003e\u003cpre\u003e\" + messages.at(i).text + \"\u003c/pre\u003e\";\n        if (role == AccessRole::Admin)\n            messagesDiv += R\"(\u003cform action=\"/delete/)\" + std::to_string(i) +\n                    R\"(\" method=\"post\"\u003e \u003cinput value=\"Delete\" type=\"submit\"\u003e\u003c/form\u003e)\";\n    }\n    return messagesDiv;\n}\n\nstd::string makeLinksDiv(AccessRole role)\n{\n    return (role == AccessRole::Admin ? R\"(\u003ca href=\"/logout\"\u003elogout\u003c/a\u003e\u0026nbsp;)\"s\n                                      : R\"(\u003ca href=\"/login\"\u003elogin\u003c/a\u003e\u0026nbsp;)\"s) +\n            R\"(\u003ca href=\"https://github.com/kamchatka-volcano/asyncgi/blob/master/examples/example_guestbook.cpp\"\u003esource\u003c/a\u003e\u003c/div\u003e)\"s;\n}\n\nauto showGuestBookPage(GuestBookState\u0026 state)\n{\n    return [\u0026state](const asyncgi::Request\u0026 request, RouteContext\u0026 context) -\u003e http::Response\n    {\n        auto page = R\"(\u003chead\u003e\u003clink rel=\"stylesheet\" href=\"https://cdn.simplecss.org/simple.min.css\"\u003e\u003c/head\u003e\n                       \u003cdiv style=\"display:flex; flex-direction: row; justify-content: flex-end\"\u003e%LINKS%\u003c/div\u003e\n                       \u003cdiv style=\"display:flex; flex-direction: column; height: calc(100vh - 5em);\"\u003e\n                           \u003ch1\u003easyncgi guest book\u003c/h1\u003e\n                           \u003cdiv style=\"flex: 1; min-height: 0; display:flex; flex-direction: column;\"\u003e\n                               \u003cdiv style=\"flex:1; overflow-y: auto; row-gap: 60px\"\u003e\n                                    %MESSAGES%\n                               \u003c/div\u003e\n                           \u003c/div\u003e\n                           \u003chr\u003e\n                           \u003cdiv\u003e\n                                %ERROR_MSG%\n                               \u003cform style=\"display:flex; flex-direction: column; width:66%;\" method=\"post\" enctype=\"multipart/form-data\"\u003e\n                                   \u003clabel for=\"name\"\u003eName:\u003c/label\u003e\n                                   \u003cinput id=\"name\" name=\"name\" style=\"width:50%\"\u003e\n                                   \u003ctextarea style=\"min-height:5em; resize:none;\" name=\"msg\" id=\"msg\" maxlength=\"4192\" autocomplete=\"off\"\u003e\u003c/textarea\u003e\n                                   \u003cdiv style=\"display:flex; flex-direction: row; justify-content: flex-end\"\u003e\n                                      \u003cinput value=\"Submit\" data-popup=\"true\" type=\"submit\" style=\"width:33%\"\u003e\u003c/td\u003e\n                                   \u003c/div\u003e\n                               \u003c/form\u003e\n                           \u003c/div\u003e\n                       \u003c/div\u003e)\"s;\n\n        page = std::regex_replace(page, std::regex{\"%MESSAGES%\"}, makeMessagesDiv(state.messages(), context.role));\n        page = std::regex_replace(page, std::regex{\"%LINKS%\"}, makeLinksDiv(context.role));\n        if (request.hasQuery(\"error\")) {\n            if (request.query(\"error\") == \"urls_in_msg\")\n                page = std::regex_replace(page, std::regex{\"%ERROR_MSG%\"}, \"\u003cmark\u003eMessages can't contain urls\u003c/mark\u003e\");\n            if (request.query(\"error\") == \"empty_msg\")\n                page = std::regex_replace(page, std::regex{\"%ERROR_MSG%\"}, \"\u003cmark\u003eMessages can't be empty\u003c/mark\u003e\");\n        }\n        else\n            page = std::regex_replace(page, std::regex{\"%ERROR_MSG%\"}, \"\");\n\n        return page;\n    };\n}\n\nauto addMessage(GuestBookState\u0026 state)\n{\n    return [\u0026state](const asyncgi::Request\u0026 request) -\u003e http::Response\n    {\n        if (std::all_of(\n                    request.formField(\"msg\").begin(),\n                    request.formField(\"msg\").end(),\n                    [](char ch)\n                    {\n                        return std::isspace(static_cast\u003cunsigned char\u003e(ch));\n                    }))\n            return http::Redirect{\"/?error=empty_msg\"};\n        else if (\n                request.formField(\"msg\").find(\"http://\") != std::string_view::npos ||\n                request.formField(\"msg\").find(\"https://\") != std::string_view::npos)\n            return http::Redirect{\"/?error=urls_in_msg\"};\n        else {\n            state.addMessage(std::string{request.formField(\"name\")}, std::string{request.formField(\"msg\")});\n            return http::Redirect{\"/\"};\n        }\n    };\n}\n\nauto removeMessage(GuestBookState\u0026 state)\n{\n    return [\u0026state](int index, const asyncgi::Request\u0026) -\u003e http::Response\n    {\n        state.removeMessage(index);\n        return http::Redirect{\"/\"};\n    };\n}\n\nint main()\n{\n    auto io = asyncgi::IO{4};\n    auto state = GuestBookState{};\n    auto router = asyncgi::Router\u003cRouteContext\u003e{io};\n    router.route(asyncgi::rx{\".*\"}).process(authorizeAdmin);\n    router.route(\"/\", http::RequestMethod::Get).process(showGuestBookPage(state));\n    router.route(\"/\", http::RequestMethod::Post).process(addMessage(state));\n    router.route(asyncgi::rx{\"/delete/(.+)\"}, http::RequestMethod::Post, AccessRole::Admin)\n            .process(removeMessage(state));\n    router.route(asyncgi::rx{\"/delete/(.+)\"}, http::RequestMethod::Post, AccessRole::Guest)\n            .set(http::ResponseStatus::_401_Unauthorized);\n    router.route(\"/login\", http::RequestMethod::Get, AccessRole::Guest).process(showLoginPage);\n    router.route(\"/login\", http::RequestMethod::Post, AccessRole::Guest).process(loginAdmin);\n    router.route(\"/logout\").process(logoutAdmin);\n    router.route().set(http::ResponseStatus::_404_Not_Found, \"Page not found\");\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\nThe live demo can be accessed [here](https://asyncgi-guestbook.eelnet.org).\n\n### Timer\n\nA timer object `asyncgi::Timer` can be created to change or check some state periodically.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_timer.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n\nnamespace http = asyncgi::http;\n\nstruct Greeter{\n    Greeter(const int\u0026 secondsCounter)\n        : secondsCounter_{\u0026secondsCounter}\n    {\n    }\n\n    http::Response operator()(const asyncgi::Request\u0026)\n    {\n        return \"Hello world\\n(alive for \" + std::to_string(*secondsCounter_) + \" seconds)\";\n    }\n\nprivate:\n    const int* secondsCounter_;\n};\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    int secondsCounter = 0;\n\n    auto timer = asyncgi::Timer{io};\n    timer.startPeriodic(\n            std::chrono::seconds(1),\n            [\u0026secondsCounter]()\n            {\n                ++secondsCounter;\n            });\n\n    auto router = asyncgi::Router{io};\n    router.route(\"/\").process\u003cGreeter\u003e(secondsCounter);\n    router.route().set(http::ResponseStatus::_404_Not_Found);\n\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n\nThe `asyncgi::Timer::waitFuture` method can accept an `std::future` object and invoke a provided callable object with\nits result when the future object becomes ready. This function does not block while waiting and uses an internal timer\nto periodically check the state of the future. To use it during request processing, a timer object created from\nan `asyncgi::Responder` reference must be used. It is important to avoid using this timer after the response has already\nbeen sent.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/response_wait_future.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003cthread\u003e\n\nusing namespace asyncgi;\n\nstruct DelayedPage{\n    void operator()(const asyncgi::Request\u0026, asyncgi::Responder\u0026 responder)\n    {\n        auto timer = asyncgi::Timer{responder};\n        timer.waitFuture(\n                std::async(\n                        std::launch::async,\n                        []\n                        {\n                            std::this_thread::sleep_for(std::chrono::seconds(3));\n                            return \"world\";\n                        }),\n                [responder](const std::string\u0026 result) mutable\n                {\n                    responder.send(http::Response{\"Hello \" + result});\n                });\n    }\n};\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto router = asyncgi::Router{io};\n    auto delayedPage = DelayedPage{};\n    router.route(\"/\", http::RequestMethod::Get).process(delayedPage);\n    router.route().set(http::ResponseStatus::_404_Not_Found);\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n### Client\n\nWith `asyncgi::Client` it's possible to make direct requests to `FastCGI` applications. This\nenables multiple `asyncgi`-based applications to communicate with each other without the need for other inter-process\ncommunication solutions.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_client.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003ciostream\u003e\n\nusing namespace asyncgi;\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto client = asyncgi::Client{io};\n    client.makeRequest(\n            \"/tmp/fcgi.sock\",\n            http::Request{http::RequestMethod::Get, \"/\"},\n            [\u0026io](const std::optional\u003chttp::ResponseView\u003e\u0026 response)\n            {\n                if (response)\n                    std::cout \u003c\u003c response-\u003ebody() \u003c\u003c std::endl;\n                else\n                    std::cout \u003c\u003c \"No response\" \u003c\u003c std::endl;\n                io.stop();\n            });\n    io.run();\n}\n```\n\u003c/details\u003e\n\nTo make FastCGI requests during request processing, a client object created from an `asyncgi::Responder` reference must\nbe used. It is important to avoid using this client object after the response has already been sent.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_client_in_processor.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n\nnamespace http = asyncgi::http;\n\nstruct RequestPage{\n    void operator()(const asyncgi::Request\u0026, asyncgi::Responder\u0026 responder)\n    {\n        // making request to FastCgi application listening on /tmp/fcgi.sock and showing the received response\n        auto client = asyncgi::Client{responder};\n        client.makeRequest(\n                \"/tmp/fcgi.sock\",\n                http::Request{http::RequestMethod::Get, \"/\"},\n                [responder](const std::optional\u003chttp::ResponseView\u003e\u0026 reqResponse) mutable\n                {\n                    if (reqResponse)\n                        responder.send(std::string{reqResponse-\u003ebody()});\n                    else\n                        responder.send(\"No response\");\n                });\n    }\n};\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto router = asyncgi::Router{io};\n    router.route(\"/\", http::RequestMethod::Get).process\u003cRequestPage\u003e();\n    router.route().set(http::ResponseStatus::_404_Not_Found);\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi_client.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\n\n### Executing an asio task\n\n`asyncgi` internally uses the `asio` library. A dispatcher object `asyncgi::AsioDispatcher` can be created to invoke\ncallable objects that require access to the `asio::io_context` object.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_asio_dispatcher.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003casio/steady_timer.hpp\u003e\n#include \u003ciostream\u003e\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto disp = asyncgi::AsioDispatcher{io};\n    disp.postTask(\n            [\u0026io](const asyncgi::TaskContext\u0026 ctx) mutable\n            {\n                auto timer = std::make_shared\u003casio::steady_timer\u003e(ctx.io());\n                timer-\u003eexpires_after(std::chrono::seconds{3});\n                timer-\u003easync_wait(\n                        [timer, ctx, \u0026io](auto\u0026) mutable\n                        {\n                            std::cout \u003c\u003c \"Hello world\" \u003c\u003c std::endl;\n                            io.stop();\n                        });\n            });\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\nTo invoke such a callable object during request processing, a dispatcher created from an `asyncgi::Responder` reference\nmust be used. It is important to avoid using this dispatcher after the response has already been sent.\n\n\u003cdetails\u003e\n  \u003csummary\u003eExample\u003c/summary\u003e\n\n```c++\n///examples/example_response_dispatching_asio_task.cpp\n///\n#include \u003casyncgi/asyncgi.h\u003e\n#include \u003casio/steady_timer.hpp\u003e\n\nnamespace http = asyncgi::http;\n\nstruct DelayedPage {\n    void operator()(const asyncgi::Request\u0026, asyncgi::Responder\u0026 responder)\n    {\n        auto disp = asyncgi::AsioDispatcher{responder};\n        disp.postTask(\n                [responder](const asyncgi::TaskContext\u0026 ctx) mutable\n                {\n                    auto timer = std::make_shared\u003casio::steady_timer\u003e(ctx.io());\n                    timer-\u003eexpires_after(std::chrono::seconds{3});\n                    timer-\u003easync_wait([timer, responder, ctx](auto\u0026) mutable { // Note how we capture ctx object here,\n                        responder.send(\"Hello world\"); // it's necessary to keep it (or its copy) alive\n                    }); // before the end of request processing\n                });\n    }\n};\n\nint main()\n{\n    auto io = asyncgi::IO{};\n    auto router = asyncgi::Router{io};\n    router.route(\"/\", http::RequestMethod::Get).process\u003cDelayedPage\u003e();\n    router.route().set(http::ResponseStatus::_404_Not_Found);\n    auto server = asyncgi::Server{io, router};\n    server.listen(\"/tmp/fcgi.sock\");\n    io.run();\n}\n```\n\n\u003c/details\u003e\n\nTo use `asyncgi` with the `Boost.Asio` library, set the `ASYNCGI_USE_BOOST_ASIO` CMake variable .\n\n## Showcase\n\n* [`stone_skipper`](https://github.com/kamchatka-volcano/stone_skipper)\n\n## Development status\n\n`asyncgi` is currently in the open beta stage, with all planned features complete. Until it reaches a non-zero major\nversion number, there may be frequent introductions of backward-incompatible changes.\n\nUnit tests are not included because most of the functionality in `asyncgi` is derived from the following libraries,\nwhich already have their own test coverage:\n\n* [asio](https://github.com/chriskohlhoff/asio) - used for establishing connections, sending and receiving data.\n* [fcgi_responder](https://github.com/kamchatka-volcano/fcgi_responder/) - implementation of the `FastCGI` protocol.\n* [whaleroute](https://github.com/kamchatka-volcano/whaleroute/) - implementation of the request router.\n* [hot_teacup](https://github.com/kamchatka-volcano/hot_teacup/) - parsing of HTTP data received over `FastCGI`\n  connections, forming HTTP responses.\n\nInstead of mocking code that integrates the functionality of these libraries, `asyncgi` is tested using functional\ntests. These tests check the behavior of the executables found in the `examples/` directory when running with the NGINX\nserver. You can find these tests in the `functional_tests/` directory.\n\n## Installation\n\nDownload and link the library from your project's CMakeLists.txt:\n```\ncmake_minimum_required(VERSION 3.14)\n\ninclude(FetchContent)\n\nFetchContent_Declare(cmdlime\n    GIT_REPOSITORY \"https://github.com/kamchatka-volcano/asyncgi.git\"\n    GIT_TAG \"origin/master\"\n)\n#uncomment if you need to install cmdlime with your target\n#set(INSTALL_ASYNCGI ON)\nFetchContent_MakeAvailable(asyncgi)\n\nadd_executable(${PROJECT_NAME})\ntarget_link_libraries(${PROJECT_NAME} PRIVATE asyncgi::asyncgi)\n```\n\nTo install the library system-wide, use the following commands:\n```\ngit clone https://github.com/kamchatka-volcano/asyncgi.git\ncd asyncgi\ncmake -S . -B build\ncmake --build build\ncmake --install build\n```\n\nAfter installation, you can use the find_package() command to make the installed library available inside your project:\n\n```\nfind_package(asyncgi 0.1.0 REQUIRED)\ntarget_link_libraries(${PROJECT_NAME} PRIVATE asyncgi::asyncgi)   \n```\n\nIf you want to use the `Boost.Asio` library, `Boost` dependencies can be resolved\nusing [vcpkg](https://vcpkg.io/en/getting-started.html) by running the build with this command:\n\n```\ncmake -S . -B build -DASYNCGI_USE_BOOST_ASIO=ON -DCMAKE_TOOLCHAIN_FILE=\u003cvcpkg path\u003e/scripts/buildsystems/vcpkg.cmake\n```\n\n## Building examples\n\n```\ncd asyncgi\ncmake -S . -B build -DENABLE_EXAMPLES=ON\ncmake --build build\ncd build/examples\n```\n\n## Running functional tests\n\nDownload [`lunchtoast`](https://github.com/kamchatka-volcano/lunchtoast/releases) executable, build `asyncgi` examples\nand start NGINX with `functional_tests/nginx_*.conf` config file.\nLaunch tests with the following command:\n\n* Linux command:\n\n```\nlunchtoast functional_tests\n```\n\n* Windows command:\n\n```\nlunchtoast.exe functional_tests -shell=\"msys2 -c\" -skip=linux\n```\n\nTo run functional tests on Windows, it's recommended to use the bash shell from the `msys2` project. After installing\nit, add the following script `msys2.cmd` to your system `PATH`:\n\n```bat\n@echo off\nsetlocal\nIF NOT DEFINED MSYS2_PATH_TYPE set MSYS2_PATH_TYPE=inherit\nset CHERE_INVOKING=1\nC:\\\\msys64\\\\usr\\\\bin\\\\bash.exe -leo pipefail %*\n```\n\n## License\n\n**asyncgi** is licensed under the [MS-PL license](/LICENSE.md)  \n\n\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkamchatka-volcano%2Fasyncgi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkamchatka-volcano%2Fasyncgi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkamchatka-volcano%2Fasyncgi/lists"}