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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/01f9f7b362733771. Report an issue: GitHub.