5 min read

My RSS Feed Problem (GoToSocial)

A valid GoToSocial RSS feed returned no posts because stale account metadata left `last_status_at` empty. One database fix brought the feed back to life.

We expected the problem to be boring.

A small ActivityPub instance was up, the profile was public, the account had posts, and the RSS option was enabled in the settings. The URL was exactly where it should have been: the profile path followed by feed.rss. A feed reader should have seen it, fetched it, and moved on with its quiet little life.

Instead, the feed existed but had no posts.

At first glance, everything looked normal. The endpoint returned 200 OK. The content type was application/rss+xml. The XML was valid. The channel had a title, a link, a description, a build date, and even the profile image. But there were no <item> entries. Not one. The feed was technically alive, but empty.

That is the kind of bug that wastes time because it does not fail loudly. A 500 error gives you a direction. A 404 tells you the route is missing. Invalid XML gives you a parser problem. But a valid RSS feed with no items politely tells you: “There is nothing to see here.” And then you have to prove that it is lying.

The first suspects

The obvious place to look was the reverse proxy.

The instance was behind Nginx, and feeds can be sensitive to headers, redirects, hostnames, and content negotiation. So we checked the public endpoint first, then bypassed the proxy and hit the service locally with the same Host and forwarded headers. The result was the same in both places. That ruled out Nginx pretty quickly.

Next we looked at the RSS setting itself. The feed was enabled in the user interface, and when enabled it returned a valid RSS document. But after toggling the setting off and on again, the feed briefly disappeared entirely and returned an HTML 404 page. That was the first useful clue: the checkbox in the interface and the value in the database were not always telling the same story.

When the setting was disabled in the database, the feed route returned 404. When it was enabled, the feed came back as RSS. So the route was not broken. The feed feature was not missing. It was being gated by the account setting exactly as expected.

But even with RSS enabled, the feed was still empty.

The visibility rabbit hole

The next theory was visibility.

GoToSocial has a few different visibility levels, and its defaults are not always what people coming from other Fediverse software expect. We wondered whether the posts were not truly public. Maybe they were “unlisted” or “unlocked,” visible on the web profile but excluded from RSS.

That seemed plausible for a while. The public profile showed the posts, the account settings allowed public and unlisted posts to appear, and the RSS documentation focuses on public posts. It would have been a clean explanation: the posts were visible, but not eligible.

So we checked the database.

The statuses were local. They were federated. They were not replies. They were not boosts. They were not pending approval. They had normal text content. Their visibility value matched the public visibility enum for that version of GoToSocial. In other words, the posts were not the problem. They were exactly the kind of posts the RSS feed should include.

That theory died there.

Trying versions, chasing behavior

We also pinned the GoToSocial image instead of relying on a moving tag. The instance had been running a recent 0.21.x build, and we tried to remove uncertainty by using a specific version. The behavior did not change.

That mattered. It meant we were not just fighting a bad container pull or a half-updated image. The problem survived restarts and version pinning within the same release line. We were dealing either with persistent state in the database or a behavior in the application that depended on that state.

At this point the RSS endpoint was doing something very specific:

  • If RSS was disabled for the account, the route returned 404.
  • If RSS was enabled, the route returned valid RSS.
  • The feed included channel metadata.
  • The feed did not include any posts.
  • The public web profile could still find and render the posts.
  • The database query for obvious RSS-eligible posts returned all of them.

That narrowed the field. The posts existed. The web view could query them. The RSS route was active. Something between “account has posts” and “RSS should render items” was deciding that the account had no eligible latest status.

The clue hidden in account stats

The real clue was not in the statuses table.

It was in the account statistics table.

The account had a nonzero status count. The posts existed. But last_status_at was NULL.

That was the contradiction:

statuses_count: several posts
last_status_at: NULL

For an account with public posts, that should not happen.

Once we saw that, the empty RSS feed made sense. The RSS generator appeared to rely on account-level cached/statistical metadata to decide whether there was a recent eligible status for the feed. Since last_status_at was empty, the feed could be generated as a valid channel with no items, even though the posts themselves were perfectly eligible.

The public web profile did not suffer from the same problem because it queried the statuses directly. That explained why the web profile worked while RSS did not. They were not failing through the same code path.

The fix

The fix was deliberately small.

We backed up the SQLite database. Then we updated last_status_at for the account to the created_at value of the newest eligible public status. After restarting the service, the RSS feed immediately populated with all the expected items.

No proxy change. No rewrite rule. No client workaround. No fake feed. No downgrade.

The feed went from a valid-but-empty XML document to a normal RSS feed with every public post listed.

After that, we published more test posts. They appeared in the RSS feed as well. The channel dates also started making sense once last_status_at was corrected.

So, was it a bug?

In practical terms, yes.

Whether it is best described as a GoToSocial bug, a migration edge case, an account stats initialization issue, or stale derived state, the result was the same: account statistics said the account had posts, but also said it had no last status. RSS trusted that derived state and produced an empty feed.

The important part is that the underlying posts were fine. The RSS feature was fine. The URL was fine. The proxy was fine. The broken piece was cached account metadata.

That is also why the problem was so slippery. Every normal check passed. The profile loaded. The posts were public. The feed endpoint responded. The XML validated. The database contained the posts. Nothing looked catastrophically wrong.

Only one field was wrong, and it was not in the first table we looked at.

In GoToSocial, the public profile and RSS feed may use different internal paths and different assumptions. If the profile shows posts but RSS is empty, checking only the statuses is not enough.

Its that account-level derived data matters. Fields like statuses_count and last_status_at are not just decorative. They can influence what other parts of the application think exists.

A 200 OK response with valid XML can still be wrong. Sometimes the bug is not that the endpoint failed. Sometimes the bug is that it succeeded with the wrong idea of reality.

In our case, the fix took one small database correction. Finding that correction took a few hours of chasing ghosts: headers, visibility, settings, version tags, profile behavior, API behavior, and finally the stats row that gave the whole thing away.

Reply

Got a thought? Reply by email — or publish a response on your own site and it'll show up above via Webmention.

Reply by email