{"record":{"id":"df6a7713e7ae501b","repo":"netbirdio/netbird","slug":"duration-must-not-be-negative-df6a77","errorCode":null,"errorMessage":"duration must not be negative","messagePattern":"duration must not be negative","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"proxy/internal/debug/handler.go","lineNumber":836,"sourceCode":"//\tduration: capture duration (0 or absent = max, capped at 30m)\n//\tformat:   \"text\" for human-readable output (default: pcap)\n//\tfilter:   BPF-like filter expression (e.g. \"host 10.0.0.1 and tcp port 443\")\nfunc (h *Handler) handleCapture(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {\n\tclient, ok := h.provider.GetClient(accountID)\n\tif !ok {\n\t\thttp.Error(w, \"client not found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tduration := maxCaptureDuration\n\tif durationStr := r.URL.Query().Get(\"duration\"); durationStr != \"\" {\n\t\td, err := time.ParseDuration(durationStr)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"invalid duration: \"+err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif d < 0 {\n\t\t\thttp.Error(w, \"duration must not be negative\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tif d > 0 {\n\t\t\tduration = min(d, maxCaptureDuration)\n\t\t}\n\t}\n\n\tfilter := r.URL.Query().Get(\"filter\")\n\twantText := r.URL.Query().Get(\"format\") == \"text\"\n\tverbose := r.URL.Query().Get(\"verbose\") == \"true\"\n\tascii := r.URL.Query().Get(\"ascii\") == \"true\"\n\n\topts := nbembed.CaptureOptions{Filter: filter, Verbose: verbose, ASCII: ascii}\n\tif wantText {\n\t\tw.Header().Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\t\topts.TextOutput = w\n\t} else {\n\t\tw.Header().Set(\"Content-Type\", \"application/vnd.tcpdump.pcap\")","sourceCodeStart":818,"sourceCodeEnd":854,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/proxy/internal/debug/handler.go#L818-L854","documentation":"A duration that parses successfully but is negative is rejected explicitly: the handler checks d < 0 after time.ParseDuration. Zero is allowed and means 'use the 30-minute maximum'; only values below zero are refused.","triggerScenarios":"GET /debug/clients/<id>/capture?duration=-1s or any negative Go duration string, e.g. a computed 'time until deadline' that already passed.","commonSituations":"Computing duration as deadline minus now, which goes negative once the deadline passes; a UI numeric field with a minus sign; sign errors in generated URLs.","solutions":["Send a positive duration or omit the parameter","Clamp computed durations to zero before sending: if d < 0 { d = 0 }","Treat a negative remaining time in the caller as 'capture window already ended' and skip the request"],"exampleFix":"// before\nd := deadline.Sub(time.Now()) // may be negative once the deadline passed\nGET /debug/clients/<id>/capture?duration=<d>\n\n// after\nif d < 0 {\n    d = 0 // zero means 'use the 30m maximum'\n}","handlingStrategy":"validation","validationCode":"d, err := time.ParseDuration(s)\nif err != nil {\n    return err\n}\nif d < 0 {\n    d = 0 // zero selects the 30-minute maximum\n}\nq := url.Values{\"duration\": []string{d.String()}}","typeGuard":"func isNonNegativeDuration(s string) bool {\n    d, err := time.ParseDuration(s)\n    return err == nil && d >= 0\n}","tryCatchPattern":null,"preventionTips":["Clamp deadline-derived durations to zero before building the URL","Treat a negative remaining window in the caller as 'already finished' and skip the request"],"tags":["http","proxy","debug-ui","packet-capture","duration","validation"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}