{"record":{"id":"1a8cc09e1d4c0852","repo":"laurent22/joplin","slug":"invalid-response-shares-list-is-not-an-array-was","errorCode":null,"errorMessage":"Invalid response: Shares list is not an array. Was ${typeof shares?.items}.","messagePattern":"Invalid response: Shares list is not an array\\. Was (.+?)\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/lib/services/share/ShareService.ts","lineNumber":474,"sourceCode":"\t\t))) {\n\t\t\treturn true;\n\t\t}\n\n\t\t// In some cases, an item can have is_shared = 1, but no share in `shares`. In this case,\n\t\t// either the item has been unpublished remotely and not yet synced, or the item was published\n\t\t// by another user. Send a network request to determine whether the item is actually published:\n\t\tif (item.is_shared) {\n\t\t\tconst shares = (await this.loadSharesByItem(item.id))\n\t\t\t\t.filter(isPublishedItemShare);\n\t\t\treturn shares.length > 0;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\tprivate async loadSharesByItem(itemId: string) {\n\t\tconst shares = await this.api().exec('GET', 'api/shares', { item: itemId });\n\t\tif (!Array.isArray(shares?.items)) throw new Error(`Invalid response: Shares list is not an array. Was ${typeof shares?.items}.`);\n\n\t\tconst items: StateShare[] = shares.items.filter(\n\t\t\t// For compatibility with older server versions that don't support search\n\t\t\t(i: StateShare) => (\n\t\t\t\t(i.type === ShareType.Note && i.note_id === itemId)\n\t\t\t\t|| (i.type === ShareType.PublishedFolder && i.folder_id === itemId)\n\t\t\t\t|| (i.type === ShareType.Folder && i.folder_id === itemId)\n\t\t\t),\n\t\t);\n\t\treturn items;\n\t}\n\n\tprivate async loadShares() {\n\t\treturn this.api().exec('GET', 'api/shares');\n\t}\n\n\tprivate async loadShareUsers(shareId: string) {\n\t\treturn this.api().exec('GET', `api/shares/${shareId}/users`);","sourceCodeStart":456,"sourceCodeEnd":492,"githubUrl":"https://github.com/laurent22/joplin/blob/dc4e0b464e43f63851261b0f72fb59d301fc99a7/packages/lib/services/share/ShareService.ts#L456-L492","documentation":"Thrown by ShareService.loadSharesByItem() when the response to GET api/shares?item=<id> does not contain an array under .items. The client expects the Joplin Server paginated shape { items: StateShare[] }; if items is undefined, an object, or a string (typeof is interpolated into the message), the response came from an incompatible backend, a proxy, or an error page rather than a real shares listing. Note the code already tolerates older servers that ignore the item query param by filtering client-side afterwards.","triggerScenarios":"Pointing api() at a Joplin Server older than the version that supports the item search param (or a non-Joplin backend) so the payload shape differs; an auth failure or rate-limit page returned as HTML/JSON without an items field; a reverse proxy or gateway rewriting the response; a server bug or partial deployment returning an error object where the client expects the list envelope.","commonSituations":"Self-hosted Joplin Server behind nginx/Cloudflare that intercepts errors and returns a branded error body; server upgraded or downgraded out of step with the client; custom API wrappers that return the Axios response object (so .data.items exists but .items does not); development against a mock server that omits the items wrapper.","solutions":["Log the full response payload when the guard fires to identify what the server actually returned (HTML error page, error JSON, empty body).","Verify the API base URL targets a compatible Joplin Server instance and that the account has access (a 4xx body often reaches this check when errors are swallowed upstream).","Upgrade (or align) the Joplin Server version so GET api/shares supports the item param and returns { items: [...] }.","If wrapping the API yourself, unwrap resp.data before returning so callers see the server envelope, not the transport object."],"exampleFix":"// before\nconst shares = await this.api().exec('GET', 'api/shares', { item: itemId });\nreturn shares.items.filter(matchItem);\n\n// after\nconst resp = await this.api().exec('GET', 'api/shares', { item: itemId });\nif (!Array.isArray(resp?.items)) {\n\tthrow new Error(`Invalid response: Shares list is not an array. Was ${typeof resp?.items}. Payload: ${JSON.stringify(resp)?.slice(0, 200)}`);\n}\nreturn resp.items.filter(matchItem);","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"interface SharesListResponse { items: StateShare[] }\n\nconst isSharesListResponse = (r: unknown): r is SharesListResponse =>\n\t!!r && typeof r === 'object' && Array.isArray((r as SharesListResponse).items);","tryCatchPattern":"try {\n\tawait shareService.isPublished(note, knownShares);\n} catch (error) {\n\tif (error instanceof Error && error.message.startsWith('Invalid response: Shares list is not an array')) {\n\t\t// Backend/proxy mismatch: log the payload, verify server version and API base URL\n\t} else {\n\t\tthrow error;\n\t}\n}","preventionTips":["Pin clients to a Joplin Server version compatible with the shares API (GET api/shares returning { items: [...] }).","Ensure reverse proxies pass API errors through as real HTTP status codes instead of HTML error pages.","When wrapping the API, return the parsed body (resp.data), never the Axios response object.","Log the raw payload alongside the error to classify server-vs-proxy-vs-wrapper causes quickly."],"tags":["joplin","api","response-validation","server-compatibility","shares"],"backgroundTag":"api-response-schema-mismatch","analyzedSha":"dc4e0b464e43f63851261b0f72fb59d301fc99a7","analyzedAt":"2026-08-21T12:05:35.031Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}