{"record":{"id":"eb0eaf2a95eef7c5","repo":"laurent22/joplin","slug":"this-item-is-already-present-and-cannot-be-added-again-item","errorCode":null,"errorMessage":"This item is already present and cannot be added again: ${item.name}","messagePattern":"This item is already present and cannot be added again: (.+?)","errorType":"http","errorClass":"ErrorConflict","httpStatus":409,"severity":"error","filePath":"packages/server/src/models/ItemModel.ts","lineNumber":1207,"sourceCode":"\t\t\t// Savepoint needed because on Postgres a failed statement aborts the whole\n\t\t\t// transaction, and we recover from the unique constraint error below.\n\t\t\tconst savePoint = await this.setSavePoint();\n\n\t\t\ttry {\n\t\t\t\titem = await super.save(item, options);\n\t\t\t\tawait this.releaseSavePoint(savePoint);\n\t\t\t} catch (error) {\n\t\t\t\tawait this.rollbackSavePoint(savePoint);\n\n\t\t\t\tif (isUniqueConstraintError(error)) {\n\t\t\t\t\t// The item was created by a concurrent request - typically a client\n\t\t\t\t\t// retrying an upload that is still being processed. Save it as an update\n\t\t\t\t\t// so that the retry is not treated as an error.\n\t\t\t\t\tconst existingItem = await this.loadByName(item.owner_id || userId, item.name, { fields: ['id'] });\n\n\t\t\t\t\tif (!existingItem) {\n\t\t\t\t\t\tmodelLogger.error(`Unique constraint error on item, but the item could not be found: ${JSON.stringify({ id: item.id, name: item.name, jop_id: item.jop_id, owner_id: item.owner_id })}`, error);\n\t\t\t\t\t\tthrow new ErrorConflict(`This item is already present and cannot be added again: ${item.name}`);\n\t\t\t\t\t}\n\n\t\t\t\t\tmodelLogger.info(`Item was created by a concurrent request - updating it instead: ${JSON.stringify({ name: item.name, jop_id: item.jop_id, owner_id: item.owner_id })}`);\n\n\t\t\t\t\tisNew = false;\n\t\t\t\t\titem = await super.save({ ...item, id: existingItem.id }, { ...options, isNew: false });\n\t\t\t\t} else {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (isNew) await this.models().userItem().add(userId, item.id);\n\n\t\t\t// We only record updates. Create and Delete events are recorded elsewhere.\n\t\t\tconst changeItemName = item.name || previousName;\n\n\t\t\tif (!isNew && this.shouldRecordChange(changeItemName)) {\n\t\t\t\tawait this.models().change().recordChange({","sourceCodeStart":1189,"sourceCodeEnd":1225,"githubUrl":"https://github.com/laurent22/joplin/blob/981a03c5c9e88130bccff4db47c411d35ec7ae2c/packages/server/src/models/ItemModel.ts#L1189-L1225","documentation":"When inserting an item hits a unique-constraint conflict on the server, ItemModel tries to locate the existing item by owner and name. If that lookup finds nothing (e.g. jop_id/id mismatch or an inconsistent constraint), it cannot treat the insert as an update, so it throws ErrorConflict to the client.","triggerScenarios":"A PUT/POST upload that races with a concurrent request creating the same item name, where the subsequent loadByName(owner_id, name) returns null despite the duplicate-key error.","commonSituations":"Two clients uploading the same item simultaneously; retries after a network timeout where the server-side state is inconsistent; sync engines colliding on item names for the same user.","solutions":["Retry the upload after a short backoff — the concurrent request may finish and the item become visible.","Verify the item id/jop_id/owner_id sent matches the existing item; mismatches can defeat the lookup.","Re-sync the client so server state is authoritative before re-uploading.","If it persists, inspect server logs — the modelLogger.error line records the failing identifiers."],"exampleFix":"// before\nawait api.post('/items', item); // throws ErrorConflict\n// after\ntry {\n  await api.post('/items', item);\n} catch (e) {\n  if (e.message.includes('already present')) await sleep(backoff).then(() => api.put(`/items/${existingId}`, item));\n  else throw e;\n}","handlingStrategy":"retry","validationCode":"const existing = await this.loadByName(item.owner_id || userId, item.name, { fields: ['id'] });\nif (existing) return this.save({ ...item, id: existing.id }, { isNew: false });","typeGuard":"null","tryCatchPattern":"try { await itemModel.save(item, { isNew: true }); } catch (e) { if (e instanceof ErrorConflict) await sleep(500).then(() => itemModel.save(item, { isNew: false })); else throw e; }","preventionTips":["Upload idempotently using stable item ids/names","Serialize uploads of the same item across clients","Check for the item before creating (loadByName first)","Monitor server logs for unique-constraint errors to find racy sync paths"],"tags":["server","conflict","sync","unique-constraint"],"backgroundTag":"file-already-exists","analyzedSha":"981a03c5c9e88130bccff4db47c411d35ec7ae2c","analyzedAt":"2026-09-17T14:49:40.960Z","contentChangedAt":"2026-09-17T14:49:40.960Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}