{"record":{"id":"ba0091731580e798","repo":"cli/cli","slug":"expected-the-owner-repo-format-got-q","errorCode":null,"errorMessage":"expected the \\\"OWNER/REPO\\\" format, got %q","messagePattern":"expected the \\\\\"OWNER/REPO\\\\\" format, got %q","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/safeurl/safeurl.go","lineNumber":24,"sourceCode":"\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// RepoPartsFromNWO parses a raw \"owner/repo\" string and returns the owner and name\n// unescaped. It returns an error unless nwo contains exactly one slash with a non-empty\n// owner and name, so a value carrying extra slashes cannot smuggle additional path\n// segments through as the owner or name.\n//\n// This intentionally does not reuse ghrepo.FromFullName, which accepts the broader\n// \"[HOST/]OWNER/REPO\" form. The call sites here only ever handle a bare \"OWNER/REPO\",\n// so a stricter parse that rejects an unexpected host component is the safer fit.\nfunc RepoPartsFromNWO(nwo string) (owner, name string, err error) {\n\tparts := strings.Split(nwo, \"/\")\n\tif len(parts) != 2 || parts[0] == \"\" || parts[1] == \"\" {\n\t\treturn \"\", \"\", fmt.Errorf(\"expected the \\\"OWNER/REPO\\\" format, got %q\", nwo)\n\t}\n\treturn parts[0], parts[1], nil\n}\n\n// SafeURL is the sealed interface implemented by the URL types in this package.\n// It exists so that a value known to address a safe REST API URL can be passed\n// around and rendered without exposing how it was built.\ntype SafeURL interface {\n\tString() string\n\n\t// The sealed method keeps the set of implementations closed to this package,\n\t// so callers outside it cannot forge a value that claims to be safe.\n\tsealed()\n}\n\n// MutableSafeURL is a REST API URL built from a host prefix, path components, and query\n// parameters. The path components and query parameters are URL encoded (aka\n// percent-encoded) when the URL is rendered so that caller supplied values cannot","sourceCodeStart":6,"sourceCodeEnd":42,"githubUrl":"https://github.com/cli/cli/blob/0eeec0b92edbe70199f9768522f831d3534f41ad/internal/safeurl/safeurl.go#L6-L42","documentation":"RepoPartsFromNWO in internal/safeurl parses a raw \"OWNER/REPO\" string and throws this error when the input does not contain exactly one slash with a non-empty owner and a non-empty repo name. It is deliberately stricter than ghrepo.FromFullName: it rejects the \"[HOST/]OWNER/REPO\" form and any value with extra slashes, so a host component or smuggled extra path segments cannot pass through as owner or repo. Any caller that feeds it a full name like \"github.com/cli/cli\" or \"cli\" will fail.","triggerScenarios":"Calling safeurl.RepoPartsFromNWO with: a bare repo name (\"cli\"), a full name with host (\"github.com/cli/cli\"), a trailing/leading slash (\"cli/\" or \"/cli\"), an empty string, or a value with two or more slashes (\"a/b/c\"). Only exact \"owner/repo\" with both parts non-empty passes.","commonSituations":"Passing a value obtained from ghrepo.FromFullName().RepoSlug() mixed with host-qualified names, reusing user input that may include a hostname, or forgetting to strip a trailing slash from a repo argument in a CLI wrapper around skills discovery.","solutions":["Pass a bare \"OWNER/REPO\" string, e.g. \"cli/cli\"; strip any host prefix or trailing slash before calling","If the input may carry a host, parse it first with ghrepo.FromFullName and rebuild owner/name from the resulting Repository fields","Trim slashes and split input yourself, verifying len(parts)==2 and both parts non-empty, before handing it to this function"],"exampleFix":"// before\nowner, repo, err := safeurl.RepoPartsFromNWO(\"github.com/cli/cli\")\n\n// after\nr, err := ghrepo.FromFullName(\"github.com/cli/cli\") // tolerates HOST/OWNER/REPO\nif err != nil {\n\treturn err\n}\nowner, repo, err := safeurl.RepoPartsFromNWO(r.RepoSlug()) // now bare OWNER/REPO","handlingStrategy":"validation","validationCode":"// ValidateNWO reports whether s is a bare \"OWNER/REPO\" with non-empty parts.\nfunc ValidateNWO(s string) bool {\n\tparts := strings.Split(s, \"/\")\n\treturn len(parts) == 2 && parts[0] != \"\" && parts[1] != \"\"\n}\n\nif !ValidateNWO(fullName) {\n\treturn fmt.Errorf(\"%q must be OWNER/REPO without host\", fullName)\n}\nowner, repo, err := safeurl.RepoPartsFromNWO(fullName)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Normalize names through ghrepo.FromFullName and pass r.RepoSlug() so no host component ever reaches the parser","Trim leading/trailing slashes from user input before parsing","Reject inputs containing more than one slash at the CLI boundary with a usage message"],"tags":["validation","parsing","safeurl","input-validation"],"backgroundTag":null,"analyzedSha":"0eeec0b92edbe70199f9768522f831d3534f41ad","analyzedAt":"2026-08-15T12:31:05.478Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}