toeverything/AFFiNE · error · InvalidArg
ech_config_required
Error message
ech_config_required
What it means
Bailed in ech_config_list() (safe_fetch.rs:251) when the SafeFetchRequest has enable_ech set to a truthy value but ech_config_list is None. ECH (Encrypted Client Hello) is the TLS ESNI/ECH extension that hides the SNI from network observers; the config_list is the raw ECHConfigList bytes obtained from a DNS HTTPS/HTTPSRR record. The bail message is surfaced to the JS caller as a napi Status::InvalidArg error (via invalid_arg at safe_fetch.rs:124/256), because safe_fetch_request() propagates it through map_err(invalid_arg).
Source
Thrown at packages/backend/native/src/safe_fetch.rs:251
allow_private_target_origin: request.allow_private_target_origin,
ech_config_list: ech_config_list(request)?,
})
}
fn image_inspection_options(options: ImageInspectionOptions) -> safefetch::ImageInspectionOptions {
safefetch::ImageInspectionOptions {
max_width: options.max_width,
max_height: options.max_height,
max_pixels: options.max_pixels,
}
}
fn ech_config_list(request: &SafeFetchRequest) -> anyhow::Result<Option<Vec<u8>>> {
if !request.enable_ech.unwrap_or(false) {
return Ok(None);
}
let Some(config_list) = request.ech_config_list.as_ref() else {
anyhow::bail!("ech_config_required");
};
Ok(Some(config_list.to_vec()))
}
fn invalid_arg(error: impl ToString) -> Error {
Error::new(Status::InvalidArg, error.to_string())
}
View on GitHub (pinned to 26c515e050)
Solutions
- Provide both fields together: set ech_config_list to the ECHConfigList bytes obtained from safefetch::ech::cloudflare_https_ech_config_list(host, timeout), exactly as license.rs:464-480 does.
- If you do not have an ECH config and do not strictly need ECH, set enable_ech to false (or omit it — it defaults to false via unwrap_or(false)) and the bail is skipped.
- Cache the ECH config (license.rs uses a OnceLock<Mutex<Option<Vec<u8>>>>) so repeated fetches do not re-query DNS and so a transient DNS failure does not leave you with None.
- If ECH DNS retrieval is failing, check network egress to the resolver and raise ECH_DNS_QUERY_TIMEOUT_MS; fall back to disable_ech rather than crashing the whole fetch.
Example fix
// before (JS caller via napi)
const resp = await safeFetch({
url: 'https://pro.affine.ai',
method: SafeFetchMethod.Get,
enable_ech: true,
// ech_config_list missing
});
// after — either disable ECH, or supply the config
// option A: disable ECH
const resp = await safeFetch({ url: 'https://pro.affine.ai', method: SafeFetchMethod.Get });
// option B: supply config (fetch it from DNS HTTPS record first)
const resp = await safeFetch({
url: 'https://pro.affine.ai',
method: SafeFetchMethod.Get,
enable_ech: true,
ech_config_list: Buffer.from(echConfigBytes),
}); Defensive patterns
Strategy: validation
Validate before calling
// JS-side guard before calling the napi safeFetch export.
function assertEchConsistent(req) {
if (req.enable_ech) {
if (!req.ech_config_list || req.ech_config_list.length === 0) {
throw new TypeError('ech_config_list is required when enable_ech is true');
}
}
return req;
}
// Rust-side guard (if authoring a new caller in native code):
// fn validate(req: &SafeFetchRequest) -> Result<()> {
// if req.enable_ech.unwrap_or(false) && req.ech_config_list.is_none() {
// return Err(Error::new(Status::InvalidArg, "ech_config_list required when enable_ech is true"));
// }
// Ok(())
// } Type guard
function isValidEchRequest(req: unknown): req is { enable_ech: true; ech_config_list: Buffer } | { enable_ech?: false } {
if (typeof req !== 'object' || req === null) return false;
const r = req as { enable_ech?: unknown; ech_config_list?: unknown };
const echOn = r.enable_ech === true;
const hasConfig = Buffer.isBuffer(r.ech_config_list) && (r.ech_config_list as Buffer).length > 0;
return !echOn || hasConfig;
} Try / catch
try {
await safeFetch(request);
} catch (err) {
if (err?.code === 'InvalidArg' && /ech_config_required/.test(err.message)) {
// Either disable ECH for this request or fetch and attach the config.
await safeFetch({ ...request, enable_ech: false });
} else {
throw err;
}
} Prevention
- Never set enable_ech without also setting ech_config_list; treat them as a single coupled option.
- Fetch the ECH config once via safefetch::ech::cloudflare_https_ech_config_list and cache it (mirror the OnceLock pattern in license.rs) so you always have bytes to attach.
- If the DNS-based config fetch is unreliable in your environment, default enable_ech to false rather than crashing the whole request.
- Add a unit test asserting that safe_fetch_request rejects enable_ech=true with no config list.
When it happens
Trigger: Calling safe_fetch() (the napi export) with { enable_ech: true } but omitting ech_config_list, or passing it as null/undefined. The ech_config_list function only short-circuits to Ok(None) when enable_ech is falsy or absent; once ECH is requested the config buffer is mandatory. The canonical caller (license.rs:383) always pairs them: it fetches the config via safefetch::ech::cloudflare_https_ech_config_list() and passes it as Some(...).
Common situations: A new caller enabling ECH for privacy but forgetting that ECH config is not auto-discovered by the fetcher — it must be supplied by the caller; copy-pasting a SafeFetchRequest where enable_ech was flipped to true during a security review without also wiring the config fetch; DNS retrieval of the ECH config failing upstream (license.rs:473) so a caller that assumes affine_pro_ech_config() will succeed gets None; version skew where an older caller expected the native module to fetch the config itself.
Related errors
- Failed to read image size
- ErrorCode.DefaultRuntimeError
- Invalid key for: ${key}
- SchemaValidateError
- bad_request
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/01f9f7b362733771.
Report an issue: GitHub.