From 3c17cb206fd257e03ce12f98f45998d10fc69097 Mon Sep 17 00:00:00 2001 From: Andrew Guz Date: Thu, 9 Jan 2025 10:39:47 +0300 Subject: [PATCH 01/12] Possibility to mirror Sec-WebSocket-Protocols --- include/crow/routing.h | 11 +++++++++-- include/crow/websocket.h | 8 +++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/include/crow/routing.h b/include/crow/routing.h index 26690a50a..de79a4df5 100644 --- a/include/crow/routing.h +++ b/include/crow/routing.h @@ -440,12 +440,12 @@ namespace crow // NOTE: Already documented in "crow/app.h" void handle_upgrade(const request& req, response&, SocketAdaptor&& adaptor) override { max_payload_ = max_payload_override_ ? max_payload_ : app_->websocket_max_payload(); - new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); + new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_, mirror_protocols_); } #ifdef CROW_ENABLE_SSL void handle_upgrade(const request& req, response&, SSLAdaptor&& adaptor) override { - new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_); + new crow::websocket::Connection(req, std::move(adaptor), app_, max_payload_, subprotocols_, open_handler_, message_handler_, close_handler_, error_handler_, accept_handler_, mirror_protocols_); } #endif @@ -498,6 +498,12 @@ namespace crow // NOTE: Already documented in "crow/app.h" return *this; } + self_t& mirrorprotocols() + { + mirror_protocols_ = true; + return *this; + } + protected: App* app_; std::function open_handler_; @@ -505,6 +511,7 @@ namespace crow // NOTE: Already documented in "crow/app.h" std::function close_handler_; std::function error_handler_; std::function accept_handler_; + bool mirror_protocols_ = false; uint64_t max_payload_; bool max_payload_override_ = false; std::vector subprotocols_; diff --git a/include/crow/websocket.h b/include/crow/websocket.h index fa5e18546..a315226f6 100644 --- a/include/crow/websocket.h +++ b/include/crow/websocket.h @@ -116,7 +116,8 @@ namespace crow // NOTE: Already documented in "crow/app.h" std::function message_handler, std::function close_handler, std::function error_handler, - std::function accept_handler): + std::function accept_handler, + bool mirror_protocols): adaptor_(std::move(adaptor)), handler_(handler), max_payload_bytes_(max_payload), @@ -145,6 +146,11 @@ namespace crow // NOTE: Already documented in "crow/app.h" } } + if (mirror_protocols & !requested_subprotocols_header.empty()) + { + subprotocol_ = requested_subprotocols_header; + } + if (accept_handler_) { void* ud = nullptr; From 1760ed4380f684dd9237ff3f76153e63f99bef57 Mon Sep 17 00:00:00 2001 From: Andrew Guz Date: Thu, 9 Jan 2025 11:04:10 +0300 Subject: [PATCH 02/12] Unit test for possibility to mirror Sec-WebSocket-Protocols --- tests/unittest.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/unittest.cpp b/tests/unittest.cpp index 2d3945dd2..2ed0e0820 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -3200,6 +3200,57 @@ TEST_CASE("websocket_subprotocols") app.stop(); } +TEST_CASE("mirror_websocket_subprotocols") +{ + static std::string http_message = "GET /ws HTTP/1.1\r\nConnection: keep-alive, Upgrade\r\nupgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Protocol: protocol1, protocol2\r\nSec-WebSocket-Version: 13\r\nHost: localhost\r\n\r\n"; + + static websocket::connection* connection = nullptr; + static bool connected{false}; + + SimpleApp app; + + CROW_WEBSOCKET_ROUTE(app, "/ws") + .mirrorprotocols() + .onaccept([&](const crow::request& req, void**) { + CROW_LOG_INFO << "Accepted websocket with URL " << req.url; + return true; + }) + .onopen([&](websocket::connection& con) { + connected = true; + connection = &con; + CROW_LOG_INFO << "Connected websocket and subprotocol is " << con.get_subprotocol(); + }) + .onclose([&](websocket::connection&, const std::string&, uint16_t) { + CROW_LOG_INFO << "Closing websocket"; + }); + + app.validate(); + + auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async(); + app.wait_for_server_start(); + asio::io_context ic; + + asio::ip::tcp::socket c(ic); + c.connect(asio::ip::tcp::endpoint( + asio::ip::make_address(LOCALHOST_ADDRESS), 45451)); + + + char buf[2048]; + + //----------Handshake---------- + { + std::fill_n(buf, 2048, 0); + c.send(asio::buffer(http_message)); + + c.receive(asio::buffer(buf, 2048)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CHECK(connected); + CHECK(connection->get_subprotocol() == "protocol1, protocol2"); + } + + app.stop(); +} + #ifdef CROW_ENABLE_COMPRESSION TEST_CASE("zlib_compression") { From 3da9c2c75199ad5401ab8680086a8224b3ce0a6e Mon Sep 17 00:00:00 2001 From: Andrew Guz Date: Thu, 9 Jan 2025 12:55:51 +0300 Subject: [PATCH 03/12] Review points --- tests/unittest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unittest.cpp b/tests/unittest.cpp index 2ed0e0820..ece43cd7f 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -3204,8 +3204,8 @@ TEST_CASE("mirror_websocket_subprotocols") { static std::string http_message = "GET /ws HTTP/1.1\r\nConnection: keep-alive, Upgrade\r\nupgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Protocol: protocol1, protocol2\r\nSec-WebSocket-Version: 13\r\nHost: localhost\r\n\r\n"; - static websocket::connection* connection = nullptr; - static bool connected{false}; + websocket::connection* connection = nullptr; + bool connected{false}; SimpleApp app; From b23953cb74067fd392c9e9aec49a9e40487a636e Mon Sep 17 00:00:00 2001 From: Andrew Guz Date: Fri, 10 Jan 2025 12:11:30 +0300 Subject: [PATCH 04/12] Review points --- include/crow/routing.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/crow/routing.h b/include/crow/routing.h index de79a4df5..1ee7b0f57 100644 --- a/include/crow/routing.h +++ b/include/crow/routing.h @@ -498,9 +498,9 @@ namespace crow // NOTE: Already documented in "crow/app.h" return *this; } - self_t& mirrorprotocols() + self_t& mirrorprotocols(bool mirror_protocols = true) { - mirror_protocols_ = true; + mirror_protocols_ = mirror_protocols; return *this; } From 481a363cfa8a0acfd81ed937bc8eae1d25489547 Mon Sep 17 00:00:00 2001 From: gulliver Date: Mon, 14 Oct 2024 15:35:02 +0200 Subject: [PATCH 05/12] added wait timeout to server start --- include/crow/app.h | 27 ++++++++++++++++++++------- include/crow/http_server.h | 11 +++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/include/crow/app.h b/include/crow/app.h index fbf53a075..4e577ece6 100644 --- a/include/crow/app.h +++ b/include/crow/app.h @@ -695,19 +695,32 @@ namespace crow } /// \brief Wait until the server has properly started - void wait_for_server_start() + std::cv_status wait_for_server_start(std::chrono::milliseconds wait_timeout = std::chrono::milliseconds(3000)) { + std::cv_status status = std::cv_status::no_timeout; + auto wait_until = std::chrono::steady_clock::now() + wait_timeout; { std::unique_lock lock(start_mutex_); - while (!server_started_) - cv_started_.wait(lock); + while (!server_started_ && (status == std::cv_status::no_timeout)) + { + status = cv_started_.wait_until(lock, wait_until); + } } - if (server_) - server_->wait_for_start(); + + if (status == std::cv_status::no_timeout) + { + if (server_) + { + status = server_->wait_for_start(wait_until); + } #ifdef CROW_ENABLE_SSL - else if (ssl_server_) - ssl_server_->wait_for_start(); + else if (ssl_server_) + { + status = ssl_server_->wait_for_start(wait_until); + } #endif + } + return status; } private: diff --git a/include/crow/http_server.h b/include/crow/http_server.h index ed9ae0fc1..e74c5a3fd 100644 --- a/include/crow/http_server.h +++ b/include/crow/http_server.h @@ -196,12 +196,15 @@ namespace crow // NOTE: Already documented in "crow/app.h" return acceptor_.local_endpoint().port(); } - /// Wait until the server has properly started - void wait_for_start() + /// Wait until the server has properly started or until timeout + std::cv_status wait_for_start(std::chrono::steady_clock::time_point wait_until) { std::unique_lock lock(start_mutex_); - while (!server_started_) - cv_started_.wait(lock); + + std::cv_status status = std::cv_status::no_timeout; + while (!server_started_ && ( status==std::cv_status::no_timeout )) + status = cv_started_.wait_until(lock,wait_until); + return status; } void signal_clear() From 44bc05591153ae4349b409817373e887f7a21047 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Mon, 25 Nov 2024 21:28:19 +0100 Subject: [PATCH 06/12] tcp endpoint created before server creation, invalid address error is logged now, server runs into timeout in that case added testcase for invalid address --- include/crow/app.h | 13 ++++++++++--- include/crow/http_server.h | 23 +++++++++++++---------- tests/unittest.cpp | 23 ++++++++++++++++++++--- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/include/crow/app.h b/include/crow/app.h index 4e577ece6..4119859b7 100644 --- a/include/crow/app.h +++ b/include/crow/app.h @@ -365,7 +365,7 @@ namespace crow } /// \brief Get the number of threads that server is using - std::uint16_t concurrency() + std::uint16_t concurrency() const { return concurrency_; } @@ -506,11 +506,18 @@ namespace crow #endif validate(); + error_code ec; + asio::ip::address addr = asio::ip::make_address(bindaddr_,ec); + if (ec){ + CROW_LOG_ERROR << ec.message() << " - Can not create valid ip address from string: \"" << bindaddr_ << "\""; + return; + } + tcp::endpoint endpoint(addr, port_); #ifdef CROW_ENABLE_SSL if (ssl_used_) { router_.using_ssl = true; - ssl_server_ = std::move(std::unique_ptr(new ssl_server_t(this, bindaddr_, port_, server_name_, &middlewares_, concurrency_, timeout_, &ssl_context_))); + ssl_server_ = std::move(std::unique_ptr(new ssl_server_t(this, endpoint, server_name_, &middlewares_, concurrency_, timeout_, &ssl_context_))); ssl_server_->set_tick_function(tick_interval_, tick_function_); ssl_server_->signal_clear(); for (auto snum : signals_) @@ -523,7 +530,7 @@ namespace crow else #endif { - server_ = std::move(std::unique_ptr(new server_t(this, bindaddr_, port_, server_name_, &middlewares_, concurrency_, timeout_, nullptr))); + server_ = std::move(std::unique_ptr(new server_t(this, endpoint, server_name_, &middlewares_, concurrency_, timeout_, nullptr))); server_->set_tick_function(tick_interval_, tick_function_); for (auto snum : signals_) { diff --git a/include/crow/http_server.h b/include/crow/http_server.h index e74c5a3fd..12ff3726d 100644 --- a/include/crow/http_server.h +++ b/include/crow/http_server.h @@ -42,16 +42,20 @@ namespace crow // NOTE: Already documented in "crow/app.h" class Server { public: - Server(Handler* handler, std::string bindaddr, uint16_t port, std::string server_name = std::string("Crow/") + VERSION, std::tuple* middlewares = nullptr, uint16_t concurrency = 1, uint8_t timeout = 5, typename Adaptor::context* adaptor_ctx = nullptr): - acceptor_(io_context_, tcp::endpoint(asio::ip::make_address(bindaddr), port)), + Server(Handler* handler, + const tcp::endpoint& endpoint, + std::string server_name = std::string("Crow/") + VERSION, + std::tuple* middlewares = nullptr, + uint16_t concurrency = 1, + uint8_t timeout = 5, + typename Adaptor::context* adaptor_ctx = nullptr): + acceptor_(io_context_,endpoint), signals_(io_context_), tick_timer_(io_context_), handler_(handler), concurrency_(concurrency), timeout_(timeout), server_name_(server_name), - port_(port), - bindaddr_(bindaddr), task_queue_length_pool_(concurrency_ - 1), middlewares_(middlewares), adaptor_ctx_(adaptor_ctx) @@ -150,11 +154,12 @@ namespace crow // NOTE: Already documented in "crow/app.h" }); } - port_ = acceptor_.local_endpoint().port(); - handler_->port(port_); + handler_->port(acceptor_.local_endpoint().port()); - CROW_LOG_INFO << server_name_ << " server is running at " << (handler_->ssl_used() ? "https://" : "http://") << bindaddr_ << ":" << acceptor_.local_endpoint().port() << " using " << concurrency_ << " threads"; + CROW_LOG_INFO << server_name_ + << " server is running at " << (handler_->ssl_used() ? "https://" : "http://") + << acceptor_.local_endpoint().address() << ":" << acceptor_.local_endpoint().port() << " using " << concurrency_ << " threads"; CROW_LOG_INFO << "Call `app.loglevel(crow::LogLevel::Warning)` to hide Info level logs."; signals_.async_wait( @@ -192,7 +197,7 @@ namespace crow // NOTE: Already documented in "crow/app.h" io_context_.stop(); // Close main io_service } - uint16_t port(){ + uint16_t port() const { return acceptor_.local_endpoint().port(); } @@ -293,8 +298,6 @@ namespace crow // NOTE: Already documented in "crow/app.h" uint16_t concurrency_{2}; std::uint8_t timeout_; std::string server_name_; - uint16_t port_; - std::string bindaddr_; std::vector> task_queue_length_pool_; std::chrono::milliseconds tick_interval_; diff --git a/tests/unittest.cpp b/tests/unittest.cpp index d2ca615a4..ff2114620 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -643,7 +643,22 @@ TEST_CASE("server_handling_error_request") app.stop(); } // server_handling_error_request -TEST_CASE("server_dynamic_port_allication") +TEST_CASE("server_invalid_ip_address") +{ + SimpleApp app; + CROW_ROUTE(app, "/") + ([] { + return "A"; + }); + auto _ = app.bindaddr("192.").port(45451).run_async(); + auto state = app.wait_for_server_start(); + + // we should run into a timeout as the server will not started + CHECK(state==cv_status::timeout); +} // server_invalid_ip_address + + +TEST_CASE("server_dynamic_port_allocation") { SimpleApp app; CROW_ROUTE(app, "/") @@ -659,7 +674,7 @@ TEST_CASE("server_dynamic_port_allication") asio::ip::make_address(LOCALHOST_ADDRESS), app.port())); } app.stop(); -} // server_dynamic_port_allication +} // server_dynamic_port_allocation TEST_CASE("server_handling_error_request_http_version") { @@ -1520,7 +1535,9 @@ struct NullSimpleMiddleware TEST_CASE("middleware_simple") { App app; - decltype(app)::server_t server(&app, LOCALHOST_ADDRESS, 45451); + asio::ip::address adr = asio::ip::make_address(LOCALHOST_ADDRESS); + tcp::endpoint ep(adr,45451); + decltype(app)::server_t server(&app, ep); CROW_ROUTE(app, "/") ([&](const crow::request& req) { app.get_context(req); From 100227255411adf615efbaba9bc2b0bba17a180e Mon Sep 17 00:00:00 2001 From: gittiver Date: Sun, 29 Dec 2024 22:33:58 +0100 Subject: [PATCH 07/12] Update build_and_test.yml --- .github/workflows/build_and_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index c2344bfe5..d66833d40 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -23,7 +23,7 @@ jobs: windows-latest, macos-latest, ubuntu-20.04, - ubuntu-24.04, + ubuntu-22.04, macos-13 ] # ubuntu-18.04 does not work due to compile error on asio From feb71effe0028700819eb4b985cc4d373bde70a1 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaytsev Date: Thu, 9 Jan 2025 18:37:14 +0300 Subject: [PATCH 08/12] Writing to socket synchronously --- include/crow/http_connection.h | 4 +-- tests/unittest.cpp | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/include/crow/http_connection.h b/include/crow/http_connection.h index 04909453e..76c82f014 100644 --- a/include/crow/http_connection.h +++ b/include/crow/http_connection.h @@ -128,7 +128,7 @@ namespace crow buffers_.clear(); static std::string expect_100_continue = "HTTP/1.1 100 Continue\r\n\r\n"; buffers_.emplace_back(expect_100_continue.data(), expect_100_continue.size()); - do_write(); + do_write_sync(buffers_); } } @@ -427,7 +427,7 @@ namespace crow res_body_copy_.swap(res.body); buffers_.emplace_back(res_body_copy_.data(), res_body_copy_.size()); - do_write(); + do_write_sync(buffers_); if (need_to_start_read_after_complete_) { diff --git a/tests/unittest.cpp b/tests/unittest.cpp index ff2114620..e774601ab 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -2,6 +2,7 @@ #define CROW_LOG_LEVEL 0 #include +#include #include #include #include @@ -4021,3 +4022,65 @@ TEST_CASE("http2_upgrade_is_ignored") CHECK(res.find("http2 upgrade is not supported so body is parsed") != std::string::npos); app.stop(); } + +TEST_CASE("option_header_passed_in_full") +{ + const std::string ServerName = "AN_EXTREMELY_UNIQUE_SERVER_NAME"; + + crow::App + app; + + app.get_middleware() // + .global() + .allow_credentials() + .expose("X-Total-Pages", "X-Total-Entries", "Content-Disposition"); + + app.server_name(ServerName); + + + CROW_ROUTE(app, "/echo").methods(crow::HTTPMethod::Options)([]() { + crow::response response{}; + response.add_header("Key", "Value"); + return response; + }); + + auto _ = app.bindaddr(LOCALHOST_ADDRESS).port(45451).run_async(); + + app.wait_for_server_start(); + + asio::io_service is; + + auto make_request = [&](const std::string& rq) { + asio::ip::tcp::socket c(is); + c.connect(asio::ip::tcp::endpoint( + asio::ip::address::from_string(LOCALHOST_ADDRESS), 45451)); + c.send(asio::buffer(rq)); + std::string fullString{}; + asio::error_code error; + char buffer[1024]; + while (true) + { + size_t len = c.read_some(asio::buffer(buffer), error); + + if (error == asio::error::eof) + { + break; + } + else if (error) + { + throw system_error(error); + } + + fullString.append(buffer, len); + } + c.close(); + return fullString; + }; + + std::string request = + "OPTIONS /echo HTTP/1.1\r\n"; + + auto res = make_request(request); + CHECK(res.find(ServerName) != std::string::npos); + app.stop(); +} From 27636c1523307c3689a1dbbe8bb2c4cc97d14bf6 Mon Sep 17 00:00:00 2001 From: Gulliver Date: Mon, 6 Jan 2025 20:11:27 +0100 Subject: [PATCH 09/12] replaced not reliable sleep by a asio::waitable_timer (steady_timer) which is more precise and reliable. --- tests/unittest.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/unittest.cpp b/tests/unittest.cpp index e774601ab..db72d6215 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -3930,13 +3930,26 @@ TEST_CASE("task_timer") b = true; }); - this_thread::sleep_for(3 * timer.get_tick_length()); + asio::steady_timer t(io_service); + asio::error_code ec; + + t.expires_from_now(3 * timer.get_tick_length()); + t.wait(ec); + // we are at 3 ticks, nothing be changed yet + CHECK(!ec); CHECK(a == false); CHECK(b == false); - this_thread::sleep_for(3 * timer.get_tick_length()); + t.expires_from_now(3 * timer.get_tick_length()); + t.wait(ec); + // we are at 3+3 = 6 ticks, so first task_timer handler should have runned + CHECK(!ec); CHECK(a == true); CHECK(b == false); - this_thread::sleep_for(5 * timer.get_tick_length()); + + t.expires_from_now(5 * timer.get_tick_length()); + t.wait(ec); + //we are at 3+3 +5 = 11 ticks, both task_timer handlers shoudl have run now + CHECK(!ec); CHECK(a == true); CHECK(b == true); From 523ae689a68c017c7deab3357d6ac877cc039eb0 Mon Sep 17 00:00:00 2001 From: gittiver Date: Mon, 6 Jan 2025 20:23:16 +0100 Subject: [PATCH 10/12] Update tests/unittest.cpp Co-authored-by: Ilya Andreev --- tests/unittest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unittest.cpp b/tests/unittest.cpp index db72d6215..454614292 100644 --- a/tests/unittest.cpp +++ b/tests/unittest.cpp @@ -3930,8 +3930,8 @@ TEST_CASE("task_timer") b = true; }); - asio::steady_timer t(io_service); - asio::error_code ec; + asio::steady_timer t(io_context); + asio_error_code ec; t.expires_from_now(3 * timer.get_tick_length()); t.wait(ec); From c0da4dbafaf50f39ecc9c47d95324d5ece560141 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 13:43:02 +0000 Subject: [PATCH 11/12] Bump dawidd6/action-download-artifact from 7 to 8 Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 7 to 8. - [Release notes](https://github.com/dawidd6/action-download-artifact/releases) - [Commits](https://github.com/dawidd6/action-download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: dawidd6/action-download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/submit_coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/submit_coverage.yml b/.github/workflows/submit_coverage.yml index c768b680a..04758a77b 100644 --- a/.github/workflows/submit_coverage.yml +++ b/.github/workflows/submit_coverage.yml @@ -16,7 +16,7 @@ jobs: if: github.event.workflow_run.conclusion == 'success' steps: - name: Download artifact - uses: dawidd6/action-download-artifact@v7 + uses: dawidd6/action-download-artifact@v8 with: workflow: ${{ github.event.workflow_run.workflow_id }} workflow_conclusion: success From 96ed14aea63896dd0168232fe4aedba5148088f4 Mon Sep 17 00:00:00 2001 From: fliiiix Date: Fri, 24 Jan 2025 09:55:35 +0100 Subject: [PATCH 12/12] Update testing documentation to use correct function --- docs/guides/testing.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/guides/testing.md b/docs/guides/testing.md index 377432221..f7c49ba51 100644 --- a/docs/guides/testing.md +++ b/docs/guides/testing.md @@ -15,7 +15,10 @@ Crow allows users to handle requests that may not come from the network. This is req.url = "/place"; - app.handle(req, res); //res will contain a code of 200, and a response body of "hi" + app.handle_full(req, res); + // res will contain: + // res.code == 200 + // res.body == "hi" } ```