[GH-ISSUE #885] next/font/google shim hardcodes :wght@100..900 when no weight is specified, causing HTTP 400 from Google Fonts for fonts with a narrower weight range #198

Closed
opened 2026-05-06 12:38:04 +02:00 by BreizhHardware · 0 comments

Originally created by @eashish93 on GitHub (Apr 24, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/885

Summary

When a caller uses next/font/google without passing an explicit weight option, the vinext shim builds the Google Fonts URL with :wght@100..900. Many Google Fonts don't support that full range (e.g. Sen is 400–800), so Google Fonts CDN responds with HTTP 400 and the stylesheet never loads. The rendered page computes font-family: 'Sen', sans-serif correctly, but because no @font-face registers, the browser paints the generic sans-serif fallback — so the bug looks like "font-family cascade issue" even though the real fault is the URL.

Reproduction

  1. In app/layout.tsx:
    import { Sen } from 'next/font/google';
    const appFont = Sen({ subsets: ['latin'] });
    
  2. Load the page and check the head — vinext emits:
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sen:wght@100..900&display=swap" />
    
  3. curl -I that URL:
    HTTP/2 400
    content-type: text/html; charset=utf-8
    content-length: 0
    
  4. Compare with Sen's actual supported range:
    $ curl -I 'https://fonts.googleapis.com/css2?family=Sen:wght@400..800&display=swap'
    HTTP/2 200
    content-type: text/css; charset=utf-8
    
    $ curl -I 'https://fonts.googleapis.com/css2?family=Sen&display=swap'
    HTTP/2 200
    content-type: text/css; charset=utf-8
    

Root cause

dist/shims/font-google-base.js:80-95:

function buildGoogleFontsUrl(family, options) {
  const params = new URLSearchParams();
  let spec = family;
  const weights = options.weight ? ;
  const styles  = options.style  ? ;
  if (weights.length > 0 || styles.length > 0) {
    
  } else spec += `:wght@100..900`;   // ← always-wrong default
  params.set("family", spec);
  params.set("display", options.display ?? "swap");
  return `https://fonts.googleapis.com/css2?${params.toString()}`;
}

The hardcoded :wght@100..900 assumes every Google font is a 100–900 variable axis. Many aren't:

  • Sen → 400–800
  • Archivo Black, Anton, Fjalla One, Russo One, and many display fonts → single static weight (400)
  • Many classical fonts → 300–700 or 400–900 only

Next.js real implementation looks up each family's axis metadata from a bundled fonts database and either fetches with no :wght@ (which makes Google serve the default variable font) or with the exact valid range. vinext's shim can't do the former because it always appends a :wght@ segment.

Suggested fix

The minimal, safe fix is to omit :wght@… when no weight is provided. Google Fonts CDN handles the no-weight case correctly and returns the variable font (or the default weight for static fonts):

- } else spec += `:wght@100..900`;
+ }

A more Next.js-faithful fix would be to bundle a slimmed-down axis database (hexere already has a similar google-fonts-meta.json for its own use, ~500 KB at most) and either (a) validate the requested weight range against the font's real axis, or (b) auto-select a full range when the caller passes no weight.

Impact

Every call site that uses next/font/google without an explicit weight option and happens to pick a font whose axis range isn't exactly 100–900 silently loads nothing. The failure mode is invisible in document.fonts (entries are registered by <link>-sourced @font-face, and a 400-response stylesheet registers no faces), which makes this bug very hard to diagnose — devs typically spend hours staring at Tailwind preflight cascade rules before checking the CDN response.

Related: this is one of three shim-level issues I hit while porting a Next.js app to vinext — see also:

  • HMR state accumulation in the same shim (separate issue)
  • Metadata file URL extension stripping (separate issue)

Workaround

Pass explicit weights that match the font's real axis range, e.g.:

const appFont = Sen({
  subsets: ['latin'],
  weight: ['400', '500', '600', '700', '800'],
});
Originally created by @eashish93 on GitHub (Apr 24, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/885 ## Summary When a caller uses `next/font/google` without passing an explicit `weight` option, the vinext shim builds the Google Fonts URL with `:wght@100..900`. Many Google Fonts don't support that full range (e.g. **Sen** is 400–800), so Google Fonts CDN responds with **HTTP 400** and the stylesheet never loads. The rendered page computes `font-family: 'Sen', sans-serif` correctly, but because no `@font-face` registers, the browser paints the generic `sans-serif` fallback — so the bug looks like "font-family cascade issue" even though the real fault is the URL. ## Reproduction 1. In `app/layout.tsx`: ```ts import { Sen } from 'next/font/google'; const appFont = Sen({ subsets: ['latin'] }); ``` 2. Load the page and check the head — vinext emits: ```html <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sen:wght@100..900&display=swap" /> ``` 3. `curl -I` that URL: ``` HTTP/2 400 content-type: text/html; charset=utf-8 content-length: 0 ``` 4. Compare with Sen's actual supported range: ``` $ curl -I 'https://fonts.googleapis.com/css2?family=Sen:wght@400..800&display=swap' HTTP/2 200 content-type: text/css; charset=utf-8 $ curl -I 'https://fonts.googleapis.com/css2?family=Sen&display=swap' HTTP/2 200 content-type: text/css; charset=utf-8 ``` ## Root cause `dist/shims/font-google-base.js:80-95`: ```js function buildGoogleFontsUrl(family, options) { const params = new URLSearchParams(); let spec = family; const weights = options.weight ? …; const styles = options.style ? …; if (weights.length > 0 || styles.length > 0) { … } else spec += `:wght@100..900`; // ← always-wrong default params.set("family", spec); params.set("display", options.display ?? "swap"); return `https://fonts.googleapis.com/css2?${params.toString()}`; } ``` The hardcoded `:wght@100..900` assumes every Google font is a 100–900 variable axis. Many aren't: - **Sen** → 400–800 - **Archivo Black**, **Anton**, **Fjalla One**, **Russo One**, and many display fonts → single static weight (400) - Many classical fonts → 300–700 or 400–900 only Next.js real implementation looks up each family's axis metadata from a bundled fonts database and either fetches with no `:wght@` (which makes Google serve the default variable font) or with the exact valid range. vinext's shim can't do the former because it always appends a `:wght@` segment. ## Suggested fix The minimal, safe fix is to **omit** `:wght@…` when no `weight` is provided. Google Fonts CDN handles the no-weight case correctly and returns the variable font (or the default weight for static fonts): ```diff - } else spec += `:wght@100..900`; + } ``` A more Next.js-faithful fix would be to bundle a slimmed-down axis database (hexere already has a similar `google-fonts-meta.json` for its own use, ~500 KB at most) and either (a) validate the requested weight range against the font's real axis, or (b) auto-select a full range when the caller passes no weight. ## Impact Every call site that uses `next/font/google` without an explicit `weight` option and happens to pick a font whose axis range isn't exactly 100–900 silently loads nothing. The failure mode is invisible in `document.fonts` (entries are registered by `<link>`-sourced `@font-face`, and a 400-response stylesheet registers no faces), which makes this bug very hard to diagnose — devs typically spend hours staring at Tailwind preflight cascade rules before checking the CDN response. Related: this is one of three shim-level issues I hit while porting a Next.js app to vinext — see also: - HMR state accumulation in the same shim (separate issue) - Metadata file URL extension stripping (separate issue) ## Workaround Pass explicit weights that match the font's real axis range, e.g.: ```ts const appFont = Sen({ subsets: ['latin'], weight: ['400', '500', '600', '700', '800'], }); ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#198
No description provided.