{"record":{"id":"0ede26d4c4fadd7d","repo":"hashicorp/terraform","slug":"url-is-not-a-valid-s3-url","errorCode":null,"errorMessage":"URL is not a valid S3 URL","messagePattern":"URL is not a valid S3 URL","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/getmodules/moduleaddrs/detect_s3.go","lineNumber":22,"sourceCode":"package moduleaddrs\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\n// detectS3 detects strings that seem like schemeless references to\n// Amazon S3 and translates them into URLs for the \"s3\" getter.\nfunc detectS3(src string) (string, bool, error) {\n\tif len(src) == 0 {\n\t\treturn \"\", false, nil\n\t}\n\n\tif strings.Contains(src, \".amazonaws.com/\") {\n\t\tparts := strings.Split(src, \"/\")\n\t\tif len(parts) < 2 {\n\t\t\treturn \"\", false, fmt.Errorf(\n\t\t\t\t\"URL is not a valid S3 URL\")\n\t\t}\n\n\t\thostParts := strings.Split(parts[0], \".\")\n\t\tif len(hostParts) == 3 {\n\t\t\treturn detectS3PathStyle(hostParts[0], parts[1:])\n\t\t} else if len(hostParts) == 4 {\n\t\t\treturn detectS3OldVhostStyle(hostParts[1], hostParts[0], parts[1:])\n\t\t} else if len(hostParts) == 5 && hostParts[1] == \"s3\" {\n\t\t\treturn detectS3NewVhostStyle(hostParts[2], hostParts[0], parts[1:])\n\t\t} else {\n\t\t\treturn \"\", false, fmt.Errorf(\n\t\t\t\t\"URL is not a valid S3 URL\")\n\t\t}\n\t}\n\n\treturn \"\", false, nil\n}","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/getmodules/moduleaddrs/detect_s3.go#L4-L40","documentation":"Raised by detectS3 (internal/getmodules/moduleaddrs/detect_s3.go:22). The source contains '.amazonaws.com/' so S3 detection engages, but splitting the string on '/' yields fewer than 2 parts, meaning there is no bucket segment after the host. A valid schemeless S3 reference needs at least a host and a bucket (e.g. s3.amazonaws.com/bucket).","triggerScenarios":"A source like 's3.amazonaws.com/' (trailing slash, nothing after) or just a hostname with no bucket, e.g. 's3.us-east-1.amazonaws.com'. The detector entered because '.amazonaws.com/' was present but found no bucket path component.","commonSituations":"Truncated or templated S3 URL where the bucket variable rendered empty; a copy-paste that lost the bucket/key portion; a CI pipeline injecting an empty bucket name.","solutions":["Append the bucket name (and key) after the host: s3.amazonaws.com/my-bucket/modules/vpc.","If the bucket is supplied via a variable, validate it is non-empty before rendering the source string.","Prefer the explicit s3::https://... URL form so missing segments are more obvious."],"exampleFix":"// before\nmodule \"x\" { source = \"s3.amazonaws.com/${var.bucket}\" }  // var.bucket is empty\n// after\nmodule \"x\" { source = \"s3.amazonaws.com/my-bucket/modules/vpc\" }","handlingStrategy":"validation","validationCode":"// Ensure a schemeless S3 reference has at least a host + bucket.\nfunc validS3Shorthand(src string) bool {\n\tif !strings.Contains(src, \".amazonaws.com/\") {\n\t\treturn false\n\t}\n\tparts := strings.Split(src, \"/\")\n\treturn len(parts) >= 2 && parts[1] != \"\"\n}","typeGuard":null,"tryCatchPattern":"addr, err := moduleaddrs.ParseModuleSource(src)\nif err != nil && strings.Contains(src, \"amazonaws.com\") {\n    return fmt.Errorf(\"S3 source %q is missing a bucket/key segment: %w\", src, err)\n}","preventionTips":["Always include the bucket name and key after the S3 host.","Validate that templated bucket variables are non-empty before rendering the source.","Prefer the explicit s3::https:// form for clarity."],"tags":["s3","module-source","url-parsing","validation"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}