{"record":{"id":"4079664528372bce","repo":"mihomo-party-org/clash-party","slug":"profile-is-not-a-yaml-mapping","errorCode":null,"errorMessage":"Profile is not a YAML mapping","messagePattern":"Profile is not a YAML mapping","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/renderer/src/components/profiles/edit-tunnels-modal.tsx","lineNumber":223,"sourceCode":"\n  const targetInvalid = useMemo(() => {\n    if (!newTunnel.target.trim()) return false\n    return !isValidTarget(newTunnel.target)\n  }, [newTunnel.target])\n\n  const canSubmit = useMemo(() => isTunnelValid(newTunnel), [newTunnel])\n\n  useEffect(() => {\n    const loadContent = async (): Promise<void> => {\n      setIsLoading(true)\n      setLoadFailed(false)\n      try {\n        const content = await getProfileStr(id)\n        const parsed = load(content)\n        // 解析结果不是 YAML mapping 时（例如 age 密文、根数组或日期），\n        // 旧实现静默退化成 {} 且不提示，保存就会把整份订阅写没。\n        if (Object.prototype.toString.call(parsed) !== '[object Object]') {\n          throw new Error('Profile is not a YAML mapping')\n        }\n        const nextProfile = parsed as ProfileYaml\n        setProfile(nextProfile)\n        setTunnels(parseTunnels(nextProfile.tunnels))\n        setProxyNames(collectProxyNames(nextProfile))\n      } catch (e) {\n        setLoadFailed(true)\n        toast.error(\n          t('profiles.editTunnels.loadError') + ': ' + (e instanceof Error ? e.message : String(e))\n        )\n      } finally {\n        setIsLoading(false)\n      }\n    }\n\n    loadContent()\n  }, [id, t])\n","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/mihomo-party-org/clash-party/blob/911e090537acdf7c50bee1c3aebecc2ef119a8b5/src/renderer/src/components/profiles/edit-tunnels-modal.tsx#L205-L241","documentation":"EditTunnelsModal's loadContent() fetches the profile text via getProfileStr(id), parses it with YAML load(), and throws 'Profile is not a YAML mapping' when the parsed root is not a plain object (e.g. an array, scalar, date, or age-encrypted ciphertext). The guard exists because the old code silently coerced non-mapping roots to {}, and saving would then overwrite the whole subscription with an empty profile.","triggerScenarios":"Opening the tunnels editor for a profile whose stored content is an age-encrypted blob, a bare YAML list at the root, a scalar, or a date — anything whose Object.prototype.toString class is not '[object Object]'.","commonSituations":"Subscribing to a URL that returns encrypted (age) content the app cannot decrypt; a hand-edited profile file starting with '- ' (list) or a plain string; a subscription endpoint returning an error page that parses as a scalar YAML.","solutions":["Fix the profile source: re-download the subscription or point it at a URL returning a proper YAML mapping (top-level keys like proxies/proxy-groups).","If the profile is age-encrypted, decrypt or import it through the encrypted-profile flow before editing tunnels.","Hand-edit the profile file so the root is a mapping (key: value pairs at top level).","Keep the thrown error visible to the user and abort saving (the current catch already prevents overwriting the subscription)."],"exampleFix":"// before (old silent behavior)\nconst nextProfile = (Object.prototype.toString.call(parsed) === '[object Object]' ? parsed : {}) as ProfileYaml\n// after\nif (Object.prototype.toString.call(parsed) !== '[object Object]') {\n  throw new Error('Profile is not a YAML mapping')\n}\nconst nextProfile = parsed as ProfileYaml","handlingStrategy":"validation","validationCode":"import { load } from 'yaml'\nconst parsed = load(content)\nif (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed) || parsed instanceof Date) {\n  throw new Error('Profile is not a YAML mapping')\n}","typeGuard":"function isYamlMapping(v: unknown): v is Record<string, unknown> {\n  return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date)\n}","tryCatchPattern":"try {\n  const content = await getProfileStr(id)\n  const parsed = load(content)\n  if (!isYamlMapping(parsed)) throw new Error('Profile is not a YAML mapping')\n} catch (e) {\n  if (e.message === 'Profile is not a YAML mapping') {\n    // show UI message; never write back to this profile\n  } else {\n    throw e\n  }\n}","preventionTips":["Never silently default a parsed profile to {} before saving.","Validate subscription URLs return YAML mappings (root-level keys) after download.","Detect age-encrypted/undecryptable content before opening editors that save back.","Back up the original profile file before any editor writes to it."],"tags":["yaml","validation","profiles","data-integrity"],"backgroundTag":"yaml-root-not-a-mapping","analyzedSha":"911e090537acdf7c50bee1c3aebecc2ef119a8b5","analyzedAt":"2026-08-30T13:00:49.174Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}