Add a fallback handler for routes that cannot be enumerated #5
2 changed files with 101 additions and 19 deletions
feat(http): give ListenerHTTP the same fallback and query-strip routing
ListenerHTTP1's docs promise the same route map shape as ListenerHTTP so a handler can be registered with both and served over either protocol. That only holds if the dispatch rule is the same on both, so mirror it here: exact `:path`, then the query-stripped path, then `fallback`, then the synthetic 404. The query-strip half is a behaviour change on this listener. `/thing?x=1` previously 404'd even with `/thing` registered, while HTTP/1.1 routed it — the asymmetry the shared route map was supposed to avoid. It also matters for `fallback`: without it a query string would divert a registered path to the fallback over HTTP/3 but not over HTTP/1.1. `fallback` covers `routes` only. An unmatched WebTransport CONNECT is still a 404 — a WT handler takes a session, not a request, so there is nothing sensible to hand it. MakeBidiHandler now reads the maps off `self` instead of taking them as pointer parameters. `self` was already captured and unused, and this mirrors how ListenerHTTP1 reaches its own state through Impl::owner.
commit
7a524cfdd0
|
|
@ -157,14 +157,13 @@ struct ListenerHTTP::Impl {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Build the per-connection bidi-stream handler. Demuxes WT streams from
|
// Build the per-connection bidi-stream handler. Demuxes WT streams from
|
||||||
// HTTP/3 request streams by peeking the first varint on the wire. Lives
|
// HTTP/3 request streams by peeking the first varint on the wire. Reaches
|
||||||
// as a free helper so both ListenerHTTP constructors can install it.
|
// the route maps and `fallback` through `self` rather than capturing them,
|
||||||
std::function<void(QUICStream)> MakeBidiHandler(
|
// so nothing here has to be re-plumbed when a dispatch input is added.
|
||||||
ListenerHTTP* self, PeerState* peerState,
|
std::function<void(QUICStream)> MakeBidiHandler(ListenerHTTP* self, PeerState* peerState) {
|
||||||
const std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>>* routes,
|
return [self, peerState](QUICStream stream) {
|
||||||
const std::unordered_map<std::string, std::function<void(WebTransportSession&)>>* wtRoutes)
|
const auto& routes = self->routes;
|
||||||
{
|
const auto& wtRoutes = self->wtRoutes;
|
||||||
return [self, peerState, routes, wtRoutes](QUICStream stream) {
|
|
||||||
try {
|
try {
|
||||||
// ── Phase A: identify the stream kind ─────────────────────
|
// ── Phase A: identify the stream kind ─────────────────────
|
||||||
//
|
//
|
||||||
|
|
@ -175,7 +174,7 @@ namespace {
|
||||||
std::size_t cursor = 0;
|
std::size_t cursor = 0;
|
||||||
std::uint64_t firstType = ReadVarintFromStream(stream, peeked, cursor);
|
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.
|
// ── WT bidi data stream — second varint is session id.
|
||||||
std::uint64_t sessionId = ReadVarintFromStream(stream, peeked, cursor);
|
std::uint64_t sessionId = ReadVarintFromStream(stream, peeked, cursor);
|
||||||
std::vector<char> remaining(peeked.begin() + cursor, peeked.end());
|
std::vector<char> remaining(peeked.begin() + cursor, peeked.end());
|
||||||
|
|
@ -240,8 +239,8 @@ namespace {
|
||||||
if (request.method == "CONNECT" && protoIt != request.headers.end()
|
if (request.method == "CONNECT" && protoIt != request.headers.end()
|
||||||
&& protoIt->second == "webtransport")
|
&& protoIt->second == "webtransport")
|
||||||
{
|
{
|
||||||
auto wtIt = wtRoutes->find(request.path);
|
auto wtIt = wtRoutes.find(request.path);
|
||||||
if (wtIt == wtRoutes->end()) {
|
if (wtIt == wtRoutes.end()) {
|
||||||
HTTPResponse nf; nf.status = "404"; nf.body = "WebTransport route not found";
|
HTTPResponse nf; nf.status = "404"; nf.body = "WebTransport route not found";
|
||||||
auto wire = SerializeResponse(nf);
|
auto wire = SerializeResponse(nf);
|
||||||
try { stream.SendSync(wire.data(), static_cast<std::uint32_t>(wire.size()), true); } catch (...) {}
|
try { stream.SendSync(wire.data(), static_cast<std::uint32_t>(wire.size()), true); } catch (...) {}
|
||||||
|
|
@ -313,10 +312,18 @@ namespace {
|
||||||
pos += static_cast<std::size_t>(frameLen);
|
pos += static_cast<std::size_t>(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;
|
HTTPResponse response;
|
||||||
auto it = routes->find(request.path);
|
if (route) {
|
||||||
if (it != routes->end()) {
|
response = route(request);
|
||||||
response = it->second(request);
|
|
||||||
} else {
|
} else {
|
||||||
response.status = "404";
|
response.status = "404";
|
||||||
response.body = "Not Found";
|
response.body = "Not Found";
|
||||||
|
|
@ -344,15 +351,31 @@ namespace {
|
||||||
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
||||||
QUICServerCredentials creds,
|
QUICServerCredentials creds,
|
||||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r)
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r)
|
||||||
: ListenerHTTP(port, std::move(creds), std::move(r), {})
|
: ListenerHTTP(port, std::move(creds), std::move(r), {}, {})
|
||||||
{}
|
{}
|
||||||
|
|
||||||
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
||||||
QUICServerCredentials creds,
|
QUICServerCredentials creds,
|
||||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r,
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r,
|
||||||
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wt)
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wt)
|
||||||
|
: ListenerHTTP(port, std::move(creds), std::move(r), std::move(wt), {})
|
||||||
|
{}
|
||||||
|
|
||||||
|
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fb)
|
||||||
|
: ListenerHTTP(port, std::move(creds), std::move(r), {}, std::move(fb))
|
||||||
|
{}
|
||||||
|
|
||||||
|
ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> r,
|
||||||
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wt,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fb)
|
||||||
: routes(std::move(r))
|
: routes(std::move(r))
|
||||||
, wtRoutes(std::move(wt))
|
, wtRoutes(std::move(wt))
|
||||||
|
, fallback(std::move(fb))
|
||||||
, alpn(HTTP3::kAlpn)
|
, alpn(HTTP3::kAlpn)
|
||||||
, impl(std::make_unique<Impl>())
|
, impl(std::make_unique<Impl>())
|
||||||
{
|
{
|
||||||
|
|
@ -372,7 +395,7 @@ ListenerHTTP::ListenerHTTP(std::uint16_t port,
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Bidi: either HTTP/3 request or WT data stream. Demux inside.
|
// 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));
|
handler(std::move(stream));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -447,6 +470,23 @@ ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port,
|
||||||
, thread(&ListenerHTTP::Listen, &listener)
|
, thread(&ListenerHTTP::Listen, &listener)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
ListenerAsyncHTTP::ListenerAsyncHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> 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<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fallback)
|
||||||
|
: listener(port, std::move(creds), std::move(routes), std::move(wtRoutes), std::move(fallback))
|
||||||
|
, thread(&ListenerHTTP::Listen, &listener)
|
||||||
|
{}
|
||||||
|
|
||||||
ListenerAsyncHTTP::~ListenerAsyncHTTP() {
|
ListenerAsyncHTTP::~ListenerAsyncHTTP() {
|
||||||
Stop();
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,11 @@ namespace Crafter {
|
||||||
// through the route map, and writes a response back on the same bidi
|
// through the route map, and writes a response back on the same bidi
|
||||||
// stream. ALPN is fixed to "h3".
|
// stream. ALPN is fixed to "h3".
|
||||||
//
|
//
|
||||||
// Routes are keyed by `:path` (exact match). Unknown paths return a
|
// Routes are keyed by `:path`, matched exactly and then with the query
|
||||||
// synthetic 404. Route handlers run on the ThreadPool — multiple requests
|
// string stripped. A path matching nothing goes to `fallback` if one is
|
||||||
// on the same connection can therefore execute concurrently.
|
// 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
|
// WebTransport: pass a non-empty `wtRoutes` to additionally accept
|
||||||
// extended-CONNECT requests (`:method=CONNECT, :protocol=webtransport`)
|
// extended-CONNECT requests (`:method=CONNECT, :protocol=webtransport`)
|
||||||
|
|
@ -33,6 +35,19 @@ namespace Crafter {
|
||||||
// straightforward.
|
// straightforward.
|
||||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes;
|
||||||
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes;
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> 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/<token>`,
|
||||||
|
// `/shop/<slug>` — 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<HTTPResponse(const HTTPRequest&)> fallback;
|
||||||
std::string alpn;
|
std::string alpn;
|
||||||
|
|
||||||
ListenerHTTP(std::uint16_t port,
|
ListenerHTTP(std::uint16_t port,
|
||||||
|
|
@ -46,6 +61,19 @@ namespace Crafter {
|
||||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes);
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> 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<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||||
|
|
||||||
|
ListenerHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||||
|
|
||||||
~ListenerHTTP();
|
~ListenerHTTP();
|
||||||
ListenerHTTP(const ListenerHTTP&) = delete;
|
ListenerHTTP(const ListenerHTTP&) = delete;
|
||||||
ListenerHTTP(ListenerHTTP&&) noexcept;
|
ListenerHTTP(ListenerHTTP&&) noexcept;
|
||||||
|
|
@ -78,6 +106,20 @@ namespace Crafter {
|
||||||
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes);
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> 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<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||||
|
|
||||||
|
ListenerAsyncHTTP(std::uint16_t port,
|
||||||
|
QUICServerCredentials creds,
|
||||||
|
std::unordered_map<std::string, std::function<HTTPResponse(const HTTPRequest&)>> routes,
|
||||||
|
std::unordered_map<std::string, std::function<void(WebTransportSession&)>> wtRoutes,
|
||||||
|
std::function<HTTPResponse(const HTTPRequest&)> fallback);
|
||||||
|
|
||||||
~ListenerAsyncHTTP();
|
~ListenerAsyncHTTP();
|
||||||
void Stop();
|
void Stop();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue