{"record":{"id":"d377db1056bc31de","repo":"netbirdio/netbird","slug":"file-already-exists","errorCode":null,"errorMessage":"file already exists","messagePattern":"file already exists","errorType":"http","errorClass":null,"httpStatus":409,"severity":"warning","filePath":"upload-server/server/local.go","lineNumber":138,"sourceCode":"\n\tfilePath := filepath.Clean(filepath.Join(dirPath, uploadFile))\n\tif !strings.HasPrefix(filePath, cleanBase) {\n\t\thttp.Error(w, \"invalid path\", http.StatusBadRequest)\n\t\tlog.Warnf(\"Path traversal attempt blocked (file): %s\", filePath)\n\t\treturn\n\t}\n\n\tif err = os.MkdirAll(dirPath, 0750); err != nil {\n\t\thttp.Error(w, \"failed to create upload dir\", http.StatusInternalServerError)\n\t\tlog.Errorf(\"Failed to create upload dir: %v\", err)\n\t\treturn\n\t}\n\n\tflags := os.O_WRONLY | os.O_CREATE | os.O_EXCL\n\tf, err := os.OpenFile(filePath, flags, 0600)\n\tif err != nil {\n\t\tif os.IsExist(err) {\n\t\t\thttp.Error(w, \"file already exists\", http.StatusConflict)\n\t\t\treturn\n\t\t}\n\t\thttp.Error(w, \"failed to create file\", http.StatusInternalServerError)\n\t\tlog.Errorf(\"Failed to create file %s: %v\", filePath, err)\n\t\treturn\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\tif _, err = f.Write(body); err != nil {\n\t\thttp.Error(w, \"failed to write file\", http.StatusInternalServerError)\n\t\tlog.Errorf(\"Failed to write file %s: %v\", filePath, err)\n\t\treturn\n\t}\n\n\tlog.Infof(\"Uploaded file %s\", filePath)\n\tw.WriteHeader(http.StatusOK)\n}\n","sourceCodeStart":120,"sourceCodeEnd":156,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/upload-server/server/local.go#L120-L156","documentation":"HTTP 409 returned when os.OpenFile(filePath, O_WRONLY|O_CREATE|O_EXCL, 0600) fails with EEXIST. The O_EXCL flag makes create-or-fail atomic, so a file already exists at that exact path; the handler deliberately answers Conflict instead of overwriting, unlike S3 PUT which would replace the object.","triggerScenarios":"Retrying a PUT with the same upload URL/key after a previous (even partial) upload created the file; re-using a previously fetched upload URL instead of requesting a new one; theoretically a UUID collision in the generated key.","commonSituations":"Client retry logic that replays the same PUT on timeout; two racing uploads consuming the same URL; upload frameworks that assume S3-style overwrite semantics against the local backend.","solutions":["On 409, request a fresh URL from GET /upload-url (it generates a new <id>/<uuid> key) and PUT again","Make 'fetch a new URL' part of every retry step; never replay the same PUT URL","If you need idempotent overwrite semantics, run the S3 backend (BUCKET + AWS_REGION set) instead of local storage"],"exampleFix":"// before: replaying the same URL; second attempt gets 409\nfor i := 0; i < retries; i++ { httpPut(url, body) }\n\n// after: fresh URL per attempt\nfor i := 0; i < retries; i++ {\n\turl := getUploadURL(id) // GET /upload-url -> new <id>/<uuid> key\n\tif err := httpPut(url, body); err == nil { break }\n}","handlingStrategy":"validation","validationCode":"// always obtain a fresh key per upload attempt\nurl, err := getUploadURL(ctx, id) // GET /upload-url?id=<id>\nif err != nil {\n\treturn err\n}\n// PUT to url once; on any failure loop back to getUploadURL, never reuse url","typeGuard":null,"tryCatchPattern":"If resp.StatusCode == 409 ('file already exists'), the key is burned: fetch a new URL from GET /upload-url and PUT there. Retrying the same URL always returns 409.","preventionTips":["Fetch a new upload URL for every attempt and every resume","Do not persist upload URLs across restarts; keys are single-use on the local backend","Remember the local backend is create-only (O_EXCL); S3 PUT semantics differ"],"tags":["http","conflict","upload","idempotency","go"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}