fix(pipeline): classify suffix http clients correctly

Generic route-method suffix fallback treated client calls such as resty.Get('/api/products') as route registration before registry/service classification could identify the HTTP client library. Centralize suffix policy in service_patterns so generic verb suffixes require handler evidence, while route-specific APIs such as websocket/include_router/add_api_route keep handlerless route-registration support.

Validation: edge_types_probe 52 passed; parallel 30 passed; pipeline 346 passed; httplink 39 passed; lang_contract passed; product build passed; source-safety passed; escalated full ASan/UBSan suite passed with 6426 tests. Logs are recorded in the active plan and forensics ledger.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-05 06:21:38 -04:00
parent 961cea5d98
commit 9ab9fd0cb9
4 changed files with 77 additions and 46 deletions
+62 -43
View File
@@ -463,61 +463,66 @@ static const lib_pattern_t trpc_libraries[] = {
{NULL, CBM_SVC_NONE, NULL},
};
/* Method suffix type (used by both route registration and HTTP client tables) */
typedef struct {
const char *suffix;
const char *method;
} method_suffix_t;
typedef struct {
const char *suffix;
const char *method;
bool allows_no_handler;
} route_reg_suffix_t;
/* Route registration method suffixes — matched on callee name.
* These are methods on router objects that register handlers. */
static const method_suffix_t route_reg_suffixes[] = {
static const route_reg_suffix_t route_reg_suffixes[] = {
/* HTTP method registrations */
{".GET", "GET"},
{".Get", "GET"},
{".get", "GET"},
{".POST", "POST"},
{".Post", "POST"},
{".post", "POST"},
{".PUT", "PUT"},
{".Put", "PUT"},
{".put", "PUT"},
{".DELETE", "DELETE"},
{".Delete", "DELETE"},
{".delete", "DELETE"},
{".PATCH", "PATCH"},
{".Patch", "PATCH"},
{".patch", "PATCH"},
{".GET", "GET", false},
{".Get", "GET", false},
{".get", "GET", false},
{".POST", "POST", false},
{".Post", "POST", false},
{".post", "POST", false},
{".PUT", "PUT", false},
{".Put", "PUT", false},
{".put", "PUT", false},
{".DELETE", "DELETE", false},
{".Delete", "DELETE", false},
{".delete", "DELETE", false},
{".PATCH", "PATCH", false},
{".Patch", "PATCH", false},
{".patch", "PATCH", false},
/* Handle/HandleFunc (Go stdlib, gorilla) */
{".Handle", "ANY"},
{".HandleFunc", "ANY"},
{".handle", "ANY"},
{".Handle", "ANY", false},
{".HandleFunc", "ANY", false},
{".handle", "ANY", false},
/* Framework-specific route registration */
{".Route", "ANY"},
{".route", "ANY"},
{".websocket_route", "ANY"},
{".websocket", "ANY"},
{"::get", "GET"},
{"::post", "POST"},
{"::put", "PUT"},
{"::delete", "DELETE"},
{"::patch", "PATCH"},
{".Route", "ANY", false},
{".route", "ANY", false},
{".websocket_route", "ANY", true},
{".websocket", "ANY", true},
{"::get", "GET", false},
{"::post", "POST", false},
{"::put", "PUT", false},
{"::delete", "DELETE", false},
{"::patch", "PATCH", false},
/* Minimal API (C# ASP.NET) */
{".MapGet", "GET"},
{".MapPost", "POST"},
{".MapPut", "PUT"},
{".MapDelete", "DELETE"},
{".MapGet", "GET", false},
{".MapPost", "POST", false},
{".MapPut", "PUT", false},
{".MapDelete", "DELETE", false},
/* Router mounting / prefix registration (any method) */
{".include_router", "ANY"},
{".mount", "ANY"},
{".add_url_rule", "ANY"},
{".register_blueprint", "ANY"},
{".use", "ANY"},
{".register", "ANY"},
{".add_route", "ANY"},
{".add_api_route", "ANY"},
{".add_api_websocket_route", "ANY"},
{NULL, NULL},
{".include_router", "ANY", true},
{".mount", "ANY", false},
{".add_url_rule", "ANY", true},
{".register_blueprint", "ANY", true},
{".use", "ANY", false},
{".register", "ANY", false},
{".add_route", "ANY", true},
{".add_api_route", "ANY", true},
{".add_api_websocket_route", "ANY", true},
{NULL, NULL, false},
};
/* ── HTTP method inference from function/method name suffix ───── */
@@ -893,6 +898,20 @@ const char *cbm_service_pattern_route_method(const char *callee_name) {
return NULL;
}
bool cbm_service_pattern_route_suffix_allows_no_handler(const char *callee_name) {
if (!callee_name) {
return false;
}
size_t clen = strlen(callee_name);
for (int i = 0; route_reg_suffixes[i].suffix != NULL; i++) {
size_t slen = strlen(route_reg_suffixes[i].suffix);
if (clen >= slen && strcmp(callee_name + clen - slen, route_reg_suffixes[i].suffix) == 0) {
return route_reg_suffixes[i].allows_no_handler;
}
}
return false;
}
const char *cbm_service_pattern_broker(const char *resolved_qn) {
if (!resolved_qn) {
return NULL;
+5
View File
@@ -53,6 +53,11 @@ const char *cbm_service_pattern_http_method(const char *callee_name);
* Returns NULL if not a known route registration method. */
const char *cbm_service_pattern_route_method(const char *callee_name);
/* True when a route-registration suffix is specific enough to use without a
* handler argument in suffix-only fallback. Generic HTTP verbs like .get/.Get
* return false so HTTP client wrappers get normal resolution first. */
bool cbm_service_pattern_route_suffix_allows_no_handler(const char *callee_name);
/* Classify a string literal as a genuine HTTP route path. Returns true for
* real routes ("/api/orders", "/users/:id", "https://..."); false for file
* paths ("/tmp/foo.md"), CLI slash-commands ("/ar:allow"), description strings
+6 -2
View File
@@ -330,7 +330,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
const char *handler_ref = NULL;
const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref);
if (route_path) {
if (route_path &&
((handler_ref && handler_ref[0] != '\0') ||
cbm_service_pattern_route_suffix_allows_no_handler(call->callee_name))) {
handle_route_registration(ctx, call, source_node, route_path, handler_ref,
module_qn, imp_keys, imp_vals, imp_count);
return SKIP_ONE;
@@ -342,7 +344,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
if (cbm_service_pattern_route_method(call->callee_name) != NULL) {
const char *handler_ref = NULL;
const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref);
if (route_path) {
if (route_path &&
((handler_ref && handler_ref[0] != '\0') ||
cbm_service_pattern_route_suffix_allows_no_handler(call->callee_name))) {
handle_route_registration(ctx, call, source_node, route_path, handler_ref, module_qn,
imp_keys, imp_vals, imp_count);
return SKIP_ONE;
+4 -1
View File
@@ -1303,7 +1303,10 @@ static void emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
if (svc == CBM_SVC_ROUTE_REG) {
const char *handler_ref = NULL;
const char *route_path = cbm_pipeline_call_route_path_and_handler(call, &handler_ref);
if (route_path) {
bool has_route_handler = handler_ref && handler_ref[0] != '\0';
bool handlerless_route_api =
cbm_service_pattern_route_suffix_allows_no_handler(call->callee_name);
if (route_path && (!suffix_only_route_reg || has_route_handler || handlerless_route_api)) {
emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count);
return;