diff --git a/implementations/Crafter.Network-ListenerHTTP.cpp b/implementations/Crafter.Network-ListenerHTTP.cpp index b364cfd..21f6a95 100644 --- a/implementations/Crafter.Network-ListenerHTTP.cpp +++ b/implementations/Crafter.Network-ListenerHTTP.cpp @@ -157,14 +157,13 @@ struct ListenerHTTP::Impl { namespace { // Build the per-connection bidi-stream handler. Demuxes WT streams from - // HTTP/3 request streams by peeking the first varint on the wire. Lives - // as a free helper so both ListenerHTTP constructors can install it. - std::function MakeBidiHandler( - ListenerHTTP* self, PeerState* peerState, - const std::unordered_map>* routes, - const std::unordered_map>* wtRoutes) - { - return [self, peerState, routes, wtRoutes](QUICStream stream) { + // HTTP/3 request streams by peeking the first varint on the wire. Reaches + // the route maps and `fallback` through `self` rather than capturing them, + // so nothing here has to be re-plumbed when a dispatch input is added. + std::function MakeBidiHandler(ListenerHTTP* self, PeerState* peerState) { + return [self, peerState](QUICStream stream) { + const auto& routes = self->routes; + const auto& wtRoutes = self->wtRoutes; try { // ── Phase A: identify the stream kind ───────────────────── // @@ -175,7 +174,7 @@ namespace { std::size_t cursor = 0; std::uint64_t firstType = ReadVarintFromStream(stream, peeked, cursor); - if (firstType == HTTP3::kFrameWtStream && !wtRoutes->empty()) { + if (firstType == HTTP3::kFrameWtStream && !wtRoutes.empty()) { // ── WT bidi data stream — second varint is session id. std::uint64_t sessionId = ReadVarintFromStream(stream, peeked, cursor); std::vector remaining(peeked.begin() + cursor, peeked.end()); @@ -240,8 +239,8 @@ namespace { if (request.method == "CONNECT" && protoIt != request.headers.end() && protoIt->second == "webtransport") { - auto wtIt = wtRoutes->find(request.path); - if (wtIt == wtRoutes->end()) { + auto wtIt = wtRoutes.find(request.path); + if (wtIt == wtRoutes.end()) { HTTPResponse nf; nf.status = "404"; nf.body = "WebTransport route not found"; auto wire = SerializeResponse(nf); try { stream.SendSync(wire.data(), static_cast(wire.size()), true); } catch (...) {} @@ -313,10 +312,18 @@ namespace { pos += static_cast(frameLen); } + // Exact `:path`, then the query-stripped path, then the + // caller's fallback. Same precedence as ListenerHTTP1, so a + // handler set behaves identically over either protocol. + auto it = routes.find(request.path); + if (it == routes.end()) { + it = routes.find(std::string(PathWithoutQueryHTTP(request.path))); + } + const auto& route = it != routes.end() ? it->second : self->fallback; + HTTPResponse response; - auto it = routes->find(request.path); - if (it != routes->end()) { - response = it->second(request); + if (route) { + response = route(request); } else { response.status = "404"; response.body = "Not Found"; @@ -344,15 +351,31 @@ namespace { ListenerHTTP::ListenerHTTP(std::uint16_t port, QUICServerCredentials creds, std::unordered_map> r) - : ListenerHTTP(port, std::move(creds), std::move(r), {}) + : ListenerHTTP(port, std::move(creds), std::move(r), {}, {}) {} ListenerHTTP::ListenerHTTP(std::uint16_t port, QUICServerCredentials creds, std::unordered_map> r, std::unordered_map> wt) + : ListenerHTTP(port, std::move(creds), std::move(r), std::move(wt), {}) +{} + +ListenerHTTP::ListenerHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> r, + std::function fb) + : ListenerHTTP(port, std::move(creds), std::move(r), {}, std::move(fb)) +{} + +ListenerHTTP::ListenerHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> r, + std::unordered_map> wt, + std::function fb) : routes(std::move(r)) , wtRoutes(std::move(wt)) + , fallback(std::move(fb)) , alpn(HTTP3::kAlpn) , impl(std::make_unique()) { @@ -372,7 +395,7 @@ ListenerHTTP::ListenerHTTP(std::uint16_t port, return; } // Bidi: either HTTP/3 request or WT data stream. Demux inside. - auto handler = MakeBidiHandler(this, statePtr, &this->routes, &this->wtRoutes); + auto handler = MakeBidiHandler(this, statePtr); handler(std::move(stream)); }); @@ -447,6 +470,23 @@ ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, , thread(&ListenerHTTP::Listen, &listener) {} +ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> routes, + std::function fallback) + : listener(port, std::move(creds), std::move(routes), std::move(fallback)) + , thread(&ListenerHTTP::Listen, &listener) +{} + +ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> routes, + std::unordered_map> wtRoutes, + std::function fallback) + : listener(port, std::move(creds), std::move(routes), std::move(wtRoutes), std::move(fallback)) + , thread(&ListenerHTTP::Listen, &listener) +{} + ListenerAsyncHTTP::~ListenerAsyncHTTP() { Stop(); } diff --git a/interfaces/Crafter.Network-ListenerHTTP.cppm b/interfaces/Crafter.Network-ListenerHTTP.cppm index 20076d3..a6b0ec2 100644 --- a/interfaces/Crafter.Network-ListenerHTTP.cppm +++ b/interfaces/Crafter.Network-ListenerHTTP.cppm @@ -15,9 +15,11 @@ namespace Crafter { // through the route map, and writes a response back on the same bidi // stream. ALPN is fixed to "h3". // - // Routes are keyed by `:path` (exact match). Unknown paths return a - // synthetic 404. Route handlers run on the ThreadPool — multiple requests - // on the same connection can therefore execute concurrently. + // Routes are keyed by `:path`, matched exactly and then with the query + // string stripped. A path matching nothing goes to `fallback` if one is + // set, and returns a synthetic 404 otherwise. Route handlers run on the + // ThreadPool — multiple requests on the same connection can therefore + // execute concurrently. // // WebTransport: pass a non-empty `wtRoutes` to additionally accept // extended-CONNECT requests (`:method=CONNECT, :protocol=webtransport`) @@ -33,6 +35,19 @@ namespace Crafter { // straightforward. std::unordered_map> routes; std::unordered_map> wtRoutes; + + // Called for any request whose `:path` matches no entry in `routes`, + // with the full path still in `request.path`. Lets a caller route + // paths that cannot be enumerated up front — `/order/`, + // `/shop/` — with its own matcher, instead of this class + // imposing a pattern syntax. Leave unset to keep synthesising a 404. + // Applies to `routes` only: an unmatched WebTransport CONNECT is + // still a 404, since a WT handler has a different signature. + // + // Must be assigned before Listen(); stream handlers read it without + // synchronisation. ListenerAsyncHTTP starts listening in its + // constructor, so pass the fallback to that constructor instead. + std::function fallback; std::string alpn; ListenerHTTP(std::uint16_t port, @@ -46,6 +61,19 @@ namespace Crafter { std::unordered_map> routes, std::unordered_map> wtRoutes); + // Fallback-aware overloads, for callers that construct and Listen() + // in one step (and for ListenerAsyncHTTP, which has to). + ListenerHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> routes, + std::function fallback); + + ListenerHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> routes, + std::unordered_map> wtRoutes, + std::function fallback); + ~ListenerHTTP(); ListenerHTTP(const ListenerHTTP&) = delete; ListenerHTTP(ListenerHTTP&&) noexcept; @@ -78,6 +106,20 @@ namespace Crafter { std::unordered_map> routes, std::unordered_map> wtRoutes); + // Fallback-aware overloads. The accept loop starts inside these + // constructors, so a fallback has to be installed here rather than + // assigned to `listener.fallback` afterwards. + ListenerAsyncHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> routes, + std::function fallback); + + ListenerAsyncHTTP(std::uint16_t port, + QUICServerCredentials creds, + std::unordered_map> routes, + std::unordered_map> wtRoutes, + std::function fallback); + ~ListenerAsyncHTTP(); void Stop(); };