netbirdio/netbird · warning
file already exists
Error message
file already exists
What it means
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.
Source
Thrown at upload-server/server/local.go:138
filePath := filepath.Clean(filepath.Join(dirPath, uploadFile))
if !strings.HasPrefix(filePath, cleanBase) {
http.Error(w, "invalid path", http.StatusBadRequest)
log.Warnf("Path traversal attempt blocked (file): %s", filePath)
return
}
if err = os.MkdirAll(dirPath, 0750); err != nil {
http.Error(w, "failed to create upload dir", http.StatusInternalServerError)
log.Errorf("Failed to create upload dir: %v", err)
return
}
flags := os.O_WRONLY | os.O_CREATE | os.O_EXCL
f, err := os.OpenFile(filePath, flags, 0600)
if err != nil {
if os.IsExist(err) {
http.Error(w, "file already exists", http.StatusConflict)
return
}
http.Error(w, "failed to create file", http.StatusInternalServerError)
log.Errorf("Failed to create file %s: %v", filePath, err)
return
}
defer func() { _ = f.Close() }()
if _, err = f.Write(body); err != nil {
http.Error(w, "failed to write file", http.StatusInternalServerError)
log.Errorf("Failed to write file %s: %v", filePath, err)
return
}
log.Infof("Uploaded file %s", filePath)
w.WriteHeader(http.StatusOK)
}
View on GitHub (pinned to 93e97f4bf1)
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
Example fix
// before: replaying the same URL; second attempt gets 409
for i := 0; i < retries; i++ { httpPut(url, body) }
// after: fresh URL per attempt
for i := 0; i < retries; i++ {
url := getUploadURL(id) // GET /upload-url -> new <id>/<uuid> key
if err := httpPut(url, body); err == nil { break }
} Defensive patterns
Strategy: validation
Validate before calling
// always obtain a fresh key per upload attempt
url, err := getUploadURL(ctx, id) // GET /upload-url?id=<id>
if err != nil {
return err
}
// PUT to url once; on any failure loop back to getUploadURL, never reuse url Try / catch
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. Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/d377db1056bc31de.
Report an issue: GitHub.