A Next.js 16 build that imports a font from next/font/google fails intermittently with Error while looking up import map: next/font/google queries have exactly one entry under Turbopack, or TypeError: Cannot read properties of null (reading '1') under webpack. Both strings have one cause: Google Fonts occasionally answers the build's CSS request with a font URL that carries no file extension, such as https://fonts.gstatic.com/l/font?kit=UcCO.... No release fixes it yet, so this walkthrough makes the failure reproducible on demand, then takes the font fetch out of the build entirely.

Prerequisites

  • Next.js 16.2.12, 16.3.6 or 16.4.0. Those are the versions named in vercel/next.js#99114, which was still open on 9 October 2026 with both candidate fixes unmerged. Turbopack is the default bundler for next dev and next build in Next.js 16; --webpack opts out.
  • Node.js 20 or newer, plus curl, awk and grep.
  • A project with a Google font imported in app/layout.tsx or a fonts module.
  • Write access to the repo. The fix is a code change, not a config flag.

Step-by-step

1. Which error did you get, and which bundler produced it?

The two messages come from two separate implementations of the same loader. Match yours before changing anything:

What the build printsBundlerEnforcing artifactWhat it means
TypeError: Cannot read properties of null (reading '1') in next/dist/compiled/@next/font/dist/google/loader.jswebpack, via next build --webpackpackages/font/src/google/loader.tsthe extension regex matched nothing and the result was dereferenced anyway
Error while looking up import map: next/font/google queries have exactly one entryTurbopack, the Next.js 16 defaultfont_file_options_from_query_map in crates/next-core/src/next_font/google/mod.rsthe serialized font options split into several query entries
font url ... is missing an extensionTurbopackNextFontGoogleFontFileReplacerthe failure waiting one line further down the same path

The webpack loader runs /\.(woff|woff2|eot|ttf|otf)$/.exec(googleFontFileUrl)![1]. That pattern is anchored to the end of the string, so a URL carrying a query string never matches, and the non-null assertion converts the miss into a TypeError.

The Turbopack side fails earlier and more strangely. In next_font/google/mod.rs, update_google_stylesheet serializes the font options to JSON, then feeds that JSON to qstring to build the query of an internal import:

let query_str = qstring::QString::from(serde_json::to_string(&query)?.as_str());

qstring parses on &. A URL such as /l/font?kit=UcCO...&skey=...&v=v20 contains ampersands, so one JSON blob becomes several query pairs, and the consumer refuses it:

if query_map.len() != 1 {
    bail!("next/font/google queries have exactly one entry");
}

The import-map prefix comes from the replacer that resolves the virtual font import. The message therefore reads like a resolver or next.config.js problem while the fault sits in a URL returned by a third party.

next/font/google call in app/layout.tsxBuild fetches fonts.googleapis.com/css2 with a Chrome 104 user agentDoes src: url end in .woff2 ?Font emitted to .next/static/media, build passesWhich bundler is running?loader.ts regex returns nullTypeError: Cannot read properties of null reading 1qstring splits the JSON on &next/font/google queries have exactly one entryyes"no: /l/font?kit=...""webpack""Turbopack, default"

Bundler-specific crash paths are routine in this toolchain: the webpack 5.111 top-level await fix skips async cycles in the same way one implementation gets a patch and the other keeps the bug. Until you can trigger this one on demand it is indistinguishable from CI noise, and the first green re-run closes the ticket. An npm ci that exits 0 and leaves build/Release missing gets filed the same way.

2. Confirm Google is serving you the extensionless shape

The build sends a fixed user agent so Google answers with woff2. Reproduce that request directly:

UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
for i in $(seq 1 120); do
  curl -s -A "$UA" 'https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'
done | grep -o 'url(https://fonts.gstatic.com/[^)]*)' | sort | uniq -c | sort -rn | head

Healthy responses are all /s/inter/v20/....woff2. Any line containing /l/font?kit= is the poisoned shape. The reporter of #99114 measured roughly 1 response in 60, which is the frequency that trains a team to re-run the job and stop thinking about it.

3. Make the failure deterministic with the mock loader

next/font reads an environment variable for its own test suite that the public docs do not mention: NEXT_FONT_GOOGLE_MOCKED_RESPONSES. It points at a CommonJS module exporting a map of request URL to CSS body, and both bundlers honour it, since the Rust path reads the same variable.

Learn the exact URL your font call requests first. Point the variable at an empty map and let the build tell you:

echo 'module.exports = {}' > /tmp/fontmock.js
NEXT_FONT_GOOGLE_MOCKED_RESPONSES=/tmp/fontmock.js npx next build --webpack

The build stops with Missing mocked response for URL: https://fonts.googleapis.com/css2?family=Inter:wght@...&display=swap. Copy that URL verbatim. Hand-guessing the axes formatting is the step that costs an afternoon.

Now key a mock on it, with the extensionless URL in src:

// /tmp/fontmock.js  (use the URL the previous command printed)
module.exports = {
  'https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap': `
/* latin */
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/l/font?kit=UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hiA&skey=a0a0a0a0a0a0a0a0&v=v20) format('woff2');
  unicode-range: U+0000-00FF;
}`,
}

Run it against both bundlers:

NEXT_FONT_GOOGLE_MOCKED_RESPONSES=/tmp/fontmock.js npx next build --webpack
NEXT_FONT_GOOGLE_MOCKED_RESPONSES=/tmp/fontmock.js npx next build

Both error strings now appear on demand. Keep this file in the repo: it is the regression test for whichever upstream PR eventually lands.

4. Pull the real font files down once

Capture the CSS and pair every subset with its URL. The subset name sits in a comment on its own line above each @font-face:

curl -s -A "$UA" 'https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap' -o /tmp/inter.css
awk '/^\/\* /{s=$2} /src: url\(/{match($0,/https:[^)]*/); print s, substr($0,RSTART,RLENGTH)}' /tmp/inter.css

Download the slices your app actually renders. For a Latin-only interface:

mkdir -p app/fonts
curl -sL -o app/fonts/Inter-latin.woff2 'https://fonts.gstatic.com/s/inter/v20/<the-latin-url-from-above>.woff2'

Commit the woff2. Google's /s/ URLs are versioned per font release, so the file you commit is the file your build ships, every time.

5. Swap the import to next/font/local

next/font/local takes the same options and emits the same @font-face machinery without the fetch. Copy font-weight and unicode-range out of the @font-face block you downloaded; do not retype them from memory:

// app/fonts.ts
import localFont from 'next/font/local'

export const inter = localFont({
  src: './fonts/Inter-latin.woff2',
  weight: '100 900',
  style: 'normal',
  display: 'swap',
  variable: '--font-inter',
  adjustFontFallback: 'Arial',
  declarations: [{ prop: 'unicode-range', value: 'U+0000-00FF' }],
})

Per the font API reference, src paths resolve relative to the file that calls the loader, declarations accepts any @font-face descriptor, and adjustFontFallback for local fonts takes 'Arial', 'Times New Roman' or false. Then use it as before:

// app/layout.tsx
import { inter } from './fonts'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (<html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>)
}

6. Keep the fetch from coming back

One reintroduced import restores the flake. Add a gate that runs before the build:

# scripts/no-build-time-fonts.sh
if grep -rn "next/font/google" app src 2>/dev/null; then
  echo "next/font/google fetches at build time; see vercel/next.js#99114" >&2
  exit 1
fi

Verify it works

Build with the default bundler and with webpack, then confirm the fonts were emitted locally:

npx next build && npx next build --webpack
ls .next/static/media | grep -i woff2

Then prove the build no longer depends on Google. Point the proxy variables at a closed port so any outbound HTTP fails:

NEXT_TELEMETRY_DISABLED=1 \
  http_proxy=http://127.0.0.1:9 https_proxy=http://127.0.0.1:9 \
  npx next build

A build that completes has no build-time font fetch left in it. Run the same command on the commit before this change and it fails while resolving fonts.googleapis.com, which is the dependency you just removed. Last check, against the rendered output:

grep -r 'fonts.gstatic.com' .next/server/app | head

Expected result: no matches.

Common pitfalls

Re-running the job appears to fix it. It does, about 59 times in 60. A green re-run is evidence of the random response, not of a fixed build.

Upgrading does not help. #99114 reports the same failure on 16.2.12, 16.3.6 and 16.4.0-canary.40, and both candidate patches (#99132 and #99308) are open, so there is no version to move to today.

Patching node_modules covers half the bug. The webpack loader is JavaScript and can be patched; the Turbopack implementation is compiled into the Rust binary and cannot. With Turbopack as the Next.js 16 default, a patch to loader.js leaves your real build path broken. If you do patch for a webpack build, note that the tooling can hide the underlying error on npm 12, covered in patch-package "status: 1" on npm 12.

A font proxy or mirror makes this constant. With the webpack regex anchored at $ and qstring splitting on &, any rewritten gstatic URL carrying query parameters fails 100% of the time. PR #99308 names proxy URLs explicitly. A rewriting intermediary breaking a tool's URL assumptions is the same trap as npm EALLOWREMOTE on a private registry.

NEXT_FONT_GOOGLE_MOCKED_RESPONSES needs an absolute path. The webpack path loads it with bare require(), so a relative path resolves against Next.js's own module directory and throws a module-not-found error that looks unrelated to fonts.

Downloading one slice silently drops glyphs. Google returns one @font-face per subset, each with its own unicode-range. Ship only the Latin slice and Cyrillic or Vietnamese text falls back to Arial with no error anywhere. Call localFont once per slice you need, each with its own unicode-range declaration.

Setting font-family in declarations renames the family. The docs are explicit about this: keep the value equal to the exported constant, or className and the CSS variable stop pointing at the same family.

FAQ

Is Error while looking up import map ever a real import-map problem? Not here. The string is the wrapper Turbopack's ImportMappingReplacement puts around any failure while resolving the internal font import. The text after the colon is the real error.

Does deleting .next fix it? No. The failure happens while reading a fresh HTTP response, so a cold cache re-rolls the dice.

Can I keep next/font/google and retry the build on failure? A retry loop mostly works. It also leaves every build one unlucky response away from red, and a build that depends on a third-party response cannot be reproduced later.

Will the merged fix make this walkthrough obsolete? Both open PRs fall back to the format('woff2') hint on the same src line, so next/font/google will stop crashing when one lands. I treat a build-time fetch of a third-party asset as a dependency that will eventually be down or wrong, and that is reason enough to make this change anyway.

Does this affect Pages Router projects? Yes. The loader is shared; only the import site differs.

Wrap-up

You end up with a reproduction you can run offline, a build with no third-party font fetch in it, and a gate that fails the pipeline if someone adds one back. Keep /tmp/fontmock.js in the repo as a fixture and subscribe to #99114 so the workaround gets removed deliberately rather than forgotten. The next scheduled break for this same toolchain arrives on 28 October, when node:lts and lts/* move to Node 26 with no commit of yours involved: Node 26 LTS lands Oct 28.