feat(http1): dispatch unmatched paths to an optional fallback handler
The route map only answers paths that are known when the listener is built. A route with an unbounded segment — /shop/<slug>, /order/<token>, /posts/<id> — cannot be pre-registered: the token space is unbounded and the product set changes while the server runs. Every such request became a synthetic 404 that the application never got to see. Add one optional member, called for anything `routes` missed, with the full target still in request.path. Precedence is exact path, then the query-stripped path, then fallback, then the 404 as before, so a default-constructed listener behaves byte-identically and no call site changes. A hook rather than a pattern syntax: consumers that already have a router — one shared between a wasm frontend and the server, so a URL cannot mean different things to a crawler and to the app — keep using it, and there is no second route table to disagree with the first. PathWithoutQuery moves out of this file into :HTTP as an exported PathWithoutQueryHTTP, since ListenerHTTP now needs the same split and a fallback handler almost always does too. ListenerAsyncHTTP1 starts accepting inside its constructor, so assigning `listener.fallback` afterwards would race the accept loop; it gets a constructor overload that installs the fallback before the thread starts.
This commit is contained in:
parent
219a31a8f7
commit
4d5ef7c96b
3 changed files with 54 additions and 14 deletions
|
|
@ -43,14 +43,6 @@ namespace {
|
|||
return ReadStatus::Data;
|
||||
}
|
||||
}
|
||||
|
||||
// Everything up to '?' — used as the fallback route key so `/thing?x=1`
|
||||
// reaches the handler registered for `/thing`. Browsers append query
|
||||
// strings freely, and requiring handlers to register every variant is
|
||||
// not a workable API.
|
||||
std::string_view PathWithoutQuery(std::string_view target) {
|
||||
return target.substr(0, target.find('?'));
|
||||
}
|
||||
}
|
||||
|
||||
// One accepted connection: the socket, the thread serving it, and a flag
|
||||
|
|
@ -128,14 +120,15 @@ struct ListenerHTTP1::Impl {
|
|||
const auto& routes = owner->routes;
|
||||
auto route = routes.find(request.path);
|
||||
if (route == routes.end()) {
|
||||
const std::string bare(PathWithoutQuery(request.path));
|
||||
const std::string bare(PathWithoutQueryHTTP(request.path));
|
||||
route = routes.find(bare);
|
||||
}
|
||||
if (route == routes.end()) {
|
||||
const auto& handler = route != routes.end() ? route->second : owner->fallback;
|
||||
if (!handler) {
|
||||
return CreateResponseHTTP("404", "Not Found");
|
||||
}
|
||||
try {
|
||||
return route->second(request);
|
||||
return handler(request);
|
||||
} catch (const std::exception& error) {
|
||||
return CreateResponseHTTP("500", std::string(error.what()));
|
||||
} catch (...) {
|
||||
|
|
@ -242,7 +235,14 @@ struct ListenerHTTP1::Impl {
|
|||
|
||||
ListenerHTTP1::ListenerHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes)
|
||||
: ListenerHTTP1(port, std::move(routes), {})
|
||||
{}
|
||||
|
||||
ListenerHTTP1::ListenerHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback)
|
||||
: routes(std::move(routes))
|
||||
, fallback(std::move(fallback))
|
||||
, impl(std::make_unique<Impl>())
|
||||
{
|
||||
impl->owner = this;
|
||||
|
|
@ -254,6 +254,7 @@ ListenerHTTP1::ListenerHTTP1(std::uint16_t port,
|
|||
|
||||
ListenerHTTP1::ListenerHTTP1(ListenerHTTP1&& other) noexcept
|
||||
: routes(std::move(other.routes))
|
||||
, fallback(std::move(other.fallback))
|
||||
, keepAliveTimeout(other.keepAliveTimeout)
|
||||
, requestTimeout(other.requestTimeout)
|
||||
, limits(other.limits)
|
||||
|
|
@ -318,6 +319,13 @@ ListenerAsyncHTTP1::ListenerAsyncHTTP1(std::uint16_t port,
|
|||
, thread(&ListenerHTTP1::Listen, &listener)
|
||||
{}
|
||||
|
||||
ListenerAsyncHTTP1::ListenerAsyncHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback)
|
||||
: listener(port, std::move(routes), std::move(fallback))
|
||||
, thread(&ListenerHTTP1::Listen, &listener)
|
||||
{}
|
||||
|
||||
ListenerAsyncHTTP1::~ListenerAsyncHTTP1() {
|
||||
Stop();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,15 @@ namespace Crafter {
|
|||
std::string body;
|
||||
};
|
||||
|
||||
// Everything in a request target up to '?'. Both listeners dispatch on
|
||||
// this when the full target is not a registered route, so `/thing?x=1`
|
||||
// reaches the handler registered for `/thing`: browsers append query
|
||||
// strings freely and registering every variant is not a workable API.
|
||||
// Exported because a fallback handler generally needs the same split.
|
||||
export inline std::string_view PathWithoutQueryHTTP(std::string_view target) {
|
||||
return target.substr(0, target.find('?'));
|
||||
}
|
||||
|
||||
export inline HTTPRequest CreateRequestHTTP(std::string method, std::string path, std::string authority) {
|
||||
HTTPRequest r;
|
||||
r.method = std::move(method);
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import :HTTP1;
|
|||
|
||||
#ifndef CRAFTER_NETWORK_BROWSER
|
||||
namespace Crafter {
|
||||
// HTTP/1.1 server over plain TCP. Same route map shape as ListenerHTTP,
|
||||
// so a handler can be registered with both and served over either
|
||||
// protocol.
|
||||
// HTTP/1.1 server over plain TCP. Same route map and `fallback` shape as
|
||||
// ListenerHTTP, so a handler can be registered with both and served over
|
||||
// either protocol.
|
||||
//
|
||||
// Each accepted connection gets its own thread and is served
|
||||
// sequentially until the peer closes it, `Connection: close` is seen, or
|
||||
|
|
@ -29,6 +29,17 @@ namespace Crafter {
|
|||
public:
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
|
||||
|
||||
// Called for any request whose target matches no entry in `routes`,
|
||||
// with the full target still in `request.path`. Lets a caller route
|
||||
// paths that cannot be enumerated up front — `/order/<token>`,
|
||||
// `/shop/<slug>` — with its own matcher, instead of this class
|
||||
// imposing a pattern syntax. Leave unset to keep synthesising a 404.
|
||||
//
|
||||
// Must be assigned before Listen(); the accept loop reads it without
|
||||
// synchronisation. ListenerAsyncHTTP1 starts listening in its
|
||||
// constructor, so pass the fallback to that constructor instead.
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback;
|
||||
|
||||
// How long a connection may stay idle between requests, and how long
|
||||
// a single request may take to arrive once started. Both guard
|
||||
// against a peer holding a thread forever.
|
||||
|
|
@ -40,6 +51,10 @@ namespace Crafter {
|
|||
ListenerHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes);
|
||||
|
||||
ListenerHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||
|
||||
~ListenerHTTP1();
|
||||
ListenerHTTP1(const ListenerHTTP1&) = delete;
|
||||
ListenerHTTP1(ListenerHTTP1&&) noexcept;
|
||||
|
|
@ -69,6 +84,14 @@ namespace Crafter {
|
|||
|
||||
ListenerAsyncHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes);
|
||||
|
||||
// Fallback-aware overload. The accept loop starts inside this
|
||||
// constructor, so a fallback has to be installed here rather than
|
||||
// assigned to `listener.fallback` afterwards.
|
||||
ListenerAsyncHTTP1(std::uint16_t port,
|
||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||
|
||||
~ListenerAsyncHTTP1();
|
||||
void Stop();
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue