Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
9 min read

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:

text
Error: only absolute urls are supported

The 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:

text
### 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 URL

In 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):

text
тип 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:

text
### isomorphic-unfetch 1.0.0 (June 2017 era) on Node 22: global fetch hidden, relative URL
Error: only absolute urls are supported

Later 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():

text
### node-fetch 3.x called with a relative URL
TypeError: Invalid URL | code: ERR_INVALID_URL

isomorphic-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:

text
### isomorphic-unfetch 3 on Node 22 with a relative URL
TypeError: Failed to parse URL from /api/products

Fix#

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:

js
// 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:

js
// 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.

jsx
// 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.

jsx
// 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:

jsx
// 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 Products

One 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:

bash
npx next build
npx next start -p 3000
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/products

That 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:

text
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:

text
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 URL

Swap in a hardcoded absolute URL and the error changes, but the build still fails. Nothing is listening on port 3000 while next build runs:

text
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:3000

Reading 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

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace тАФ we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet тАФ never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev тАФ Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote