ErrLookup › Background articles › "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures
"Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures
"Invalid URL", "URL cannot be empty", and "couldn't parse URL" errors happen before any network traffic: a library's URL parser (WHATWG new URL, Go's net/url, Rust's Url::parse, Ruby's URI) rejected the string you handed it. This guide explains the common triggers — missing scheme, empty or whitespace-only values, unencoded spaces and control characters, empty hosts — and how to fix and prevent them.
Distilled from 125 documented records across 50 repositories.
Background
The invalid-url family is produced at the request-construction layer, before any socket is opened. Every HTTP client must turn a string into a structured URL object — JavaScript's WHATWG URL constructor, Go's url.Parse or http.NewRequestWithContext, Rust's reqwest::Url::parse, Ruby's URI.parse combined with Addressable — and each of those parsers has a strict grammar. When the string violates it (or fails a scheme/host policy check layered on top of the parse), the library fails fast with an error like "Invalid URL", "URL cannot be empty", or a wrapped parse error such as "invalid server URL %q: %w". Because request construction performs no I/O, the error fires immediately and deterministically: the same input produces the same error every time, with no network, timeout, or server involved.
These checks exist for two reasons beyond basic parsing. First, a malformed URL that slipped through parsing would otherwise surface later as a confusing DNS failure, 404, or timeout deep inside the request path — several libraries (pnpm's registry alias validation, jaeger's OTLP proxy registration, Tailscale's client constructors) validate explicitly to move that failure to startup or configuration time. Second, some checks are security guards: an empty host would send a request to the local machine (siyuan's WebFetch treats this as an SSRF risk), absolute-form paths could retarget a proxied request past Deno's --allow-net permission check, and postal's AddressGuard treats a hostless URL as a blocked destination rather than attempting a connection.
From the caller's side, the error usually points at a configuration value or user input rather than code logic. The recurring culprits across the 50 repositories are remarkably consistent: a base URL or endpoint stored without its https:// scheme ("api.example.com/v1", "localhost:8000"), an empty string from an unset environment variable or config key, whitespace or control characters picked up from copy-paste or YAML parsing, unencoded spaces in a path, or a host segment accidentally dropped by string concatenation ("https:/example.com", "http:///path"). The error message often embeds the offending value — sometimes quoted with %q to make invisible characters visible — or, deliberately, omits it when the URL may contain credentials (GitNexus's LLM base URL validation).
The family varies across libraries in where it draws the line. What one parser rejects outright, another accepts: Go's url.Parse is lenient and lets scheme-less hosts and empty strings through, pushing those failures to later scheme/host checks, while the WHATWG URL constructor requires an absolute URL and throws on anything relative without a base. Empty-host detection is another fork: some libraries reject at parse time, others (siyuan, postal) as a distinct post-parse guard. And libraries that must accept relative URLs (gemini-cli's OAuth endpoints with allowRelative) fail only when the combination of relative string and base URI is unparseable. So the same input string can produce this error in one library and pass into a different failure mode in another.
Common causes
- Missing scheme. Bare hostnames like "api.example.com/v1" or "localhost:8000" are the single most common trigger across the family. The WHATWG URL constructor rejects them outright (or parses "localhost:8080" with scheme "localhost:"), while lenient Go/Ruby parsers accept them and fail later on a scheme or host check.
- Empty or whitespace-only URL. An unset environment variable, missing config key, nil-checked pointer dereferenced to empty, or a base-plus-path concatenation with an empty base yields an empty string. zeroclaw trims first, so even " " lands on this check.
- Stray whitespace, quotes, or control characters. Copy-pasted URLs carry trailing spaces, no-break spaces, smart quotes, or newlines; YAML/env parsing leaks surrounding quotes into the value. These make the string unparseable, and %q-style error messages exist precisely to make the hidden bytes visible.
- Empty host after parsing. Inputs like "https:/single-slash", "http:///path", "https://", or scheme-less "example.com/path" parse but yield an empty host component. Some libraries reject this as a separate guard, often framed as an SSRF protection because an empty host would target the local machine.
- Unencoded special characters and bad percent-escapes. Raw spaces in paths ("http://example.com/my file.jpg"), invalid sequences like %zz, or path characters inside IPv6 brackets ("http://[fe80::1/foo]") violate the URL grammar. Dynamic segments must be percent-encoded before assembly.
- String concatenation dropping or corrupting the host. Building URLs by concatenating possibly-empty variables or interpolated config values routinely produces hostless or truncated URLs. Records across Go, Rust, and Ruby codebases recommend assembling with the language's URL type instead.
- Relative URL where an absolute one is required, or a bad base. Libraries that do accept relative URLs (Angular's upgrade shim, gemini-cli's OAuth endpoints, importScripts in workers) throw when the base is missing, empty, or itself malformed — new URL(relative) without a valid base always fails.
- Wrong or typo'd scheme for the client. Non-http schemes fail scheme-enforcement checks: "ftp://" where only http/https requests are built, "file:///tmp/icon.png" rejected by siyuan's emoji downloader, or a typo like "htp://" or "http//host" failing the parse itself.
What usually fixes it
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
- [object Object]
Go deeper
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Documented occurrences
- URL cannot be empty (zeroclaw-labs/zeroclaw)
- Failed to resolve relative OAuth URL "${urlStr}" against base "${options.baseUri}": ${getErrorMessage(e)} (google-gemini/gemini-cli)
- creating upload request: %w (grafana/k6)
- ERR_INVALID_URL: Invalid URL: ${path} (denoland/deno)
- Invalid URL (${url}) with base (${base}) (angular/angular)
- creating register request: %w (tailscale/tailscale)
- invalid pkgsAddr %q: %w (tailscale/tailscale)
- SyntaxError: e.message (denoland/deno)
- data?.msg || data?.message || window.siyuan.languages._kernel[28] (siyuan-note/siyuan)
- custom provider base URL is invalid: {err} (Hmbown/CodeWhale)
- invalid connstring URL: {err} (neondatabase/neon)
- invalid OTLP proxy target %q: %w (jaegertracing/jaeger)
- Invalid URL: ${url} (santifer/career-ops)
- failed to create HTTP request: %w (navidrome/navidrome)
- SyntaxError: Failed to parse URL: ${scriptUrl} (denoland/deno)
- invalid stack URL: %w (grafana/k6)
- invalid custom emoji URL (siyuan-note/siyuan)
- No host was given for the request (postalserver/postal)
- Invalid LLM base URL: must be a well-formed http:// or https:// URL (abhigyanpatwari/GitNexus)
- ERR_INVALID_URL: Invalid URL: ${url} (denoland/deno)
…and 105 more across the corpus — use search.
Honest provenance: generated on 2026-09-01 from AI-assisted analysis of the linked records. See how records are made.