From 285477455752af8c828ef0467b3ab7b42dbafd19 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 8 May 2026 09:50:31 +0200 Subject: [PATCH 01/11] feat(Adapters): Add item context to HTTP errors and always log response body when HTTP status >= 400 see #2158 Signed-off-by: Marcel Klehr --- src/errors/Error.ts | 8 +- src/lib/adapters/Karakeep.ts | 115 ++++++++++++++----------- src/lib/adapters/Linkwarden.ts | 44 +++++++--- src/lib/adapters/NextcloudBookmarks.ts | 57 +++++++++--- 4 files changed, 146 insertions(+), 78 deletions(-) diff --git a/src/errors/Error.ts b/src/errors/Error.ts index 017d6970c1..bbb16f0041 100644 --- a/src/errors/Error.ts +++ b/src/errors/Error.ts @@ -1,4 +1,4 @@ -import { Bookmark, TItemLocation } from '../lib/Tree' +import { Bookmark, TItem, TItemLocation } from '../lib/Tree' import { statusCodes } from '../lib/statusCodes' export class FloccusError extends Error { @@ -160,14 +160,16 @@ export class HttpError extends TransientError { public status: number public method: string public statusMessage: string + public item: TItem|undefined|null - constructor(status: number, method: string) { + constructor(status: number, method: string, item?: TItem) { super( - `E019: HTTP status ${status}. Failed ${method} request (${statusCodes[status]}). Check your server configuration and log.` + `E019: HTTP status ${status}. Failed ${method} request (${statusCodes[status]})` + (item ? ` for item ${item.inspect()}` : '') + `. Check your server configuration and log.` ) this.status = status this.method = method this.statusMessage = statusCodes[status] + this.item = item Object.setPrototypeOf(this, HttpError.prototype) } } diff --git a/src/lib/adapters/Karakeep.ts b/src/lib/adapters/Karakeep.ts index 95d319d943..4f66001fa1 100644 --- a/src/lib/adapters/Karakeep.ts +++ b/src/lib/adapters/Karakeep.ts @@ -1,5 +1,5 @@ import Adapter from '../interfaces/Adapter' -import { Bookmark, Folder, ItemLocation } from '../Tree' +import { Bookmark, Folder, ItemLocation, TItem, TItemLocation } from '../Tree' import PQueue from 'p-queue' import { ICapabilities, IHashSettings, IResource } from '../interfaces/Resource' import Logger from '../Logger' @@ -118,12 +118,7 @@ export default class KarakeepAdapter implements Adapter, IResource { + async createBookmark(bookmark: Bookmark): Promise { Logger.log('(karakeep)CREATE', { bookmark }) const response = await this.sendRequest( 'POST', @@ -133,7 +128,9 @@ export default class KarakeepAdapter implements Adapter, IResource { + async updateBookmark(bookmark: Bookmark): Promise { Logger.log('(karakeep)UPDATE', { bookmark }) const [id, oldParentId] = this.parseBookmarkId(bookmark.id) await this.sendRequest( @@ -170,7 +165,9 @@ export default class KarakeepAdapter implements Adapter, IResource { + async removeBookmark(bookmark: Bookmark): Promise { Logger.log('(karakeep)DELETE', { bookmark }) const [id, parentId] = this.parseBookmarkId(bookmark.id) @@ -208,7 +204,8 @@ export default class KarakeepAdapter implements Adapter, IResource { + async createFolder(folder: Folder): Promise { Logger.log('(karakeep)CREATEFOLDER', { folder }) const response = await this.sendRequest( 'POST', @@ -239,16 +233,14 @@ export default class KarakeepAdapter implements Adapter, IResource { + async updateFolder(folder: Folder): Promise { Logger.log('(karakeep)UPDATEFOLDER', { folder }) await this.sendRequest( 'PATCH', @@ -257,14 +249,16 @@ export default class KarakeepAdapter implements Adapter, IResource { + async removeFolder(folder: Folder): Promise { Logger.log('(karakeep)DELETEFOLDER', { folder }) const deleteListContent = async() => { @@ -276,17 +270,25 @@ export default class KarakeepAdapter implements Adapter, IResource b.id)) } while (nextCursor !== null) await Promise.all( bookmarkIds.map((id) => - this.removeBookmark({ + // create a dummy bookmark + this.removeBookmark(new Bookmark({ id: `${id};${folder.id}`, parentId: folder.id, - }) + url: '', + title: '', + location: ItemLocation.SERVER + })) ) ) } @@ -300,9 +302,13 @@ export default class KarakeepAdapter implements Adapter, IResource - this.removeFolder({ + // create dummy folder + this.removeFolder(new Folder({ id: listId, - }) + parentId: folder.id, + title: '', + location: ItemLocation.SERVER, + })) ) ) } @@ -315,7 +321,8 @@ export default class KarakeepAdapter implements Adapter, IResource = null ): Promise { const url = this.server.url + relUrl let res @@ -447,7 +455,7 @@ export default class KarakeepAdapter implements Adapter, IResource= 400) { - throw new HttpError(res.status, verb) + Logger.log( + `${verb} ${url}: Server responded with ${res.status}: ` + + (await res.text()).substring(0, 250) + ) + throw new HttpError(res.status, verb, item) } let json try { @@ -512,7 +524,8 @@ export default class KarakeepAdapter implements Adapter, IResource = null ) { let res let timedOut = false @@ -561,7 +574,11 @@ export default class KarakeepAdapter implements Adapter, IResource= 400) { - throw new HttpError(res.status, verb) + Logger.log( + `${verb} ${url}: Server responded with ${res.status}: ` + + res.data.substring(0, 250) + ) + throw new HttpError(res.status, verb, item) } const json = res.data diff --git a/src/lib/adapters/Linkwarden.ts b/src/lib/adapters/Linkwarden.ts index acf1efe0ee..6b00c5ca04 100644 --- a/src/lib/adapters/Linkwarden.ts +++ b/src/lib/adapters/Linkwarden.ts @@ -1,5 +1,5 @@ import Adapter from '../interfaces/Adapter' -import { Bookmark, Folder, ItemLocation } from '../Tree' +import { Bookmark, Folder, ItemLocation, TItem, TItemLocation } from '../Tree' import PQueue from 'p-queue' import { ICapabilities, IHashSettings, IResource } from '../interfaces/Resource' import Logger from '../Logger' @@ -118,7 +118,10 @@ export default class LinkwardenAdapter implements Adapter, IResource): Promise { Logger.log('(linkwarden)DELETE', {bookmark}) - await this.sendRequest('DELETE', `/api/v1/links/${bookmark.id}`) + await this.sendRequest('DELETE', `/api/v1/links/${bookmark.id}`, undefined, undefined, false, bookmark) } async createFolder(folder: Folder): Promise { @@ -154,7 +160,10 @@ export default class LinkwardenAdapter implements Adapter, IResource): Promise { @@ -178,7 +190,7 @@ export default class LinkwardenAdapter implements Adapter, IResource { + async sendRequest(verb:string, relUrl:string, type:string = null, body:any = null, returnRawResponse = false, item: TItem = null):Promise { const url = this.server.url + relUrl let res let timedOut = false @@ -259,7 +271,7 @@ export default class LinkwardenAdapter implements Adapter, IResource= 400) { - throw new HttpError(res.status, verb) + Logger.log( + `${verb} ${url}: Server responded with ${res.status}: ` + + (await res.text()).substring(0, 250) + ) + throw new HttpError(res.status, verb, item) } let json try { @@ -317,7 +333,7 @@ export default class LinkwardenAdapter implements Adapter, IResource = null) { let res let timedOut = false try { @@ -363,7 +379,11 @@ export default class LinkwardenAdapter implements Adapter, IResource= 400) { - throw new HttpError(res.status, verb) + Logger.log( + `${verb} ${url}: Server responded with ${res.status}: ` + + (await res.text()).substring(0, 250) + ) + throw new HttpError(res.status, verb, item) } const json = res.data diff --git a/src/lib/adapters/NextcloudBookmarks.ts b/src/lib/adapters/NextcloudBookmarks.ts index bf942c5ab5..61f9b22138 100644 --- a/src/lib/adapters/NextcloudBookmarks.ts +++ b/src/lib/adapters/NextcloudBookmarks.ts @@ -3,7 +3,7 @@ import { CapacitorHttp as Http } from '@capacitor/core' import Adapter from '../interfaces/Adapter' import HtmlSerializer from '../serializers/Html' import Logger from '../Logger' -import { Bookmark, Folder, ItemLocation, TItem } from '../Tree' +import { Bookmark, Folder, ItemLocation, TItem, TItemLocation } from '../Tree' import { Base64 } from 'js-base64' import AsyncLock from 'async-lock' import * as Parallel from 'async-parallel' @@ -430,7 +430,10 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes 'POST', 'index.php/apps/bookmarks/public/rest/v2/folder', 'application/json', - body + body, + undefined, + undefined, + folder, ) if (typeof json.item !== 'object') { throw new UnexpectedServerResponseError() @@ -475,7 +478,10 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes 'POST', `index.php/apps/bookmarks/public/rest/v2/folder/${parentId}/import`, 'multipart/form-data', - body + body, + undefined, + undefined, + folder ) }) @@ -528,7 +534,10 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes 'PUT', `index.php/apps/bookmarks/public/rest/v2/folder/${id}`, 'application/json', - body + body, + undefined, + undefined, + folder ) const oldParentFolder = this.tree.findFolder(oldFolder.parentId) if (!oldParentFolder) { @@ -573,7 +582,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes } await this.sendRequest( 'DELETE', - `index.php/apps/bookmarks/public/rest/v2/folder/${id}` + `index.php/apps/bookmarks/public/rest/v2/folder/${id}`, + undefined, + undefined, + undefined, + undefined, + folder, ) const parent = this.tree.findFolder(oldFolder.parentId) if (parent) { @@ -679,7 +693,10 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes 'POST', 'index.php/apps/bookmarks/public/rest/v2/bookmark', 'application/json', - body + body, + undefined, + undefined, + bm ) } catch (e) { if (e instanceof HttpError) { @@ -740,7 +757,10 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes 'PUT', `index.php/apps/bookmarks/public/rest/v2/bookmark/${upstreamId}`, 'application/json', - body + body, + undefined, + undefined, + newBm, ) } catch (e) { if (e instanceof HttpError) { @@ -780,7 +800,12 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes try { await this.sendRequest( 'DELETE', - `index.php/apps/bookmarks/public/rest/v2/folder/${parentId}/bookmarks/${upstreamId}` + `index.php/apps/bookmarks/public/rest/v2/folder/${parentId}/bookmarks/${upstreamId}`, + undefined, + undefined, + undefined, + undefined, + bookmark, ) // remove bookmark from the cached list const list = await this.getBookmarksList() @@ -851,7 +876,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes return json.ocs.data } - async sendRequest(verb:string, relUrl:string, type:string = null, originalBody:any = null, returnRawResponse = false, headers = {}):Promise { + async sendRequest(verb:string, relUrl:string, type:string = null, originalBody:any = null, returnRawResponse = false, headers = {}, item: TItem = null):Promise { const url = this.normalizeServerURL(this.server.url) + relUrl let res let timedOut = false @@ -870,7 +895,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes Logger.log(`QUEUING ${verb} ${url}`) if (!IS_BROWSER) { - return this.sendRequestNative(verb, url, type, body, returnRawResponse, headers) + return this.sendRequestNative(verb, url, type, body, returnRawResponse, headers, item) } const authString = !this.ticket || this.ticketTimestamp + 60 * 60 * 1000 < Date.now() @@ -920,7 +945,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes if ((res.status === 401 || res.status === 403 || res.status === 404) && authString.startsWith('Bearer')) { this.ticket = null this.ticketTimestamp = 0 - return this.sendRequest(verb, relUrl, type, originalBody, returnRawResponse, headers) + return this.sendRequest(verb, relUrl, type, originalBody, returnRawResponse, headers, item) } if (returnRawResponse) { @@ -932,7 +957,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes } if (res.status === 503 || res.status >= 400) { Logger.log(`${verb} ${url}: Server responded with ${res.status}: ` + (await res.text()).substring(0, 250)) - throw new HttpError(res.status, verb) + throw new HttpError(res.status, verb, item) } let json try { @@ -992,7 +1017,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes return res.status === 200 } - private async sendRequestNative(verb: string, url: string, type: string, body: any, returnRawResponse: boolean, headers = {}) { + private async sendRequestNative(verb: string, url: string, type: string, body: any, returnRawResponse: boolean, headers = {}, item: TItem = null) { let res let timedOut = false const authString = !this.ticket || this.ticketTimestamp + 60 * 60 * 1000 < Date.now() @@ -1037,7 +1062,7 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes if ((res.status === 401 || res.status === 403 || res.status === 404) && authString.startsWith('Bearer')) { this.ticket = null this.ticketTimestamp = 0 - return this.sendRequestNative(verb, url, type, body, returnRawResponse, headers) + return this.sendRequestNative(verb, url, type, body, returnRawResponse, headers, item) } if (returnRawResponse) { @@ -1052,6 +1077,10 @@ export default class NextcloudBookmarksAdapter implements Adapter, BulkImportRes throw new AuthenticationError() } if (res.status === 503 || res.status >= 400) { + Logger.log( + `${verb} ${url}: Server responded with ${res.status}: ` + + res.data.substring(0, 250) + ) throw new HttpError(res.status, verb) } const json = res.data From c25bb90954aafed3c1b0fdfeeb447519aaa44072 Mon Sep 17 00:00:00 2001 From: Marcel Klehr Date: Fri, 8 May 2026 09:51:08 +0200 Subject: [PATCH 02/11] fix: Run lint:fix Signed-off-by: Marcel Klehr --- src/ui/views/native/Home.vue | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/views/native/Home.vue b/src/ui/views/native/Home.vue index 668ae360bc..a83362a993 100644 --- a/src/ui/views/native/Home.vue +++ b/src/ui/views/native/Home.vue @@ -1,13 +1,14 @@