{"record":{"id":"01f9f7b362733771","repo":"toeverything/AFFiNE","slug":"ech-config-required","errorCode":null,"errorMessage":"ech_config_required","messagePattern":"ech_config_required","errorType":"validation","errorClass":"InvalidArg","httpStatus":null,"severity":"error","filePath":"packages/backend/native/src/safe_fetch.rs","lineNumber":251,"sourceCode":"    allow_private_target_origin: request.allow_private_target_origin,\n    ech_config_list: ech_config_list(request)?,\n  })\n}\n\nfn image_inspection_options(options: ImageInspectionOptions) -> safefetch::ImageInspectionOptions {\n  safefetch::ImageInspectionOptions {\n    max_width: options.max_width,\n    max_height: options.max_height,\n    max_pixels: options.max_pixels,\n  }\n}\n\nfn ech_config_list(request: &SafeFetchRequest) -> anyhow::Result<Option<Vec<u8>>> {\n  if !request.enable_ech.unwrap_or(false) {\n    return Ok(None);\n  }\n  let Some(config_list) = request.ech_config_list.as_ref() else {\n    anyhow::bail!(\"ech_config_required\");\n  };\n  Ok(Some(config_list.to_vec()))\n}\n\nfn invalid_arg(error: impl ToString) -> Error {\n  Error::new(Status::InvalidArg, error.to_string())\n}\n","sourceCodeStart":233,"sourceCodeEnd":259,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/native/src/safe_fetch.rs#L233-L259","documentation":"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).","triggerScenarios":"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(...).","commonSituations":"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.","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."],"exampleFix":"// before (JS caller via napi)\nconst resp = await safeFetch({\n  url: 'https://pro.affine.ai',\n  method: SafeFetchMethod.Get,\n  enable_ech: true,\n  // ech_config_list missing\n});\n\n// after — either disable ECH, or supply the config\n// option A: disable ECH\nconst resp = await safeFetch({ url: 'https://pro.affine.ai', method: SafeFetchMethod.Get });\n// option B: supply config (fetch it from DNS HTTPS record first)\nconst resp = await safeFetch({\n  url: 'https://pro.affine.ai',\n  method: SafeFetchMethod.Get,\n  enable_ech: true,\n  ech_config_list: Buffer.from(echConfigBytes),\n});","handlingStrategy":"validation","validationCode":"// JS-side guard before calling the napi safeFetch export.\nfunction assertEchConsistent(req) {\n  if (req.enable_ech) {\n    if (!req.ech_config_list || req.ech_config_list.length === 0) {\n      throw new TypeError('ech_config_list is required when enable_ech is true');\n    }\n  }\n  return req;\n}\n\n// Rust-side guard (if authoring a new caller in native code):\n// fn validate(req: &SafeFetchRequest) -> Result<()> {\n//   if req.enable_ech.unwrap_or(false) && req.ech_config_list.is_none() {\n//     return Err(Error::new(Status::InvalidArg, \"ech_config_list required when enable_ech is true\"));\n//   }\n//   Ok(())\n// }","typeGuard":"function isValidEchRequest(req: unknown): req is { enable_ech: true; ech_config_list: Buffer } | { enable_ech?: false } {\n  if (typeof req !== 'object' || req === null) return false;\n  const r = req as { enable_ech?: unknown; ech_config_list?: unknown };\n  const echOn = r.enable_ech === true;\n  const hasConfig = Buffer.isBuffer(r.ech_config_list) && (r.ech_config_list as Buffer).length > 0;\n  return !echOn || hasConfig;\n}","tryCatchPattern":"try {\n  await safeFetch(request);\n} catch (err) {\n  if (err?.code === 'InvalidArg' && /ech_config_required/.test(err.message)) {\n    // Either disable ECH for this request or fetch and attach the config.\n    await safeFetch({ ...request, enable_ech: false });\n  } else {\n    throw err;\n  }\n}","preventionTips":["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."],"tags":["rust","napi","tls","ech","safe-fetch","native","validation","network"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}