ScreenshotNeo

BlogHow-to

How to Parse URLs in Go

Learn when to use url.Parse or url.ParseRequestURI, validate hosts, read queries, preserve escaped paths, and resolve relative URLs safely.

By the ScreenshotNeo team1 October 20266 min read

Use Go’s standard-library net/url package. Call url.Parse for general absolute or relative URL references, and url.ParseRequestURI for input received as an HTTP request target. Parsing does not prove that an input is an absolute URL, so validate the fields your application requires and always handle errors.

Choose the parser for your input

Input Use Important behavior
General URL or URI reference url.Parse Accepts absolute and relative references.
HTTP request target url.ParseRequestURI Accepts an absolute URI or absolute path and assumes there is no fragment.
Strict query validation url.ParseQuery Returns malformed query-pair errors.
Relative link against a known page ResolveReference Resolves RFC 3986 references against an absolute base URL.

See the official net/url documentation for the API reference.

Parse and validate an absolute URL

package main

import (
    "errors"
    "fmt"
    "net/url"
)

func parseAbsolute(raw string) (*url.URL, error) {
    u, err := url.Parse(raw)
    if err != nil {
        return nil, err
    }
    if u.Scheme == "" || u.Host == "" {
        return nil, errors.New("expected an absolute URL with a scheme and host")
    }
    return u, nil
}

func main() {
    u, err := parseAbsolute("https://example.com/products?id=42#details")
    if err != nil {
        panic(err)
    }
    fmt.Println("scheme:", u.Scheme)
    fmt.Println("host:", u.Host)
    fmt.Println("path:", u.Path)
    fmt.Println("raw query:", u.RawQuery)
    fmt.Println("fragment:", u.Fragment)
}

url.Parse can successfully parse a relative reference. A value such as example.com/path may therefore have no scheme and no host in the way your application expects. The explicit validation above is application policy, not a guarantee from the parser.

Parse an HTTP request target

func requestURL(r *http.Request) (*url.URL, error) {
    u, err := url.ParseRequestURI(r.RequestURI)
    if err != nil {
        return nil, err
    }
    return u, nil
}

ParseRequestURI is intended for request-context input. It interprets the value only as an absolute URI or an absolute path and assumes no fragment suffix. Use url.Parse for ordinary URL references.

Read paths, escaped paths, and components

raw := "https://example.com/foo%2fbar?q=go%20url"
u, err := url.Parse(raw)
if err != nil {
    panic(err)
}

fmt.Println(u.Path)         // /foo/bar (decoded)
fmt.Println(u.EscapedPath()) // /foo%2fbar (encoded spelling)
fmt.Println(u.String())      // https://example.com/foo%2fbar?q=go%20url
fmt.Println(u.Hostname())
fmt.Println(u.Port())
fmt.Println(u.Fragment)

URL.Path is decoded. If an encoded slash (%2F) must remain distinct from a literal slash, use EscapedPath(). URL.String() uses the escaped path when rebuilding the URL.

Request URI output

u, err := url.Parse("https://example.org/path?foo=bar")
if err != nil {
    panic(err)
}
fmt.Println(u.RequestURI()) // /path?foo=bar

RequestURI() returns the encoded path and query portion suitable for an HTTP request. It does not include the scheme or host.

Read and update query parameters

u, err := url.Parse("https://example.com/search?q=go&page=2")
if err != nil {
    panic(err)
}

// Convenient, but malformed pairs are silently discarded.
values := u.Query()
fmt.Println(values.Get("q"))
fmt.Println(values.Get("page"))

// Strict parsing: surface malformed query data.
strict, err := url.ParseQuery(u.RawQuery)
if err != nil {
    panic(err)
}
strict.Set("page", "3")
u.RawQuery = strict.Encode()
fmt.Println(u.String())

Use u.Query() when discarded malformed pairs are acceptable. Use url.ParseQuery(u.RawQuery) when invalid input must produce an error. To modify parameters, update the returned url.Values and assign its encoded form back to RawQuery.

Resolve relative references

base, err := url.Parse("https://example.com/docs/")
if err != nil {
    panic(err)
}
ref, err := url.Parse("../guide?format=html")
if err != nil {
    panic(err)
}
absolute := base.ResolveReference(ref)
fmt.Println(absolute.String()) // https://example.com/guide?format=html

The base URL must be absolute. Resolution returns a new URL and follows standard reference-resolution rules. A reference beginning with / replaces the base path; a query-only reference keeps the base path; a fragment-only reference keeps the base path and query.

Complete command-line example

package main

import (
    "errors"
    "flag"
    "fmt"
    "net/url"
)

func main() {
    raw := flag.String("url", "", "URL to parse")
    flag.Parse()
    if *raw == "" {
        panic("usage: parseurl -url https://example.com/a?q=1")
    }

    u, err := url.Parse(*raw)
    if err != nil {
        panic(err)
    }
    if u.Scheme == "" || u.Host == "" {
        panic(errors.New("an absolute URL is required"))
    }

    fmt.Println("scheme:", u.Scheme)
    fmt.Println("host:", u.Host)
    fmt.Println("hostname:", u.Hostname())
    fmt.Println("port:", u.Port())
    fmt.Println("path:", u.Path)
    fmt.Println("escaped path:", u.EscapedPath())
    fmt.Println("request URI:", u.RequestURI())
    fmt.Println("query:", u.Query())
    fmt.Println("fragment:", u.Fragment)
}

Common errors and fixes

Symptom Cause Fix
No error, but host is empty The input is a relative reference or lacks a scheme. Require both u.Scheme and u.Host when an absolute URL is required.
%2F appears as / Path is decoded. Use EscapedPath() when encoded spelling matters.
Bad query data disappears Query() silently drops malformed pairs. Parse RawQuery with url.ParseQuery and check its error.
Fragment rejected for request input Request targets are assumed not to contain fragments. Use ParseRequestURI only for request targets; use Parse for references.
Relative link resolves incorrectly The base path’s trailing slash changes directory semantics. Use the correct absolute base, then call ResolveReference.
Credentials or tokens leak in logs Logging the complete URL includes user info or query values. Redact User, sensitive query keys, and authorization data before logging.

Validation and security checklist

  • Parse first and handle the returned error.
  • Require the schemes your application supports, usually an explicit allowlist rather than any non-empty scheme.
  • Require a host when accepting absolute URLs.
  • Use Hostname() and Port() instead of splitting Host manually.
  • Validate redirects against an allowlist; parsing alone does not make a URL safe to fetch.
  • Keep decoded and escaped paths separate when routing, signing, caching, or comparing resources.
  • Use strict query parsing for API input and reject duplicate or unexpected keys when your protocol requires it.
  • Redact query strings, user info, and authorization headers in logs.

Performance, reliability, and cost

net/url is part of Go’s standard library and requires no service, credentials, or network request. Keep parsing on the request path, validate once, and pass the resulting *url.URL to code that needs it. Avoid reparsing the same string in multiple layers. For untrusted input, bound request sizes before parsing and reject values that do not meet your application’s scheme, host, path, or query rules.

Or skip the browser setup

If your goal is to turn a URL into an image or PDF rather than inspect its components, ScreenshotNeo provides a single GET request. Its capture pipeline accepts cookie and consent banners, removes more than 60 known consent platforms plus newsletter popups and chat widgets, and bills only clean shots. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing result.

See the ScreenshotNeo API documentation for all options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo also supports full-page and element captures, device presets, retina scale, dark mode, custom CSS and JavaScript, waits, request blocking, headers, cookies, geolocation, caching, signed links, asynchronous jobs, bulk capture, PDF output, HTML/CSS rendering, usage data, and an MCP server for AI agents. Every feature is on every plan. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account and start with 1,000 screenshots per month at no charge.

FAQ

Does url.Parse fetch the URL?

No. It only parses text into a url.URL structure. Fetching is a separate operation.

Should I store Path or RawPath?

Store the representation your protocol needs. Use Path for decoded application paths and EscapedPath() when the original encoded form affects identity or signing.

Can a URL have a query without a path?

Yes. For example, https://example.com?x=1 has an empty path and a non-empty query.

When should I use RequestURI()?

Use it when an HTTP client or server API needs the encoded path and query portion, without scheme and host.