{"record":{"id":"6cb7aaf5094fcdd7","repo":"shadow1ng/fscan","slug":"w-empty-host-errinvalidurl","errorCode":null,"errorMessage":"%w: empty host (ErrInvalidURL)","messagePattern":"%w: empty host \\(ErrInvalidURL\\)","errorType":"validation","errorClass":"ErrInvalidURL","httpStatus":null,"severity":"error","filePath":"webscan/web_scan.go","lineNumber":115,"sourceCode":"func buildTargetURL(info *common.HostInfo) (string, error) {\n\t// 自动构建URL\n\tif info.URL == \"\" {\n\t\tprotocol := protocolHTTP\n\t\tif isTLSPort(info.Port) {\n\t\t\tprotocol = protocolHTTPS\n\t\t}\n\t\tinfo.URL = protocol + net.JoinHostPort(info.Host, fmt.Sprint(info.Port))\n\t} else if !hasProtocolPrefix(info.URL) {\n\t\tinfo.URL = protocolHTTP + normalizeSchemelessWebTarget(info.URL)\n\t}\n\n\t// 解析URL以提取基础部分\n\tparsedURL, err := url.Parse(info.URL)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"%w: %w\", ErrInvalidURL, err)\n\t}\n\tif parsedURL.Hostname() == \"\" {\n\t\treturn \"\", fmt.Errorf(\"%w: empty host\", ErrInvalidURL)\n\t}\n\tportStr := parsedURL.Port()\n\tif portStr == \"\" {\n\t\tif hasMalformedWebURLPort(parsedURL.Host) {\n\t\t\treturn \"\", fmt.Errorf(\"%w: invalid port\", ErrInvalidURL)\n\t\t}\n\t} else {\n\t\tport, err := strconv.Atoi(portStr)\n\t\tif err != nil || port < 1 || port > 65535 {\n\t\t\treturn \"\", fmt.Errorf(\"%w: invalid port %q\", ErrInvalidURL, portStr)\n\t\t}\n\t}\n\tparsedURL.Host = normalizeWebURLHost(parsedURL.Host)\n\n\treturn fmt.Sprintf(\"%s://%s\", parsedURL.Scheme, parsedURL.Host), nil\n}\n\n// hasProtocolPrefix 检查URL是否包含协议前缀","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/shadow1ng/fscan/blob/95cc12e753bf43de7004e5aef42a9ffba3934303/webscan/web_scan.go#L97-L133","documentation":"buildTargetURL parses a target URL and returns a wrapped ErrInvalidURL when the parsed URL has no host component. This happens when info.URL parses successfully (url.Parse rarely errors) but contains no hostname, e.g. a relative path or scheme-only string. The sentinel ErrInvalidURL lets callers check errors.Is(err, ErrInvalidURL).","triggerScenarios":"WebScan calls buildTargetURL with info.URL that yields parsedURL.Hostname() == \"\": e.g. \"/admin\", \"http:///path\", \"example.com/path\" (no scheme), or \"\".","commonSituations":"Scanner config where the URL column lost its scheme (\"example.com\" instead of \"http://example.com\"), CSV/Excel imports trimming the scheme, or hand-built URLs from host+path concatenation missing the scheme.","solutions":["Ensure info.URL includes scheme and host, e.g. http://example.com","Validate with url.Parse and check parsedURL.Hostname() != \"\" before calling WebScan","Normalize bare hosts by prefixing a default scheme (http://) in the input pipeline","Handle errors.Is(err, ErrInvalidURL) by rejecting/flagging the target"],"exampleFix":"// before\nWebScan(target)\n// after\nu, err := url.Parse(target)\nif err != nil || u.Hostname() == \"\" {\n    target = \"http://\" + strings.TrimPrefix(target, \"//\")\n}\nWebScan(target)","handlingStrategy":"validation","validationCode":"u, err := url.Parse(target)\nif err != nil || u.Hostname() == \"\" {\n    return fmt.Errorf(\"target %q has no host\", target)\n}","typeGuard":"func hasHost(raw string) bool {\n    u, err := url.Parse(raw)\n    return err == nil && u.Hostname() != \"\"\n}","tryCatchPattern":"if err := WebScan(target); err != nil {\n    if errors.Is(err, ErrInvalidURL) {\n        log.Printf(\"skipping invalid target %q: %v\", target, err)\n        return nil\n    }\n    return err\n}","preventionTips":["Always store targets with scheme+host (http://host)","Normalize bare hosts with a default scheme at ingestion","Unit-test buildTargetURL against relative/empty URLs"],"tags":["url","validation","webscan"],"backgroundTag":"invalid-url","analyzedSha":"95cc12e753bf43de7004e5aef42a9ffba3934303","analyzedAt":"2026-09-06T17:07:30.094Z","contentChangedAt":"2026-09-06T17:07:30.094Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}