Skip to content

/latest/feed.xml returns infinite redirect loop #18299

@myelinated-wackerow

Description

@myelinated-wackerow

Describe the bug

The RSS feed route added in #18146 returns an infinite redirect loop on every locale and trailing-slash variant. Visiting /latest/feed.xml ping-pongs between Netlify's edge and Next.js middleware until the browser aborts with ERR_TOO_MANY_REDIRECTS.

To reproduce

  1. Open https://dev.ethereum.org/latest/feed.xml in any browser
  2. Observe "redirected you too many times" error

Or via curl:

curl -sI -L --max-redirs 10 https://dev.ethereum.org/latest/feed.xml

The same loop occurs on /en/latest/feed.xml, /latest/feed.xml/, and /<other-locale>/latest/feed.xml.

Expected behavior

The URL should serve the RSS feed XML with Content-Type: application/rss+xml on the first or second hop.

Root cause

trailingSlash: true in next.config.js combined with an extension-bearing route path (feed.xml) puts three layers in conflict:

  1. Netlify's @netlify/plugin-nextjs-generated redirect rule adds a trailing slash to extension paths: /en/latest/feed.xml -> 301 -> /latest/feed.xml/
  2. next-intl middleware internally rewrites the unprefixed path to add the default locale: x-middleware-rewrite: /en/latest/feed.xml/
  3. Next.js's framework-level trailing-slash logic has an extension-exception and strips the trailing slash from the rewritten path, emitting 308 Location: /en/latest/feed.xml (no slash)
  4. Back to step 1. Forever.

Trace (relevant headers from a fresh curl against dev.ethereum.org):

GET /latest/feed.xml         -> 308  location: /en/latest/feed.xml (middleware)
                                     x-middleware-rewrite: /en/latest/feed.xml/
GET /en/latest/feed.xml      -> 301  location: /latest/feed.xml/   (Netlify edge)
GET /latest/feed.xml/        -> 308  location: /en/latest/feed.xml (middleware again)
                              (loop)

Recommended fix

Rename the route directory so the URL no longer has a file extension:

app/[locale]/latest/feed.xml/route.ts  ->  app/[locale]/latest/feed/route.ts

Resulting URL: /latest/feed/ (trailing-slash, no extension). Netlify and Next.js agree on this canonical form, so the loop is impossible.

Touch list:

  • Directory rename in App Router
  • Drop the .replace(/\/$/, "") slash-stripping hack on feedUrl inside the route handler -- getFullUrl now produces the correct shape directly
  • Update the metadata alternate in app/[locale]/latest/page.tsx to the new URL
  • Remove the second matcher in proxy.ts ("/:locale?/latest/feed.xml") -- it becomes dead code once the URL has no extension, since the first matcher already includes non-extension paths
  • Add a permanent redirect in next.config.js from /latest/feed.xml -> /latest/feed/ to preserve any external links to the old URL

Tradeoff: losing the .xml extension

feed.xml is the established convention for RSS endpoints across the codebase's referenced external feeds (Vitalik, Solidity, ethpandaops, the old blog.ethereum.org/en/feed.xml). Renaming to /feed/ is a visible departure from that convention.

In practice this doesn't break anything:

  • Feed readers route by Content-Type: application/rss+xml, not URL suffix
  • HTML feed discovery uses <link rel="alternate" type="application/rss+xml" href="...">, not extension
  • Major platforms split both ways: Substack uses /feed, Hashnode uses /rss.xml, Medium uses /feed

The optional permanent redirect from /latest/feed.xml -> /latest/feed/ covers any external links that were already shared with the old URL.

Alternatives considered

These were investigated and considered less desirable than the rename:

  • Skip middleware for .xml paths via the proxy.ts matcher. Doesn't fix the loop on its own -- Netlify's edge redirect (step 1) is the trailing-slash adder; middleware exclusion alone leaves the same cycle, just half-decoupled.
  • Custom netlify.toml redirects to override the plugin's auto-redirect. Brittle: the plugin regenerates redirect rules on every build, ordering matters, and the override would need maintenance every time @netlify/plugin-nextjs changes its output.
  • Custom next.config.js rewrite/redirect for /latest/feed.xml. Runs after the Netlify edge redirect in the request lifecycle, so the loop still triggers before the rewrite gets a chance.
  • Disable trailingSlash: true site-wide. Site-wide regression across thousands of URLs; not justified by this one route.
  • Pre-render feed.xml as a static asset in public/ via a build script. Loses the App Router route pattern, requires per-locale generation logic outside getBlogPostsData, and creates a second source of truth.

Acceptance criteria

  • curl -sI https://dev.ethereum.org/latest/feed/ returns 200 OK with Content-Type: application/rss+xml; charset=utf-8 on the first hop (or one clean redirect from the locale-prefixed variant)
  • curl -sI https://dev.ethereum.org/en/latest/feed/ returns one 301 to /latest/feed/, no loop
  • curl -sI https://dev.ethereum.org/es/latest/feed/ returns the Spanish-locale feed directly with 200 OK
  • Feed validates as RSS 2.0 in a feed validator (e.g. W3C Feed Validation Service)
  • HTML page at /latest/ still includes <link rel="alternate" type="application/rss+xml" href="..."> pointing at the new URL
  • If the optional redirect is added: curl -sI https://dev.ethereum.org/latest/feed.xml returns one 301 to /latest/feed/

Out of scope

  • Migrating to a different feed format (Atom, JSON Feed) -- keep RSS 2.0
  • Adding <media:content> or <enclosure> for post images -- separate follow-up
  • Changing trailing-slash behavior site-wide

Want to contribute?

  • Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐛Something isn't workingneeds triage 📥This issue needs triaged before being worked on

    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