{"record":{"id":"95b1bc63fdc604db","repo":"stamparm/maltrail","slug":"just-probed","errorCode":null,"errorMessage":"just probed","messagePattern":"just probed","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/process.rs","lineNumber":1137,"sourceCode":"            } else {\n                // parts = url.split(check); every non-empty part is bracketed\n                let url = match &prebuilt_url {\n                    Some(u) => Cow::Borrowed(u.as_str()),\n                    None => Cow::Owned(format!(\"{host}{path}\")),\n                };\n                let trail = bracket_around(&url, &candidate);\n                emit_ep(st, sec, usec, ep, PROTO::TCP, TRAIL::URL, Field::Text(trail), &info, &reference);\n            }\n            return;\n        }\n    }\n\n    // `format!(\"{host}/\")` allocated on every request; the candidate buffer is already sized.\n    candidate.clear();\n    candidate.push_str(&host);\n    candidate.push('/');\n    if st.trails.db().get(&candidate).is_some() {\n        let hit = st.trails.db().get(&candidate).expect(\"just probed\");\n        let (info, reference) = (hit.info.to_string(), hit.reference.to_string());\n        emit_ep(st, sec, usec, ep, PROTO::TCP, TRAIL::URL, Field::Text(candidate.clone()), &info, &reference);\n        return;\n    }\n\n    if !st.cfg.use_heuristics || heuristics_suppressed {\n        return;\n    }\n\n    // Forwarded-for headers are searched in the RAW packet bytes, case-insensitively. The\n    // literal pre-condition comes first: asking a case-insensitive alternation for capture\n    // groups walks the whole packet, and next to no request carries one of these headers.\n    let mut src_ip_field = ep.src.render().as_str().to_string();\n    if st.statics.forwarded_for_pre_condition.is_match(packet_bytes) {\n        if let Some(caps) = st.statics.forwarded_for.captures(packet_bytes) {\n            if let Some(m) = caps.get(2) {\n                let forwarded = String::from_utf8_lossy(m.as_bytes()).to_string();\n                src_ip_field = format!(\"{src_ip_field},{forwarded}\");","sourceCodeStart":1119,"sourceCodeEnd":1155,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/process.rs#L1119-L1155","documentation":"In the HTTP request host-based trail lookup, the code calls `st.trails.db().get(&candidate)` twice: once with `is_some()` to test, then again with `.expect(\"just probed\")` to unwrap. If the two lookups ever disagree — or the first check passes but the second returns `None` — the `expect` panics. This is a redundant double-lookup pattern whose invariant ('we just probed it, so it must still be there') is asserted rather than guaranteed.","triggerScenarios":"An HTTP request whose `host + '/'` key is reported present by the first `db().get()` but absent on the immediate second `db().get()`, panicking at `.expect(\"just probed\")`. Any non-deterministic `get` (concurrent DB reload/swap between the two calls, `get` with side effects or interior mutability) triggers it.","commonSituations":"Trails DB hot-reload racing request handling; a custom `db()` implementation whose `get` is not pure; copy-paste refactors where the boolean check and the unwrap query different keys.","solutions":["Replace the check-then-unwrap double lookup with a single `if let Some(hit) = st.trails.db().get(&candidate)` that uses the returned reference directly — removes the invariant entirely.","If cloning is needed, clone `info`/`reference` from the single `get` result instead of re-querying.","Ensure the trails DB handle returned by `db()` is stable across calls within one request, or snapshot it once per request.","Add a concurrency test that reloads the DB while requests are parsed to prove no panic path remains."],"exampleFix":"// before\nif st.trails.db().get(&candidate).is_some() {\n    let hit = st.trails.db().get(&candidate).expect(\"just probed\");\n    let (info, reference) = (hit.info.to_string(), hit.reference.to_string());\n    ...\n}\n// after\nif let Some(hit) = st.trails.db().get(&candidate) {\n    let (info, reference) = (hit.info.to_string(), hit.reference.to_string());\n    ...\n}","handlingStrategy":"validation","validationCode":"// single lookup; no second get() can disagree\nif let Some(hit) = st.trails.db().get(&candidate) { /* use hit */ }","typeGuard":null,"tryCatchPattern":"catch_unwind(AssertUnwindSafe(|| handle_http_host(st, ep)))\n    .unwrap_or_else(|_| log::warn!(\"http host lookup panicked; request skipped\"));","preventionTips":["Never check-then-requery: bind the result of the first get()","Treat db().get() as possibly non-idempotent under reload","Snapshot the DB handle once per request","Fuzz/concurrency-test with concurrent DB reloads"],"tags":["rust","panic","expect","http","double-lookup"],"backgroundTag":"internal-invariant-violation","analyzedSha":"77cfb06d7606506d101bbcec0786c77166c4255e","analyzedAt":"2026-09-13T03:50:16.010Z","contentChangedAt":"2026-09-13T03:50:16.010Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}