mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(observability): demote backend Cloudflare anti-bot wrap (Sentry TAURI-RUST-34H) (#2692)
This commit is contained in:
@@ -636,6 +636,25 @@ fn is_provider_user_state_message(lower: &str) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TAURI-RUST-34H — composio backend endpoint (e.g.
|
||||
// `/agent-integrations/composio/connections`) wraps an upstream
|
||||
// Cloudflare anti-bot challenge as `Backend returned 500 Internal
|
||||
// Server Error … 403 <!DOCTYPE html>…<title>Just a moment...</title>…`.
|
||||
// The CF interstitial is keyed by the user's network reputation /
|
||||
// geo / cookie state — there is nothing in `openhuman_core` that
|
||||
// can act on it. Backend ops or the user's network is the
|
||||
// remediation path; Sentry has no signal.
|
||||
//
|
||||
// Double-anchor on the Cloudflare challenge title + the literal
|
||||
// "cloudflare" token to avoid colliding with unrelated bodies that
|
||||
// merely mention "Just a moment" in a different context.
|
||||
//
|
||||
// Drops ~8.9 k events / 14d (TAURI-RUST-34H, sibling -32G / -34J /
|
||||
// -323 share the same cascade).
|
||||
if lower.contains("just a moment...") && lower.contains("cloudflare") {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
@@ -2299,6 +2318,80 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// ── TAURI-RUST-34H: backend-wrapped Cloudflare anti-bot interstitial ─
|
||||
|
||||
#[test]
|
||||
fn classifies_backend_cloudflare_antibot_wrap_as_provider_user_state() {
|
||||
// Canonical Sentry TAURI-RUST-34H wire shape — the verbatim title
|
||||
// body from the issue (8,851 events / 14d on self-hosted
|
||||
// `tauri-rust`). The backend wraps an upstream Cloudflare 403
|
||||
// anti-bot challenge as `Backend returned 500 … 403 <!DOCTYPE …
|
||||
// Just a moment... … cloudflare …`. The 500 escapes the 4xx-only
|
||||
// `is_backend_user_error_message` classifier, so this body-shape
|
||||
// arm catches it and demotes to `ProviderUserState`.
|
||||
let msg = r#"Backend returned 500 Internal Server Error for GET https://api.tinyhumans.ai/agent-integrations/composio/connections: 403 <!DOCTYPE html><html lang="en-US"><head><title>Just a moment...</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><meta name="robots" content="noindex,nofollow"><meta name="viewport" content="width=device-width,initial-scale=1"><link href="/cdn-cgi/styles/challenges.css" rel="stylesheet"></head><body class="no-js"><div class="main-wrapper" role="main"><div class="main-content"><h1 class="zone-name-title h1"><img class="heading-favicon" src="/favicon.ico" onerror="this.onerror=null;this.parentNode.removeChild(this)" alt="Icon for api.tinyhumans.ai">api.tinyhumans.ai</h1>...Powered by Cloudflare..."#;
|
||||
assert_eq!(
|
||||
expected_error_kind(msg),
|
||||
Some(ExpectedErrorKind::ProviderUserState),
|
||||
"backend-wrapped Cloudflare anti-bot interstitial must demote to ProviderUserState"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_minimal_cloudflare_antibot_body_as_provider_user_state() {
|
||||
// Strip the wire shape down to just the two anchors — the
|
||||
// matcher should still fire so future renderings (different
|
||||
// line breaks, stripped HTML, alternate caller wrappers) still
|
||||
// demote.
|
||||
let msg = "Just a moment...\ncloudflare\n";
|
||||
assert_eq!(
|
||||
expected_error_kind(msg),
|
||||
Some(ExpectedErrorKind::ProviderUserState),
|
||||
"minimal `Just a moment...` + `cloudflare` body must demote"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_half_anchor_cloudflare_messages_as_user_state() {
|
||||
// Discrimination test for the double-anchor: either half on its
|
||||
// own must NOT match. This guards against unrelated bodies that
|
||||
// happen to use either phrase out of context.
|
||||
|
||||
// Half-anchor 1: `just a moment` without `cloudflare` — e.g.
|
||||
// a daemon restart spinner blurb.
|
||||
let half_a = "Just a moment, while we restart the daemon";
|
||||
assert_ne!(
|
||||
expected_error_kind(half_a),
|
||||
Some(ExpectedErrorKind::ProviderUserState),
|
||||
"`Just a moment` without `cloudflare` must NOT match the CF anti-bot arm"
|
||||
);
|
||||
|
||||
// Half-anchor 2: `cloudflare` without `just a moment...` — e.g.
|
||||
// a CF Workers footer mention elsewhere.
|
||||
let half_b = "Powered by Cloudflare";
|
||||
assert_ne!(
|
||||
expected_error_kind(half_b),
|
||||
Some(ExpectedErrorKind::ProviderUserState),
|
||||
"`cloudflare` without `Just a moment...` must NOT match the CF anti-bot arm"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_classify_genuine_backend_500_without_cloudflare_body() {
|
||||
// Real bug shape — a 500 from the same backend endpoint with no
|
||||
// Cloudflare interstitial body — must still fall through so
|
||||
// Sentry sees it. Without this guard the arm could be too
|
||||
// permissive and silence genuine database / handler faults.
|
||||
let msg = "Backend returned 500 Internal Server Error for GET \
|
||||
https://api.tinyhumans.ai/agent-integrations/composio/connections: \
|
||||
database connection pool exhausted";
|
||||
assert_eq!(
|
||||
expected_error_kind(msg),
|
||||
None,
|
||||
"genuine backend 500 without Cloudflare body must NOT demote — it is a real bug"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_local_ai_binary_missing_errors() {
|
||||
// OPENHUMAN-TAURI-9N: `local_ai_tts` returns this exact string
|
||||
|
||||
Reference in New Issue
Block a user