mihomo-party-org/clash-party · error

Profile "${id}" contains HTML instead of YAML. The subscript

Error message

Profile "${id}" contains HTML instead of YAML. The subscription may have returned an error page. Please re-import or update the subscription.

What it means

parseProfileContent detects when a stored profile's content is actually HTML (DOCTYPE, <html> tags, or inline <style> in the first 500 chars) rather than YAML. This indicates the subscription URL returned an error page (login, captcha, 4xx/5xx HTML) that got saved as the profile, so parsing is aborted with a descriptive message telling the user to re-import.

Source

Thrown at src/main/config/profile.ts:620

  return await parseProfileContent(id, await getProfileStr(id), item?.ageSecretKey)
}

export async function parseProfileContent(
  id: string | undefined,
  content: string,
  ageSecretKey?: string
): Promise<IMihomoConfig> {
  const profile = await decryptAgeContent(content, ageSecretKey, `profile "${id || 'default'}"`)

  // 检测是否为 HTML 内容(订阅返回错误页面)
  const trimmed = profile.trim()
  if (
    trimmed.startsWith('<!DOCTYPE') ||
    trimmed.startsWith('<html') ||
    trimmed.startsWith('<HTML') ||
    /<style[^>]*>/i.test(trimmed.slice(0, 500))
  ) {
    throw new Error(
      `Profile "${id}" contains HTML instead of YAML. The subscription may have returned an error page. Please re-import or update the subscription.`
    )
  }

  try {
    let result = parse(profile)
    if (typeof result !== 'object') result = {}
    return result as IMihomoConfig
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e)
    throw new Error(`Failed to parse profile "${id}": ${msg}`)
  }
}

// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
function parseFilename(str: string): string {
  if (str.match(/filename\*=.*''/)) {
    const parts = str.split(/filename\*=.*''/)

View on GitHub (pinned to 911e090537)

Solutions

  1. Re-fetch the subscription: open the URL and confirm it returns YAML, then re-import or update the profile.
  2. Check subscription authentication (token/cookie) — renew the expired link from the provider.
  3. If behind a captive portal, complete portal login and retry the import.
  4. Delete the corrupted profile entry and add it again from the correct URL.

Example fix

// before
await getProfile(id) // stored content is '<!DOCTYPE html>...'
// after
await fetchSub(correctUrl, options) // re-fetch valid YAML, then load the profile
Defensive patterns

Strategy: try-catch

Validate before calling

const trimmed = content.trim().slice(0, 500)
const looksHtml = /^<!DOCTYPE|^<html/i.test(trimmed) || /<style[^>]*>/i.test(trimmed)
if (looksHtml) {
  // re-fetch the subscription before calling getProfile
  await reImportSubscription(profileUrl)
}

Type guard

function looksLikeHtml(content: string): boolean {
  const t = content.trim()
  return t.startsWith('<!DOCTYPE') || /^<html/i.test(t) || /<style[^>]*>/i.test(t.slice(0, 500))
}

Try / catch

try {
  const cfg = await getProfile(id)
} catch (e) {
  if (e.message.includes('contains HTML instead of YAML')) {
    // subscription returned an error page: renew token and re-import
  }
}

Prevention

When it happens

Trigger: baseProfile or getProfile reads a profile whose content starts with '<!DOCTYPE', '<html', '<HTML', or contains a <style> tag within the first 500 characters.

Common situations: Subscription token expired and provider served a login page; captive portal or ISP interception page saved during import; provider rate-limited with an HTML error page; user pasted an HTML URL by mistake.

Related errors


AI-assisted analysis of mihomo-party-org/clash-party@911e090537 (2026-08-30). Data as JSON: /api/errors/49f3637ee1ca10e1. Report an issue: GitHub.