Next.js: Error: only absolute urls are supported (fix)
Why fetch("/api/products") works after a client-side click but throws on refresh, what each fetch implementation prints, and the two fixes that hold in both routers.
TL;DR#
fetch('/api/products') has no host when it runs on the server. In the browser, a relative URL resolves against the page address. On the server there is no page address, so the fetch library throws before any request is sent. With getInitialProps, the first load runs on the server and fails, while a next/link click runs in the browser and works. Best fix: on the server, call the code that produces the data (the database query or the shared function behind the API route) instead of fetching your own API over HTTP. When an HTTP call is unavoidable, build an absolute URL, for example from req.headers.host in getInitialProps, after checking that req exists.
The error#
The Stack Overflow question comes from an Express custom server with a /products page whose getInitialProps calls fetch('/api/products') through isomorphic-unfetch. Clicking a link to /products works, but refreshing the page throws:
Error: only absolute urls are supportedThe exact message depends on the fetch implementation your server code uses. All three of these come from the same relative call, reproduced on Node.js v22.23.3:
### node-fetch 1.x called with a relative URL
Error: only absolute urls are supported
### node-fetch 2.x called with a relative URL
TypeError: Only absolute URLs are supported
### native fetch (undici) called with a relative URL
TypeError: Failed to parse URL from /api/products | cause: TypeError: Invalid URLIn a current Next.js app the third one is what you will usually see. next start on Next.js 16.4.0 logged this on the server when the broken pages called fetch('/api/products') (stack frames trimmed):
тип TypeError: Failed to parse URL from /api/products
at ignore-listed frames {
[cause]: TypeError: Invalid URL
code: 'ERR_INVALID_URL',
input: '/api/products'The page returned HTTP 500 and the API route itself still returned 200.
Why it happens#
A URL such as /api/products is only half an address. A browser fills in the scheme and host from the page it is showing. A Node.js process has no "current page", so a fetch implementation running on the server can only accept a full URL with a scheme and a host. node-fetch 2.x states the rule in its source: when the parsed URL has no protocol or no hostname, it throws Only absolute URLs are supported. The fetch built into Node.js rejects the bare path with an Invalid URL cause whose code is ERR_INVALID_URL.
Why does it work on click and fail on refresh? The getInitialProps reference explains the split: "For the initial page load, getInitialProps will run on the server only. getInitialProps will then also run on the client when navigating to a different route with the next/link component or by using next/router." A click runs the function in the browser, where the relative URL resolves. A refresh is an initial page load, so it runs on the server, where it cannot resolve. The client-side half is documented behaviour; the server-side half is what the reproduction below shows.
The getServerSideProps docs state that it runs on the server, and App Router Server Components render on the server too, so in those the relative fetch fails on the server-rendered request rather than only on refresh.
The custom server is not the cause#
The question blames its setup, an Express server in front of Next.js. As a negative control, every request-time failure in the reproduction ran under plain next start, with no custom server at all. The custom server guide only changes how the Next.js request handler is started. It does not give server-side code an origin, and taking Express away does not make a relative fetch work.
Which library printed your message#
The lowercase message in the question is node-fetch 1.x's own wording: its source throws new Error('only absolute urls are supported'). The npm registry shows that isomorphic-unfetch 1.0.0, published in March 2017, depended on node-fetch ^1.6.3. That was the current version when the question was asked in June 2017. Running isomorphic-unfetch 1.0.0 on Node.js 22, with the built-in fetch hidden, prints the asker's line again:
### isomorphic-unfetch 1.0.0 (June 2017 era) on Node 22: global fetch hidden, relative URL
Error: only absolute urls are supportedLater versions changed the wording, not the rule. node-fetch 2.x capitalises it, and node-fetch 3.x passes the string straight to new URL():
### node-fetch 3.x called with a relative URL
TypeError: Invalid URL | code: ERR_INVALID_URLisomorphic-unfetch 3.1.0 now reuses the global fetch when one exists. On Node.js 22 the asker's code therefore prints the native message:
### isomorphic-unfetch 3 on Node 22 with a relative URL
TypeError: Failed to parse URL from /api/productsFix#
1. Read the data directly on the server (recommended)#
Your API route and your page run in the same Node.js process. Calling the API over HTTP from the server adds a network round trip just to reach code you could import. The getServerSideProps docs say so: "You do not have to call a Next.js API Route to fetch data when using getServerSideProps since the function runs on the server. Instead, you can call a CMS, database, or other third-party APIs directly from inside getServerSideProps."
Move the query into a module that both the API route and the page import:
// lib/products.js
export function getProducts() {
return [{ _id: '1', title: 'Kettle' }, { _id: '2', title: 'Toaster' }]
}In the asker's app, the body of getProducts would be the Product.find({}) query from the Express route.
Keep the API route for the browser, reusing the same function:
// pages/api/products.js
import { getProducts } from '../../lib/products'
export default function handler(req, res) { res.status(200).json(getProducts()) }Then read the data in getServerSideProps without any fetch. Note that this replaces the asker's getInitialProps with getServerSideProps, and that is deliberate: getInitialProps also runs in the browser on client-side navigation, so a database module imported into it would end up in browser code. The getInitialProps reference calls it "a legacy API" and recommends getStaticProps or getServerSideProps instead.
// pages/products.js
import { getProducts } from '../lib/products'
export default function Page({ products }) {
return <ul>{products.map(p => <li key={p._id}>{p.title}</li>)}</ul>
}
export async function getServerSideProps() {
return { props: { products: getProducts() } }
}If the query talks to PostgreSQL, keep the client in a server-only module, as described in where a node-postgres Pool belongs in a Next.js app. Importing it into a component the browser bundles fails in a different way.
2. In the App Router, call the function from the Server Component#
The backend-for-frontend guide gives the same advice for Route Handlers:
For Server Components prerendered at build time, using Route Handlers will fail the build step. This is because, while building there is no server listening for these requests.
// app/catalogue/page.js
import { getProducts } from '../../lib/products'
export default async function Page() {
const products = getProducts()
return <ul>{products.map(p => <li key={p._id}>{p.title}</li>)}</ul>
}3. When you must go over HTTP, build an absolute URL from the request#
Sometimes an HTTP call is unavoidable. You may have a legacy getInitialProps page that also runs in the browser, or the API may live behind its own middleware. In that case, derive the origin from the incoming request on the server and keep a relative URL in the browser. req exists only on the server, as the reference table says, so check it before reading its headers:
// pages/products.js (getInitialProps variant)
const Products = ({ products }) => <ul>{products.map(p => <li key={p._id}>{p.title}</li>)}</ul>
Products.getInitialProps = async function ({ req }) {
const baseUrl = req ? `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}` : ''
const res = await fetch(`${baseUrl}/api/products`)
return { products: await res.json() }
}
export default ProductsOne answer to the question sets protocol from req.headers['x-forwarded-proto'] before its req ? check. Read as written, that line runs on a client-side navigation too, where req is undefined. Put the guard first, as above.
The accepted answer hardcodes the origin in a config module, switched on NODE_ENV. That works for code that runs at request time on a server that is listening. Build time is different. The environment variables guide says that when NODE_ENV is unassigned, "Next.js automatically assigns development when running the next dev command, or production for all other commands." So during next build, that config picks the deployment URL, not localhost. A page prerendered at build time then fetches the deployed server: the build fails if that server is not up, and it bakes in whatever data the old deployment returns if it is. That path was not reproduced here. What was reproduced is the related case in At build time a hardcoded localhost URL fails as well below.
Verify the fix#
Build and start the app, then request each page from the server, so you exercise the refresh path rather than a client-side click:
npx next build
npx next start -p 3000
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/productsThat command prints only the status code. In the reproduction, a request loop over every page answered 500 for the broken pages, which logged the errors above, and printed these two lines for the direct-read page and the guarded getInitialProps page:
GET /fixed-direct -> 200 <li>Kettle</li><li>Toaster</li>
GET /fixed-gip -> 200 <li>Kettle</li><li>Toaster</li> Then click through to the page from another route in a browser. The guarded getInitialProps takes the empty-origin branch there, so the request stays relative.
One email a month тАФ the latest explained errors
What the site published that month тАФ mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
At build time a hardcoded localhost URL fails as well#
An App Router page with no dynamic data is prerendered during next build. With a relative fetch to its own Route Handler, the build stops on the same parse error:
Error occurred prerendering page "/catalogue". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: Failed to parse URL from /api/products
[cause]: TypeError: Invalid URLSwap in a hardcoded absolute URL and the error changes, but the build still fails. Nothing is listening on port 3000 while next build runs:
Error occurred prerendering page "/catalogue". Read more: https://nextjs.org/docs/messages/prerender-error
TypeError: fetch failed
[cause]: Error: connect ECONNREFUSED 127.0.0.1:3000Reading getProducts() directly built cleanly and prerendered /catalogue as static content. This is the case where fix 1 is the only fix that works. Making the URL absolute only swaps one error for another. If a hardcoded localhost URL fails at request time instead, with the server running, the address family can be the issue: see ECONNREFUSED on localhost under Node 17+.
Reproduction details#
Everything above was reproduced on 13 October 2026, in the official node:22-bookworm Docker image (Node.js v22.23.3) with Next.js 16.4.0, node-fetch 2.7.0, node-fetch 1.7.3, node-fetch 3.3.2, isomorphic-unfetch 1.0.0 and 3.1.0. The app had a Pages Router API route, five Pages Router pages (three broken, two fixed) and App Router pages, built with next build and served with next start. The browser half of the click-versus-refresh split was not run; it rests on the getInitialProps documentation quoted above.
Once the data comes from a direct call instead of a JSON response, a different check applies. getServerSideProps props must be serialisable, and database documents often carry Date objects that a JSON round trip used to turn into strings for you. If the page now fails with [object Date] cannot be serialized, continue with the Date serialisation fix for getServerSideProps. It is a common next step after moving a Mongoose or SQL query out of an API route and into the page.
Further reading: Fix: A Route is only ever to be used as the child of Routes. Also see Access to fetch at accounts.google.com blocked by CORS: fix, Next.js 'URL is malformed' error: use only absolute URLs.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace тАФ we check it against every fix and tool on the site.
Continue Reading
Next.js 'URL is malformed' error: use only absolute URLs
Reproduced on Next.js 16.4.2: the relative redirect that throws, the absolute redirect that then loops on itself, and the basePath case where new URL quietly drops the prefix.
Fix: Next.js proxy.js matcher not working for static assets
Without a matcher, proxy.js runs on every request including _next/static and public assets. And Server Functions are not separate routes тАФ a matcher gap silently drops auth coverage.
Next.js app/api 404 vs 405: route.js and named exports
Reproduced on Next.js 16.4.2: why app/api/admin/all.js answers 404, what a 405 means instead, and the static export case that only 404s in production.


