Rakesh Vidyadharan Help

HTTP Router Use

The HttpRouter<Request, Response, Map> class exposes two primary methods - add and route - that are used to set up and perform routing:

  • CTOR - Create an instance with the optional handlers to handle standard scenarios such as Not Found (404), Method Not Allowed (405), and Internal Server Error (500).

    • Use the Builder to specify the desired error handlers and initialise the router in a more convenient manner.

  • add - Use to add paths or parametrised paths to the router.

    • This is thread safe. Configuring routing should generally not need thread safety, but just in case route additions are set up in parallel in a multi-threaded environment, a std::mutex is used to ensure thread safety.

      • The general expectation (standard pattern when configuring routes) is that all the routes are configured before the server starts routing requests.

      • Performing routing while additional routes are being added to the router could lead to undefined behaviour.

    • Duplicate routes will throw a spt::http::router::DuplicateRouteError exception.

    • Routes with invalid parameter will throw a spt::http::router::InvalidParameterError exception.

      • This is thrown if a parameter uses the :<parameter> form.

      • This is thrown if a parameter does not end with the } character.

  • route - When a client request is received, delegate to the router to handle the request.

    • If a notFound handler was specified when creating the router (first optional constructor parameter), and the input request path was not found, the handler will be invoked.

    • If a methodNotFound handler was specified when creating the router (second optional constructor parameter), and the input request method was not configured for the specified path, the handler will be invoked.

    • If a errorHandler handler was specified when creating the router (third optional constructor parameter), and an exception was thrown by the configured handler function for the method:path, the handler will be invoked.

  • If Boost has been found a few additional utility methods are exposed.

    • json - Output the configured routes and some additional metadata as a JSON structure. See the sample output below from the device test.

    • str - Output the configured routes and some additional metadata as a string. This is just the JSON representation serialised.

    • operator<< - Appends the string representation to the output stream.

  • yaml - Output the configured routes in YAML format which can be embedded or cross-verified against the API OpenAPI Specifications file. If using this feature, please try to specify the optional ref parameter to the add method. For example see the output below from the device test.

Sample Output

JSON

{ "paths": [ { "path": "/device/sensor/", "methods": ["POST", "GET"] }, { "path": "/device/sensor/count/references/{id}", "methods": ["GET"] }, { "path": "/device/sensor/customer/code/{code}", "methods": ["GET"] }, { "path": "/device/sensor/facility/id/{id}", "methods": ["GET"] }, { "path": "/device/sensor/history/document/{id}", "methods": ["GET"] }, { "path": "/device/sensor/history/summary/{id}", "methods": ["GET"] }, { "path": "/device/sensor/id/{id}", "methods": ["PUT", "GET", "DELETE"] }, { "path": "/device/sensor/identifier/{identifier}", "methods": ["GET"] }, { "path": "/device/sensor/{property}/between/{start}/{end}", "methods": ["GET"] } ], "total": 9, "static": 1, "dynamic": 8 }

YAML

paths: /device/sensor/: $ref: "./paths/sensor.yaml#/root" /device/sensor/count/references/{id}: $ref: "./paths/sensor.yaml#/refcount" /device/sensor/customer/code/{code}: $ref: "./paths/sensor.yaml#/customer" /device/sensor/facility/id/{id}: $ref: "./paths/sensor.yaml#/facility" /device/sensor/history/document/{id}: $ref: "./paths/sensor.yaml#/history/document" /device/sensor/history/summary/{id}: $ref: "./paths/sensor.yaml#/history/summary" /device/sensor/id/{id}: $ref: "./paths/sensor.yaml#/id" /device/sensor/identifier/{identifier}: $ref: "./paths/sensor.yaml#/identifier" /device/sensor/{property}/between/{start}/{end}: $ref: "./paths/sensor.yaml#/between"

Example

The following shows sample use of the router. See basic test and other unit tests for more samples.

#include <router/router.h> using std::operator ""s; using std::operator ""sv; int main() { struct Request { // pass whatever you need as user data } request; const auto method = "GET"sv; spt::http::router::HttpRouter<const Request&, bool> r; r.add( "POST"sv, "/device/sensor/"sv, []( const Request&, spt::http::router::HttpRouter<const Request&, bool>::MapType args ) { assert( args.empty() ); return true; } ); r.add( method, "/device/sensor/"sv, []( const Request&, auto args ) { assert( args.empty() ); return true; } ); r.add( "PUT"sv, "/device/sensor/id/{id}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "id"sv ) ); return true; } ); r.add( method, "/device/sensor/id/{id}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "id"sv ) ); return true; } ); r.add( method, "/device/sensor/identifier/{identifier}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "identifier"sv ) ); return true; } ); r.add( method, "/device/sensor/customer/code/{code}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "code"sv ) ); return true; } ); r.add( method, "/device/sensor/facility/id/{id}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "id"sv ) ); return true; } ); r.add( method, "/device/sensor/count/references/{id}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "id"sv ) ); return true; } ); r.add( method, "/device/sensor/history/summary/{id}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "id"sv ) ); return true; } ); r.add( method, "/device/sensor/history/document/{id}"sv, []( const Request&, auto args ) { assert( args.size() == 1 ); assert( args.contains( "id"sv ) ); return true; } ); r.add( method, "/device/sensor/{property}/between/{start}/{end}"sv, []( const Request&, auto args ) { assert( args.size() == 3 ); assert( args.contains( "property"sv ) ); assert( args.contains( "start"sv ) ); assert( args.contains( "end"sv ) ); return true; } ); std::vector<std::string> urls = { "/device/sensor/"s, "/device/sensor/id/6230f3069e7c9be9ff4b78a1"s, // id=6230f3069e7c9be9ff4b78a1 "/device/sensor/identifier/Integration Test Identifier"s, // identifier=Integration Test Identifier "/device/sensor/customer/code/int-test"s, // code=int-test "/device/sensor/history/summary/6230f3069e7c9be9ff4b78a1"s, // id=6230f3069e7c9be9ff4b78a1 "/device/sensor/history/document/6230f3069e7c9be9ff4b78a1"s, // id=6230f3069e7c9be9ff4b78a1 "/device/sensor/count/references/6230f3069e7c9be9ff4b78a1"s, // id=6230f3069e7c9be9ff4b78a1 "/device/sensor/created/between/2022-03-14T20:11:50.620Z/2022-03-16T20:11:50.620Z"s, // property=created, start=2022-03-14T20:11:50.620Z, end=2022-03-16T20:11:50.620Z }; for ( auto&& url : urls ) { auto resp = r.route( "GET"sv, url, request ); assert( resp ); assert( *resp ); } auto resp = r.route( "PUT"sv, "/device/sensor/"sv ); assert( resp ); assert( !*resp ); // PUT not configured resp = r.route( "POST"sv, "/device/sensor/history/document/{id}"sv ); assert( resp ); assert( !*resp ); // POST not configured try { r.add( "PUT"sv, "/device/sensor/id/{id}"sv, []( const Request&, auto args ) { return true; } ); } catch ( const spt::http::router::DuplicateRouteError& e ) { // Will be caught as we registered the same route earlier std::cerr << e.what() << '\n'; } }

Use with nghttp2

The route method returns a std::optional<Response>. If no configured path matches, returns std::nullopt (or the response from the not found handler if specified at construction time). Otherwise, returns the response from the callback function.

#include <nghttp2/asio_http2_server.h> #include <log/NanoLog.h> #include <router/router.h> int main() { struct Request { explicit Request( const nghttp2::asio_http2::server::request& req ) : header{ req.header() }, method{ req.method() }, path{ req.uri().path }, query{ req.uri().raw_query } {} nghttp2::asio_http2::header_map header; std::string method; std::string path; std::string query; std::shared_ptr<std::string> body{ nullptr }; }; struct Response { nghttp2::asio_http2::header_map headers; std::string body{ "{}" }; uint16_t status{ 200 }; bool compressed{ false }; }; auto const error404 = []( const Request&, spt::http::router::HttpRouter<const Request&, Response>::MapType ) -> Response { auto json = R"({"code": 404, "cause": "Not Found"})"s; auto headers = nghttp2::asio_http2::header_map{ { "Access-Control-Allow-Origin", { "*", false} }, { "Access-Control-Allow-Methods", { "DELETE,GET,OPTIONS,POST,PUT", false } }, { "Access-Control-Allow-Headers", { "*, authorization", false } }, { "content-type", { "application/json; charset=utf-8", false } }, { "content-length", { std::to_string( json.size() ), false } } }; return { std::move( headers ), std::move( json ), 404, false } } auto const error405 - []( const Request&, spt::http::router::HttpRouter<const Request&, Response>::MapType ) -> Response { auto json = R"({"code": 405, "cause": "Method Not Allowed"})"s; auto headers = nghttp2::asio_http2::header_map{ { "Access-Control-Allow-Origin", { "*", false} }, { "Access-Control-Allow-Methods", { "DELETE,GET,OPTIONS,POST,PUT", false } }, { "Access-Control-Allow-Headers", { "*, authorization", false } }, { "content-type", { "application/json; charset=utf-8", false } }, { "content-length", { std::to_string( json.size() ), false } } }; return { std::move( headers ), std::move( json ), 405, false } } auto router = spt::http::router::HttpRouter<const Request&, Response>::Builder{}. withNotFound( error404 ).withMethodNotAllowed( error405 ).build(); // set up router as in above sample nghttp2::asio_http2::server::http2 server; server.num_threads( 8 ); server.handle( "/", [&router](const nghttp2::asio_http2::server::request& req, const nghttp2::asio_http2::server::response& res) { auto request = Request{ req }; auto response = router.route( request.method, request.path, request ); assert( response ); res.write_head( response->status, response->headers ); res.end( std::move( response->body ) ); }); boost::system::error_code ec; if ( server.listen_and_serve( ec, "0.0.0.0", port, true ) ) { LOG_CRIT << "error: " << ec.message(); return 1; } }
Last modified: 18 February 2025