Summary
A Node.js script that works perfectly when called from a browser fails with
TypeError: Failed to parse URL from /time
when run standalone. The culprit is a relative URL ('/time') passed to fetch().
Node’s fetch (via undici) requires an absolute URL because there is no document base to resolve against.
Root Cause
fetch('/time', options)is a relative URL.- In a browser the base URL is the page’s origin, so the request resolves to
http://<host>/time. - In Node there is no implicit base; the URL must be fully qualified (
http://localhost:80/time). - The underlying
new Request('/time')throwsERR_INVALID_URLbecause the string cannot be parsed as a valid absolute URL.
Why This Happens in Real Systems
- Runtime differences – Browsers have a document location; Node runs scripts without a document context.
- Fetch specification – The WHATWG
fetchspec expects a request input that is either aRequestobject or an absolute URL string. Relative URLs are only allowed when a base URL is provided (viaRequestInit.baseUrlin some polyfills, not in native Node). - Environment defaults – Node’s undici implementation does not infer a base from
process.cwd()or__dirname; it strictly validates the URL.
Real-World Impact
- Cron jobs or scheduled scripts that call internal APIs will fail silently or crash, leading to missed data updates.
- CI/CD pipelines that run Node‑based integration tests may report spurious failures.
- Debugging time is wasted chasing “network” issues when the problem is simply a malformed URL.
- Error propagation – Unhandled
TypeErrorcan terminate the entire process if not caught.
Example or Code (if necessary and relevant)
// ❌ Broken – relative URL, works only in a browser
const response = await fetch('/time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
// ✅ Fix – absolute URL
const response = await fetch('http://localhost:80/time', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
});
How Senior Engineers Fix It
-
Always use a fully qualified URL when calling
fetchfrom Node. -
Centralise base URL in a config constant:
const BASE_URL = process.env.API_BASE_URL || 'http://localhost:80'; const response = await fetch(`${BASE_URL}/time`, options); -
Validate URLs early with a helper:
function buildUrl(path) { const base = process.env.API_BASE_URL || 'http://localhost:80'; return new URL(path, base).toString(); } -
Add error handling around fetch calls:
try { const res = await fetch(buildUrl('/time'), options); // … } catch (err) { console.error('Fetch failed:', err.message); // retry / alert logic } -
Document the assumption that the runtime has no implicit base URL.
Why Juniors Miss It
- Browser‑centric mental model – They assume the same environment as a web page, where relative URLs resolve automatically.
- Lack of runtime awareness – Not yet familiar with the differences between Node’s
globalThis.fetchand the browser’s implementation. - Copy‑paste habits – Reusing frontend snippets without adapting them to a server‑side context.
- Insufficient debugging – The error message mentions “Failed to parse URL”, but without understanding that a base URL is missing, they chase network or server issues instead.