{"record":{"id":"10f28722e8dd9088","repo":"different-ai/openwork","slug":"failed-to-save-skill-response-status","errorCode":null,"errorMessage":"Failed to save skill (${response.status}).","messagePattern":"Failed to save skill \\((.+?)\\)\\.","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx","lineNumber":162,"sourceCode":"\nexport function useUpdateSkill(pluginId: string) {\n  const queryClient = useQueryClient();\n\n  return useMutation({\n    mutationFn: async (input: { skillId: string; draft: SkillDraft }): Promise<DenSkill> => {\n      const { response, payload } = await requestJson(\n        `/v1/config-objects/${encodeURIComponent(input.skillId)}/versions`,\n        {\n          method: \"POST\",\n          body: JSON.stringify({\n            input: { rawSourceText: skillSourceFromDraft(input.draft) },\n            reason: \"Updated from Den Web\",\n          }),\n        },\n        15000,\n      );\n      if (!response.ok) {\n        throw getRequestError(payload, response, `Failed to save skill (${response.status}).`);\n      }\n      const skill = parseSkillResponse(payload);\n      if (!skill) {\n        throw new Error(\"Skill update response was incomplete.\");\n      }\n      return skill;\n    },\n    onSuccess: async () => {\n      await Promise.all([\n        queryClient.invalidateQueries({ queryKey: skillQueryKeys.all }),\n        queryClient.invalidateQueries({ queryKey: pluginQueryKeys.detail(pluginId) }),\n      ]);\n    },\n  });\n}\n\nexport function useDeleteSkill(pluginId: string) {\n  const queryClient = useQueryClient();","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/ee/apps/den-web/app/(den)/dashboard/_components/skill-data.tsx#L144-L180","documentation":"Thrown by useUpdateSkill when POST /v1/config-objects/{skillId}/versions returns a non-ok status. This endpoint creates a new skill version with the composed rawSourceText; the error carries the server's message via getRequestError (and becomes ReauthRequiredError for 403 reauth). It signals the server rejected the version update — the local draft was never persisted.","triggerScenarios":"POST /v1/config-objects/{skillId}/versions with {input:{rawSourceText}, reason:'Updated from Den Web'} returns 400 (invalid skill markdown in the edited draft), 401 (session expired), 403 (caller lacks write access to the skill/org, or reauth challenge), 404 (skillId no longer exists — e.g. deleted in another tab), 409 (version conflict/stale edit), 429, or 5xx. 15s timeout applies.","commonSituations":"Editing a skill whose underlying config object was deleted or re-created by a teammate; org role downgraded from admin to member mid-session; pasting oversized skill body content; stale open editor after long idle causing token expiry.","solutions":["Inspect the appended server message in the thrown error for the concrete cause.","Refresh the skill detail query; if 404, the skill is gone — reload the list instead of retrying the save.","If ReauthRequiredError, wrap the save in runReauthableAction or re-authenticate the user.","Validate the draft (non-empty name/body, reasonable size) before calling mutateAsync.","For concurrent-edit 409s, re-fetch latest version, reapply the edit, and save again.","For 429/5xx retry with backoff."],"exampleFix":"// before: blind save\nawait updateSkill.mutateAsync({ skillId, draft });\n// after: revalidate then save, handling reauth\nconst latest = await queryClient.fetchQuery(skillQueryKeys.detail(orgId, pluginId, skillId));\nif (latest.updatedAt !== loadedAt) throw new Error(\"Skill changed elsewhere; reload before saving.\");\ntry {\n  await runReauthableAction(\"save-skill\", () => updateSkill.mutateAsync({ skillId, draft }));\n} catch (err) {\n  if (!isReauthRequiredError(err)) throw err;\n}","handlingStrategy":"try-catch","validationCode":"const skill = skillSourceFromDraft(draft);\nif (!draft.name.trim()) throw new Error(\"Skill name is required.\");\nif (skill.length > 512 * 1024) throw new Error(\"Skill source exceeds size limit.\");","typeGuard":"function isReauthRequiredError(e: unknown): e is ReauthRequiredError {\n  return e instanceof ReauthRequiredError;\n}","tryCatchPattern":"try {\n  await updateSkill.mutateAsync({ skillId, draft });\n} catch (err) {\n  if (isReauthRequiredError(err)) { promptSignIn(); return; }\n  if (/\\b404\\b/.test(err.message)) { reloadSkillList(); return; } // deleted elsewhere\n  showError(err.message);\n}","preventionTips":["Re-fetch the skill detail before saving to detect concurrent modifications.","Wrap saves in runReauthableAction to survive 403 reauth challenges.","Validate the draft (name, size) client-side before POSTing a new version.","Treat 404 in the error message as 'skill deleted elsewhere' and refresh instead of retrying.","Keep the editor session short or re-authenticate on idle to avoid expired-token failures."],"tags":["http","api","skills","den-web"],"backgroundTag":"http-request-failed","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}