BloopAI/vibe-kanban · critical

Approval path should be valid

Error message

Approval path should be valid

What it means

This panic fires when the dynamically built path "/pending/{approval_id}" fails `try_into()` to a `json_patch` JSON Pointer. Parsing fails if the string doesn't start with '/' or contains unescaped '~' or '/' characters. The code already escapes the approval_id via escape_pointer_segment, so the only real trigger is an approval_id containing characters that break the pointer after (mis)escaping, or a regression in the path builder.

Source

Thrown at crates/services/src/services/events/patches.rs:172

                    serde_json::to_value(info).unwrap_or(serde_json::Value::Null),
                )
            })
            .collect();

        Patch(vec![PatchOperation::Replace(ReplaceOperation {
            path: PENDING_PATH
                .try_into()
                .expect("Pending approvals path should be valid"),
            value: serde_json::Value::Object(pending),
        })])
    }

    pub fn created(info: &crate::services::approvals::ApprovalInfo) -> Patch {
        let value = serde_json::to_value(info).unwrap_or(serde_json::Value::Null);
        Patch(vec![PatchOperation::Replace(ReplaceOperation {
            path: pending_path(&info.approval_id)
                .try_into()
                .expect("Approval path should be valid"),
            value,
        })])
    }

    pub fn resolved(approval_id: &str) -> Patch {
        Patch(vec![PatchOperation::Remove(RemoveOperation {
            path: pending_path(approval_id)
                .try_into()
                .expect("Approval path should be valid"),
        })])
    }
}

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Ensure every dynamic segment is escaped: keep `escape_pointer_segment(&info.approval_id)` inside pending_path before formatting.
  2. Validate/sanitize approval_id at approval-creation time (non-empty, UUID or alphanumerics only).
  3. Use error handling instead of expect if ids can be untrusted: match on try_into() and log/skip the patch rather than panicking the event loop.
  4. Add a unit test asserting pending_path(id).try_into::<Pointer>() is Ok for edge-case ids ("~", "a/b", "").

Example fix

// before
fn pending_path(approval_id: &str) -> String {
    format!("{}/{}", PENDING_PATH, approval_id)
}
// after
fn pending_path(approval_id: &str) -> String {
    format!("{}/{}", PENDING_PATH, escape_pointer_segment(approval_id))
}
Defensive patterns

Strategy: validation

Validate before calling

// Validate id before building the patch
fn valid_approval_id(id: &str) -> bool {
    !id.is_empty() && !id.contains(|c: char| c == '~' || c == '/') && json_patch::Pointer::try_from(format!("/pending/{}", id)).is_ok()
}

Type guard

fn is_safe_pointer_segment(s: &str) -> bool {
    !s.is_empty() && !s.contains('~') && !s.contains('/')
}

Try / catch

// Avoid expect on untrusted ids:
let path = pending_path(&info.approval_id);
let Ok(ptr) = json_patch::Pointer::try_from(path) else {
    log::error!("skipping approval patch: invalid id {:?}", info.approval_id);
    return Patch(vec![]);
};

Prevention

When it happens

Trigger: Calling `approvals_patch::created(&ApprovalInfo { approval_id, .. })` where `approval_id` is empty, or where it yields an invalid pointer segment — e.g. an id containing '~' or '/' that wasn't run through escape_pointer_segment, or if pending_path were changed to drop the leading PENDING_PATH ('/') giving a bare relative pointer.

Common situations: IDs generated upstream with unusual characters; a refactor that drops escape_pointer_segment; an empty approval_id passed through from an approval service that failed to assign an id.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/f750603578b767e4. Report an issue: GitHub.