tinyhumansai/openhuman · warning · anyhow::Error
filter provider '{}' does not match source provider '{}'
Error message
filter provider '{}' does not match source provider '{}' What it means
Contract check in task_sources::store::add_source: FilterSpec is a provider-tagged enum (github|notion|linear|clickup) and its tag must equal the provider argument of the source being created. The store refuses to persist a source whose filter targets a different provider, because every downstream fetcher resolves the provider registry from TaskSource.provider and would misinterpret the filter.
Source
Thrown at src/openhuman/integrations/task_sources/store.rs:73
);
let digest = Sha256::digest(canonical.as_bytes());
format!("{digest:x}")
}
/// Insert a new task source.
#[allow(clippy::too_many_arguments)]
pub fn add_source(
config: &Config,
provider: ProviderSlug,
connection_id: Option<String>,
name: Option<String>,
filter: FilterSpec,
interval_secs: u64,
target: SourceTarget,
max_tasks_per_fetch: u32,
) -> Result<TaskSource> {
if filter.provider() != provider {
anyhow::bail!(
"filter provider '{}' does not match source provider '{}'",
filter.provider().as_str(),
provider.as_str()
);
}
// Normalize blank optional fields to NULL so a whitespace-only
// connection_id can't masquerade as a real selector (mirrors
// `update_source`).
let connection_id = connection_id.filter(|s| !s.trim().is_empty());
let name = name.filter(|s| !s.trim().is_empty());
let id = Uuid::new_v4().to_string();
let now = Utc::now();
let filter_json = serde_json::to_string(&filter).context("serialize task source filter")?;
let target_json = serde_json::to_string(&target).context("serialize task source target")?;
let interval_i64 = i64::try_from(interval_secs)
.context("task source interval_secs exceeds SQLite INTEGER range")?;
with_connection(config, |conn| {View on GitHub (pinned to 7491200858)
Solutions
- Derive the provider from the filter itself — add_source(config, filter.provider(), ...) — so the two can never diverge
- If the provider is the source of truth, rebuild the filter as the matching variant (FilterSpec::Github for ProviderSlug::Github)
- In the frontend, reset the filter form whenever the provider selection changes
- Validate the pair before persisting and return a field-level error to the user
Example fix
// before — hardcoded provider can drift from the filter variant let provider = ProviderSlug::Linear; add_source(&config, provider, conn_id, name, filter, 300, target, 25)?; // after — derive the provider from the filter tag add_source(&config, filter.provider(), conn_id, name, filter.clone(), 300, target, 25)?;
Defensive patterns
Strategy: type-guard
Validate before calling
if filter.provider() != provider {
anyhow::bail!(
"filter targets {} but source provider is {} — rebuild the filter for the selected provider",
filter.provider().as_str(),
provider.as_str()
);
} Type guard
fn filter_matches_source(filter: &FilterSpec, provider: ProviderSlug) -> bool {
filter.provider() == provider
} Try / catch
match add_source(&config, provider, conn, name, filter, interval, target, max).await {
Ok(src) => Ok(src),
Err(e) if format!("{e:#}").contains("does not match source provider") => {
// UI sent a stale filter for a different provider — reset the filter form, not a retry
Err(anyhow::anyhow!("filter/provider mismatch: reset the filter for the selected provider"))
}
Err(e) => Err(e),
} Prevention
- Derive the provider argument from the filter tag (filter.provider()) instead of passing them independently
- Reset the filter form in the UI whenever the provider selection changes
- Validate the pair at the RPC boundary and return a field-level error naming both providers
When it happens
Trigger: add_source(config, ProviderSlug::Github, ...) called with FilterSpec::Notion{..} (or any cross pair). Typically happens when the provider is chosen independently in the UI while the filter JSON was deserialized from a stale form or a different tab's picker.
Common situations: Frontend sends {provider:'linear', filter:{provider:'github',...}} after the user switched provider without resetting the filter; copy-pasting a source template and editing only the provider; hand-built RPC payloads.
Related errors
- patch filter provider '{}' does not match source provider '{
- mascot manifest: missing mascots array
- mascot manifest: no renderable mascots
- audio blob is empty
- audio blob is empty
AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17).
Data as JSON: /api/errors/3d7e8ced8928c8c0.
Report an issue: GitHub.