{"record":{"id":"aa2dedd3385637af","repo":"gotify/server","slug":"sort-key-is-not-unique","errorCode":null,"errorMessage":"sort key is not unique","messagePattern":"sort key is not unique","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"api/application.go","lineNumber":563,"sourceCode":"\t\tname := gen()\n\t\tif !exist(imgDir + name) {\n\t\t\treturn name\n\t\t}\n\t}\n}\n\nfunc ValidApplicationImageExt(ext string) bool {\n\tswitch strings.ToLower(ext) {\n\tcase \".gif\", \".png\", \".jpg\", \".jpeg\":\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc handleApplicationError(ctx *gin.Context, err error) {\n\tif errors.Is(err, gorm.ErrDuplicatedKey) {\n\t\tctx.AbortWithError(400, errors.New(\"sort key is not unique\"))\n\t} else {\n\t\tctx.AbortWithError(500, err)\n\t}\n}\n","sourceCodeStart":545,"sourceCodeEnd":568,"githubUrl":"https://github.com/gotify/server/blob/14bfc256276775c425f988d621dccfe705de18ac/api/application.go#L545-L568","documentation":"handleApplicationError maps gorm.ErrDuplicatedKey to this 400 message. It is returned when an INSERT/UPDATE violates a unique constraint — in this codebase, the applications table enforces uniqueness on the sort key column, so two applications cannot share the same sort key.","triggerScenarios":"CreateApplication called with a SortKey value that already exists on another application row (unique index hit, GORM translates the driver's duplicate-key error to gorm.ErrDuplicatedKey).","commonSituations":"Clients hardcoding sort values like 0 or 1 for every new application; concurrent requests picking the same sort key; imports/migrations that reuse existing sort keys; after a unique index was newly added and legacy rows already collide.","solutions":["Send a unique sort key (or omit it if the API can default/append to the end)","Query existing sort keys first and pick max+1 (or a gap) before creating","Retry with a different key if racing with concurrent creations","Add server-side logic to auto-assign the next free sort key instead of trusting client input"],"exampleFix":"// before\nPOST /applications {\"name\":\"App\",\"sort_key\":1}   // 1 already used\n// after\nconst max = Math.max(...apps.map(a => a.sort_key), 0);\nPOST /applications {\"name\":\"App\",\"sort_key\": max + 1}","handlingStrategy":"validation","validationCode":"const keys = apps.map(a => a.sort_key);\nconst nextKey = Math.max(0, ...keys) + 1;\nawait api.post('/applications', {name, sort_key: nextKey});","typeGuard":null,"tryCatchPattern":"try {\n  await api.post('/applications', payload);\n} catch (e) {\n  if (e.response?.status === 400 && e.response?.data?.includes('sort key is not unique')) {\n    payload.sort_key = Date.now(); // or recompute a free key and retry once\n  } else throw e;\n}","preventionTips":["Never hardcode sort keys (0/1) for multiple records","Fetch existing sort keys and compute max+1 before creating","Handle 400 'sort key is not unique' by recomputing and retrying, since races are possible","Prefer server-side auto-assignment of sort keys over client-supplied values"],"tags":["http-400","unique-constraint","gorm","database","go"],"backgroundTag":"unique-constraint-violation","analyzedSha":"14bfc256276775c425f988d621dccfe705de18ac","analyzedAt":"2026-09-05T12:52:36.781Z","contentChangedAt":"2026-09-05T12:52:36.781Z","schemaVersion":2},"datasetVersion":"2026-09-12T17:17:11.597Z"}