Don't Unsubscribe Users on GET Requests

Posted by Michael S. on February 16, 2026

This afternoon, a tweet showed up on my X feed from Aiden Bai:

"We had this insane P0 bug where users would get randomly logged out. We had a bunch of users churn... turns out Next.js was prefetching the /logout page."

Mike Coutermarsh quote-tweeted it with his own war story:

"I learned this lesson once with an unsubscribe link that worked on GET. Gmail prefetches links. Everyone got unsubscribed."

A reply in the thread explained the underlying issue: this is essentially a CSRF (Cross-Site Request Forgery) problem. GET requests can be triggered unintentionally—by prefetching, link previews, crawlers, or even an <img> tag. Any endpoint that modifies state should require POST.

I read that and immediately thought: wait, how does my newsletter handle unsubscribes?

The Bug

Sure enough, my newsletter service accepted both GET and POST on the unsubscribe endpoint—and immediately removed the subscriber on either one.

// GET+POST /api/newsletter/unsubscribe/:token
if ((method === 'GET' || method === 'POST') && unsubMatch) {
    return handleUnsubscribe(req, res, token);
}

If Gmail (or any email client) prefetches that link when displaying the email preview, the user gets unsubscribed without ever clicking anything.

The Fix

Simple: GET shows a confirmation page with a button. POST performs the actual unsubscribe.

// GET: Show confirmation page (safe from prefetching)
if (method === 'GET') {
    const template = loadTemplate('unsubscribe-confirm.html');
    return sendHtml(res, 200, template);
}

// POST: Actually perform the unsubscribe
// ... delete subscriber, return success page

Now email clients can prefetch all they want. They'll just get a harmless HTML page asking "Are you sure?"

The Rule

GET requests should never have side effects. This is HTTP 101, but it's easy to forget when you're building something quickly.

Things that should be POST (or PUT/DELETE), not GET:

  • Logout
  • Unsubscribe
  • Delete anything
  • Any "confirm" or "approve" action

Browsers, crawlers, and email clients all assume GET requests are safe to prefetch, preload, or cache. If your GET endpoint deletes data, you're going to have a bad time.


Thanks to X's algorithm for surfacing that tweet on my feed. Fixed and deployed in under 10 minutes.

Enjoyed this post?

Get notified when I publish something new. No spam, unsubscribe anytime.