Next.js

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.

Mahdi Ben RhoumaMahdi Ben Rhouma
7 min read

TL;DR#

NextResponse.redirect() and NextResponse.rewrite() only accept an absolute URL. A bare path such as '/about' fails the new URL() check inside Next.js and throws URL is malformed "/about". Please use only absolute URLs. Build the destination from the incoming request instead: request.nextUrl.clone() with a new pathname, or new URL('/about', request.url). Then make sure the middleware does not run on the destination itself, or the absolute redirect turns into an endless redirect loop. That second failure is the one the original Stack Overflow question ran into after typing the full http://localhost:3000/about.

The error#

The question's _middleware.ts file redirects every request:

ts
import { NextResponse } from 'next/server';
import type { NextFetchEvent, NextRequest } from 'next/server';
 
export function middleware(request: NextRequest, ev: NextFetchEvent) {
  return NextResponse.redirect('/about')
}

The question quotes the error as Error: URLs is malformed. Please use only absolute URLs -. Current releases word it a little differently and include the offending value. With the same redirect in a proxy.ts file on Next.js 16.4.2 (Turbopack dev server, Node.js v24.21.0), requesting / returned HTTP 500 and the dev server logged this (code frames trimmed):

text
⨯ Error: URL is malformed "/about". Please use only absolute URLs - https://nextjs.org/docs/messages/middleware-relative-urls
    at proxy (proxy.ts:5:23)
  [cause]: TypeError: Invalid URL
    code: 'ERR_INVALID_URL',
    input: '/about'

The whole page fails, not just the redirect: curl received a 500 and the dev server went on to compile its error page. If you search for either wording, this is the same check. The link in the message still points to /docs/messages/middleware-relative-urls. That URL now answers with a 308 redirect to Proxy Relative URLs. The file convention itself was renamed from middleware to proxy in Next.js 16 (more on that below).

This is not the same problem as Error: only absolute urls are supported. That one comes from a server-side fetch('/api/...') in data-fetching code, not from middleware, and has its own page: only absolute urls are supported.

Why it happens#

NextResponse.redirect puts the destination in the Location header, and NextResponse.rewrite puts it in an internal x-middleware-rewrite header. Both pass it through validateURL in packages/next/src/server/web/utils.ts first. That function is a try around String(new URL(String(url))). The WHATWG URL constructor has no base to resolve /about against, so it throws ERR_INVALID_URL. Next.js catches it and rethrows the message above, keeping the original TypeError as cause. The compiled file in the published package has the same throw: node_modules/next/dist/server/web/utils.js line 129 in 16.4.2.

The Proxy Relative URLs page gives the reason: "Prior to Next.js 12.1, we allowed passing relative URLs. However, constructing a request with new Request(url) or running fetch(url) when url is a relative URL does not work." Relative destinations were deprecated and then removed for consistency. This also explains why the first answer on the question works: it calls NextResponse.rewrite with a template string that starts with origin from req.nextUrl, so the value reaching validateURL is already absolute. Switching from redirect to rewrite changes nothing by itself; the origin prefix is the fix.

Calling the helpers outside any server shows the check directly. On Next.js 16.4.2:

text
NextResponse.redirect("/about") -> throws Error: URL is malformed "/about". Please use only absolute URLs - https://nextjs.org/docs/messages/middleware-relative-urls
NextResponse.rewrite("/about") -> throws Error: URL is malformed "/about". Please use only absolute URLs - https://nextjs.org/docs/messages/middleware-relative-urls
Response.redirect("/about") -> throws TypeError: Failed to parse URL from /about
NextResponse.redirect(new URL("/about", "http://localhost:3000/")) -> 307 http://localhost:3000/about

The standard Response.redirect has the same constraint. It only gives a different message.

Fix#

1. Build an absolute destination from the request#

The docs recommend cloning request.nextUrl and changing its path:

ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  const url = request.nextUrl.clone()
  url.pathname = '/about'
  return NextResponse.redirect(url)
}

The shorter form uses the incoming URL as the base, as in the second answer on the question:

ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/about', request.url))
}

Never hard-code http://localhost:3000. It is valid, so the error disappears, but it sends production users to localhost.

In a project that still uses middleware.ts and a middleware function, the fix is the same. The proxy.js reference lists the change under v16.0.0: "Middleware is deprecated and renamed to Proxy". The rename itself, and the middleware-to-proxy codemod that does it for you, are covered in the proxy.js matcher migration notes.

2. Keep the middleware off the page you redirect to#

This is what the question describes next: with an absolute URL, "the browser keeps executing the request several times". The redirect is now valid, but the middleware has no matcher, so it also runs on /about and redirects /about to /about. The proxy.js reference says: "Without a matcher, Proxy runs on every request". With the new URL version above and no matcher, the reproduction gave:

text
$ curl -sS -L --max-redirs 5 -o /dev/null -w "%{http_code} %{num_redirects} %{url_effective}
" http://localhost:3000/
curl: (47) Maximum (5) redirects followed
307 5 http://localhost:3000/about

curl -sI http://localhost:3000/about returned HTTP/1.1 307 Temporary Redirect with location: /about, which is a redirect to itself. That is the repeated request the question describes: a browser keeps following the same redirect until it gives up.

Scope the middleware to the paths that should redirect:

ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function proxy(request: NextRequest) {
  return NextResponse.redirect(new URL('/about', request.url))
}
 
export const config = {
  matcher: ['/'],
}

If one middleware has to handle many routes, put the guard in code instead: return NextResponse.next() when request.nextUrl.pathname is already the destination, and redirect otherwise. Setup mistakes that stop it running at all (file location, a too-narrow matcher, an Edge-incompatible import) are covered in middleware not triggering.

Verify the fix#

With the matcher in place, the same Next.js 16.4.2 app answered with exactly one redirect and then the page:

text
$ curl -sI http://localhost:3000/
HTTP/1.1 307 Temporary Redirect
location: /about
$ curl -sS -L -o /dev/null -w "%{http_code} %{num_redirects} %{url_effective}
" http://localhost:3000/
200 1 http://localhost:3000/about

To find every string-literal call that still passes a bare path, search the middleware file and anything it imports:

bash
grep -rnE "NextResponse\.(redirect|rewrite)\(['\"]/" --include='*.ts' --include='*.js' --exclude-dir=node_modules --exclude-dir=.next .

Any line it prints passes a string that starts with / and will throw on the first request that reaches it. Then check three things in your own app. The dev server log no longer shows URL is malformed. curl -sI on the source path shows a single 307. Following redirects ends on a 200 after one hop, not a curl error 47 or a browser loop. The location header was relative (/about) in this test even though the code passed an absolute URL. The absolute requirement applies to the argument you pass to NextResponse, not to the header the browser receives.

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.

When new URL drops your basePath#

The docs add one caveat to the shorter form: using the original URL as the base "will not consider configuration like basePath or locale". The reproduction confirms it. With basePath: '/docs' in next.config.mjs, a guard on request.nextUrl.pathname === '/', and new URL('/about', request.url), a request to /docs was redirected outside the app:

text
$ curl -sI http://localhost:3000/docs
HTTP/1.1 307 Temporary Redirect
location: /about

/about returned 404, because the page lives at /docs/about. Changing only the redirect to request.nextUrl.clone() with url.pathname = '/about' kept the prefix:

text
$ curl -sI http://localhost:3000/docs
HTTP/1.1 307 Temporary Redirect
location: /docs/about
$ curl -sS -L -o /dev/null -w "%{http_code} %{num_redirects} %{url_effective}
" http://localhost:3000/docs
200 1 http://localhost:3000/docs/about

request.nextUrl.pathname is / here, not /docs: nextUrl strips the base path, and its clone puts it back. If your app uses basePath or i18n routing, use the clone form everywhere. If it uses neither, both forms behave the same.

If the redirect only depends on the path, with no cookie or header check, it may not need middleware at all. redirects() in next.config.js or redirect() in a Server Component may fit better, and the trade-offs are laid out in redirecting from / to another page. Everything above was reproduced on 22 October 2026 with Next.js 16.4.2 on Node.js v24.21.0, in a Node 24 Docker container. If the redirect now works but your stylesheets, images or _next/static chunks stop loading, the matcher is catching assets: scoping the proxy.js matcher covers the negative pattern that excludes them.

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