{"record":{"id":"8801a54fe3b27b2d","repo":"siyuan-note/siyuan","slug":"send-article-to-liandi-failed-sc-d","errorCode":null,"errorMessage":"send article to liandi failed [sc=%d]","messagePattern":"send article to liandi failed \\[sc=(.+?)\\]","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/export.go","lineNumber":477,"sourceCode":"\t\t\tSetBody(map[string]any{\n\t\t\t\t\"articleTitle\":   title,\n\t\t\t\t\"articleTags\":    tags,\n\t\t\t\t\"articleContent\": content})\n\t\tvar resp *req.Response\n\t\tvar sendErr error\n\t\tif foundArticle {\n\t\t\tresp, sendErr = request.Put(apiURL)\n\t\t} else {\n\t\t\tresp, sendErr = request.Post(apiURL)\n\t\t}\n\t\tif nil != sendErr {\n\t\t\tlogging.LogErrorf(\"send article to liandi failed: %s\", sendErr)\n\t\t\treturn sendErr\n\t\t}\n\t\tif 200 != resp.StatusCode {\n\t\t\tmsg := fmt.Sprintf(\"send article to liandi failed [sc=%d]\", resp.StatusCode)\n\t\t\tlogging.LogError(msg)\n\t\t\treturn errors.New(msg)\n\t\t}\n\n\t\tif 0 != result.Code {\n\t\t\tmsg := fmt.Sprintf(\"send article to liandi failed [code=%d, msg=%s]\", result.Code, result.Msg)\n\t\t\tlogging.LogError(msg)\n\t\t\tutil.PushClearMsg(msgId)\n\t\t\treturn errors.New(result.Msg)\n\t\t}\n\n\t\tif !foundArticle {\n\t\t\tarticleId = result.Data.(string)\n\t\t\ttree, _ = LoadTreeByBlockID(id) // 这里必须重新加载，因为前面导出时已经修改了树结构\n\t\t\ttree.Root.SetIALAttr(liandiArticleIdAttrName, articleId)\n\t\t\tif err = writeTreeUpsertQueue(tree); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n","sourceCodeStart":459,"sourceCodeEnd":495,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/export.go#L459-L495","documentation":"Raised inside Export2Liandi when the liandi (community forum) server returns an HTTP status code other than 200 for the POST (new article) or PUT (update article) request to /api/v2/article. The kernel authenticates with the user's symphony cookie (UserToken) and uploads the rendered Markdown plus assets, so a non-200 response means the forum rejected the transport-level request (auth, rate-limit, server fault). The status code is interpolated into the message so the developer can distinguish 401/403/404/429/5xx.","triggerScenarios":"Calling the export-to-liandi API endpoint (POST /api/export/export2Liandi with {id}) when the user's cloud account token is expired/invalid (401/403), the forum is rate-limiting (429), the server is down (5xx), or the network proxy mangles the request. Also triggered if the article was deleted server-side between the GET check and the PUT update.","commonSituations":"The user changed their ld246.com/liuyun.io password but did not re-login in Settings - About - Sync/Cloud, leaving UserToken stale. Corporate firewalls returning a captive-portal 200/302 body that is not valid JSON, or returning 502 for the forum host. Rate-limiting after sending many articles rapidly.","solutions":["Verify the cloud account is logged in and the token is fresh: Settings - About - Cloud account, sign out and back in, then retry.","Check the kernel log for the exact status code — 401/403 means re-authenticate, 429 means wait and retry, 5xx means the forum is down.","If behind a proxy, ensure liandi/forum domains are reachable and not intercepted by a captive portal.","For a stale article-id reference (the doc was deleted on the forum), remove the custom-liandi-articleid IAL attribute from the document root so the next send creates a new article instead of PUT-updating a vanished one."],"exampleFix":"// before — stale articleId forces a PUT against a deleted article\ntree.Root.SetIALAttr(\"custom-liandi-articleid\", \"\")\n// after — clears the stale id so Export2Liandi falls back to POST (create new)\n// remove the attribute entirely via the doc's attribute panel, or in code:\ntree.Root.RemoveIALAttr(\"custom-liandi-articleid\")","handlingStrategy":"validation","validationCode":"// Validate auth and reachability before calling Export2Liandi\nuser := Conf.GetUser()\nif user == nil || user.UserToken == \"\" {\n    return errors.New(\"cloud account not logged in\")\n}\n// optional: probe the forum endpoint to detect 401/429/5xx early\nprobe := httpclient.NewCloudRequest30s().SetSuccessResult(gulu.Ret.NewResult())\nif resp, err := probe.Get(util.GetCloudAccountServer() + \"/api/v2/article\"); err != nil || resp.StatusCode >= 400 {\n    return fmt.Errorf(\"liandi unreachable [sc=%v]\", statusOf(resp))\n}","typeGuard":null,"tryCatchPattern":"// Export2Liandi returns a plain error; inspect its text for [sc=%d] to branch on transport vs business\nif err := model.Export2Liandi(id); err != nil {\n    if strings.Contains(err.Error(), \"[sc=\") {\n        // transport-level (non-200) — re-auth or retry with backoff\n    } else {\n        // surface the server/business message to the user\n    }\n}","preventionTips":["Keep the cloud-account token fresh: re-login whenever the forum password changes.","Before bulk-sending, probe the forum endpoint to confirm 200 reachability.","Clear stale custom-liandi-articleid attributes when an article was deleted server-side.","Rate-limit successive Export2Liandi calls to avoid 429."],"tags":["network","liandi","export","authentication","http-status"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}