{"record":{"id":"cfd877d4bff0510e","repo":"netbirdio/netbird","slug":"unclosed-parenthesis","errorCode":null,"errorMessage":"unclosed parenthesis","messagePattern":"unclosed parenthesis","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"util/capture/filter.go","lineNumber":374,"sourceCode":"}\n\nfunc (p *parser) parseUnary() (exprNode, error) {\n\tswitch p.peek() {\n\tcase \"not\":\n\t\tp.next()\n\t\tinner, err := p.parseUnary()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nodeNot(inner), nil\n\tcase \"(\":\n\t\tp.next()\n\t\tinner, err := p.parseOr()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := p.expect(\")\"); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"unclosed parenthesis\")\n\t\t}\n\t\treturn inner, nil\n\tdefault:\n\t\treturn p.parseAtom()\n\t}\n}\n\nfunc (p *parser) parseAtom() (exprNode, error) {\n\ttok := p.next()\n\tif tok == \"\" {\n\t\treturn nil, fmt.Errorf(\"unexpected end of expression\")\n\t}\n\n\tswitch tok {\n\tcase \"host\":\n\t\taddr, err := p.parseAddr()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"host: %w\", err)","sourceCodeStart":356,"sourceCodeEnd":392,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/util/capture/filter.go#L356-L392","documentation":"Parse error from the recursive-descent parser of NetBird's capture filter language (util/capture/filter.go). After consuming '(' and successfully parsing the inner or-expression, parser.expect(\")\") fails because the token stream ended or diverged, so parseUnary returns 'unclosed parenthesis'.","triggerScenarios":"A filter with more '(' than ')': 'host 10.0.0.1 and (port 443', '((tcp or udp)', or nested groups like 'not (tcp and (port 53' where a final ')' was never typed.","commonSituations":"Hand-written packet-capture filter strings; programmatically composed filters where a conditional clause appends '(' without a matching ')'; long one-liners pasted from issues or notes.","solutions":["Add the missing ')' so every '(' is closed; use editor bracket matching or a paren-count lint on the string","Build nested groups programmatically and close each group in the same scope that opened it","Compile the filter once at config-load time (before starting the capture) so the error surfaces with full context"],"exampleFix":"// before\nfilter := \"host 10.0.0.1 and (port 443 or port 8443\"\n\n// after\nfilter := \"host 10.0.0.1 and (port 443 or port 8443)\"","handlingStrategy":"validation","validationCode":"func balancedParens(expr string) bool {\n\tdepth := 0\n\tfor _, r := range expr {\n\t\tswitch r {\n\t\tcase '(':\n\t\t\tdepth++\n\t\tcase ')':\n\t\t\tdepth--\n\t\t\tif depth < 0 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn depth == 0\n}\n\nif !balancedParens(filter) {\n\treturn fmt.Errorf(\"filter has unbalanced parentheses\")\n}","typeGuard":"func isValidFilterExpr(s string) bool {\n\treturn strings.TrimSpace(s) != \"\" && balancedParens(s)\n}","tryCatchPattern":"Parse the filter at config-load time and wrap the parse error with the offending expression; show the string with a caret at the position of the last unmatched '('.","preventionTips":["Validate the expression before starting a capture session, not after","Close each '(' in the same code block that opened it when composing filters programmatically","Lint filter strings with a paren-balance check in config validation"],"tags":["parser","filter","capture","go","syntax"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}