Skip to content

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 #885

@eashish93

Description

@eashish93

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'],
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions