{"record":{"id":"0be21146fdfbadfa","repo":"golang/go","slug":"version-s-is-not-canonical","errorCode":null,"errorMessage":"version %s is not canonical","messagePattern":"version (.+?) is not canonical","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cmd/go/internal/modfetch/coderepo.go","lineNumber":970,"sourceCode":"\t// path might replace a module with path gopkg.in/foo.v2-unstable, and that's\n\t// ok.\n\treturn pathMajor[1:] == mpathMajor[1:]\n}\n\n// canReplaceMismatchedVersionDueToBug reports whether versions of r\n// could replace versions of mpath with otherwise-mismatched major versions\n// due to a historical bug in the Go command (golang.org/issue/34254).\nfunc (r *codeRepo) canReplaceMismatchedVersionDueToBug(mpath string) bool {\n\t// The bug caused us to erroneously accept unversioned paths as replacements\n\t// for versioned gopkg.in paths.\n\tunversioned := r.pathMajor == \"\"\n\treplacingGopkgIn := strings.HasPrefix(mpath, \"gopkg.in/\")\n\treturn unversioned && replacingGopkgIn\n}\n\nfunc (r *codeRepo) GoMod(ctx context.Context, version string) (data []byte, err error) {\n\tif version != module.CanonicalVersion(version) {\n\t\treturn nil, fmt.Errorf(\"version %s is not canonical\", version)\n\t}\n\n\tif module.IsPseudoVersion(version) {\n\t\t// findDir ignores the metadata encoded in a pseudo-version,\n\t\t// only using the revision at the end.\n\t\t// Invoke Stat to verify the metadata explicitly so we don't return\n\t\t// a bogus file for an invalid version.\n\t\t_, err := r.Stat(ctx, version)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\trev, dir, gomod, err := r.findDir(ctx, version)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif gomod != nil {","sourceCodeStart":952,"sourceCodeEnd":988,"githubUrl":"https://github.com/golang/go/blob/b6b368adc57c96c3151d224d172029f233ead2c3/src/cmd/go/internal/modfetch/coderepo.go#L952-L988","documentation":"Returned by codeRepo.GoMod when the version argument is not equal to module.CanonicalVersion(version). Canonical form strips leading zeros and redundant segments (v1.2.3, v1.2.3-pre, v1.2.3+build); anything else is rejected because every downstream cache key assumes canonicalization.","triggerScenarios":"An internal caller hands GoMod a version such as \"v1.2\", \"v1.02.3\", or a pseudo-version with extra characters. The check `version != module.CanonicalVersion(version)` fails on the first line of GoMod before any I/O.","commonSituations":"A wrapper tool or test constructs versions by string concatenation; a manual call to modfetch on a tag list without canonicalizing; upgrading code that previously tolerated loose semver.","solutions":["Canonicalize before calling: pass module.CanonicalVersion(version) (or reject early if it differs).","If the version comes from a VCS tag, normalize tags with module.CanonicalVersion and skip non-semver tags.","Reject non-canonical input at the API boundary of your tool rather than letting it reach GoMod."],"exampleFix":"// before\nrepo.GoMod(ctx, \"v1.2\")\n// after\nv := module.CanonicalVersion(\"v1.2\")\nrepo.GoMod(ctx, v)","handlingStrategy":"validation","validationCode":"v := module.CanonicalVersion(version)\nif v != version {\n    return fmt.Errorf(\"refusing non-canonical version %q (canonical %q)\", version, v)\n}\nreturn repo.GoMod(ctx, v)","typeGuard":"// isCanonicalVersion narrows a version string already confirmed canonical.\nfunc isCanonicalVersion(v string) bool {\n    return v != \"\" && v == module.CanonicalVersion(v)\n}","tryCatchPattern":null,"preventionTips":["Never hand-build version strings; route them through module.CanonicalVersion.","Reject non-canonical versions at the boundary of your tool before any modfetch call."],"tags":["versioning","canonical","go-mod","api-misuse"],"analyzedSha":"b6b368adc57c96c3151d224d172029f233ead2c3","analyzedAt":"2026-08-12T00:22:02.250Z","schemaVersion":2},"datasetVersion":"2026-08-12T06:17:24.410Z"}