{"record":{"id":"87299132202edb7f","repo":"laurent22/joplin","slug":"no-published-share-for-folder-folderid","errorCode":null,"errorMessage":"No published share for folder: ${folderId}","messagePattern":"No published share for folder: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/lib/services/share/ShareService.ts","lineNumber":330,"sourceCode":"\t\tconst folder = await Folder.load(folderId);\n\t\tif (!folder) throw new Error(`No such folder: ${folderId}`);\n\n\t\tconst share = await this.api().exec('POST', 'api/shares', {}, {\n\t\t\tfolder_id: folderId,\n\t\t\ttype: ShareType.PublishedFolder,\n\t\t});\n\n\t\tawait this.refreshShares();\n\n\t\treturn share;\n\t}\n\n\tpublic async unpublishFolder(folderId: string): Promise<void> {\n\t\tconst folder = await Folder.load(folderId);\n\t\tif (!folder) throw new Error(`No such folder: ${folderId}`);\n\n\t\tconst share = this.shares.find(s => s.type === ShareType.PublishedFolder && s.folder_id === folderId);\n\t\tif (!share) throw new Error(`No published share for folder: ${folderId}`);\n\n\t\tconst remainingShares = this.shares.filter(s => s.id !== share.id);\n\t\tconst folderIds = [folderId, ...(await Folder.allChildrenFolders(folderId)).map(f => f.id)];\n\t\tconst folders = await Folder.loadItemsByIds(folderIds) as FolderEntity[];\n\t\tconst noteIds = (await Promise.all(folderIds.map(id => Folder.noteIds(id, { includeConflicts: true, includeDeleted: true })))).flat();\n\t\tconst notes = await Note.loadItemsByIds(noteIds) as NoteEntity[];\n\t\tconst directlyPublishedNoteIds = new Set(remainingShares\n\t\t\t.filter(s => s.type === ShareType.Note && !!s.note_id)\n\t\t\t.map(s => s.note_id));\n\n\t\tfor (const folderItem of folders) {\n\t\t\tawait Folder.updateShareStatus({ ...folderItem, type_: ModelType.Folder }, false);\n\t\t}\n\n\t\tfor (const note of notes) {\n\t\t\tawait Note.updateShareStatus({ ...note, type_: ModelType.Note }, directlyPublishedNoteIds.has(note.id));\n\t\t}\n","sourceCodeStart":312,"sourceCodeEnd":348,"githubUrl":"https://github.com/laurent22/joplin/blob/dc4e0b464e43f63851261b0f72fb59d301fc99a7/packages/lib/services/share/ShareService.ts#L312-L348","documentation":"Thrown by ShareService.unpublishFolder() when the in-memory share list (this.shares, backed by this.state.shares) contains no share with type ShareType.PublishedFolder whose folder_id matches. The shares list is a local cache populated from GET api/shares via refreshShares(); the method does not re-fetch before checking, so the guard fires both when the folder was never published and when the cache is out of date.","triggerScenarios":"Calling unpublishFolder on a folder that was shared as a normal notebook (ShareType.Folder) instead of published (ShareType.PublishedFolder); calling unpublish twice in a row (second call finds no share); calling it right after app startup or after the share was created/deleted on another device, before refreshShares() has re-synced this.shares.","commonSituations":"Double-clicking an 'unpublish' button or re-running a batch script so the operation executes twice; mixing up the share types (Folder vs PublishedFolder vs Note) when building automation; shares state stale after a publish/unpublish from the web clipper or another client on the same account.","solutions":["Call await shareService.refreshShares() immediately before unpublishFolder so this.shares reflects the server.","Treat 'no published share' as a no-op: check shares.some(s => s.type === ShareType.PublishedFolder && s.folder_id === folderId) first and skip if false.","Confirm the folder was published via publishFolder() (PublishedFolder share), not merely shared as a notebook (Folder share) — the latter needs a different code path.","Guard the UI/action against double invocation (disable the button while the request is in flight)."],"exampleFix":"// before\nawait shareService.unpublishFolder(folderId);\n\n// after\nawait shareService.refreshShares();\nconst isPublished = shareService.shares.some(\n\ts => s.type === ShareType.PublishedFolder && s.folder_id === folderId,\n);\nif (isPublished) await shareService.unpublishFolder(folderId);","handlingStrategy":"validation","validationCode":"import { ShareType } from '@joplin/lib/services/share/ShareService';\n\nawait shareService.refreshShares();\nconst isPublished = shareService.shares.some(\n\ts => s.type === ShareType.PublishedFolder && s.folder_id === folderId,\n);\nif (!isPublished) return; // idempotent no-op instead of a thrown error","typeGuard":"const hasPublishedShare = (shares: StateShare[], folderId: string): boolean =>\n\tshares.some(s => s.type === ShareType.PublishedFolder && s.folder_id === folderId);","tryCatchPattern":"try {\n\tawait shareService.unpublishFolder(folderId);\n} catch (error) {\n\tif (error instanceof Error && error.message.startsWith('No published share for folder:')) {\n\t\tawait shareService.refreshShares(); // state may be stale; retry once after refresh\n\t\tawait shareService.unpublishFolder(folderId);\n\t} else {\n\t\tthrow error;\n\t}\n}","preventionTips":["Call refreshShares() right before checking/using this.shares — the in-memory list is only as fresh as the last refresh.","Make unpublish idempotent in the UI: disable the control while the request runs to prevent double invocation.","Distinguish share types up front (PublishedFolder vs Folder vs Note) so the right unshare path is used."],"tags":["joplin","sharing","unpublish","stale-state","idempotency"],"backgroundTag":"stale-local-state","analyzedSha":"dc4e0b464e43f63851261b0f72fb59d301fc99a7","analyzedAt":"2026-08-21T12:05:35.031Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}