Skip to main content

Command Palette

Search for a command to run...

The Endpoint That Moved While I Wasn't Looking

Wiring blog publishing into a new project, I spent a few hours — and one Pro subscription — chasing a 301 redirect that turned out to be a moved API endpoint, sitting in plain sight on the dashboard the whole time.

Updated
9 min read

Everything on this blog gets here through two Claude Code slash commands. The first, /draft-post, turns a working session into a markdown file. The second, /publish-post, takes that file and pushes it to Hashnode through their GraphQL API. They are unremarkable in the way most good tooling is unremarkable — I type a command, a post appears — and I had stopped thinking about them entirely, which is usually the sign that a tool has become genuinely good.

Then I started a new side project, wanted to be able to blog from it too, and went to wire the same two commands into the new repository. The wiring itself was the boring kind of work: a gitignored file to hold the credentials, a directory for the drafts to live in, a couple of lines so the publish command knew where to read its API key from. None of it was interesting, and all of it worked. /draft-post wrote files. A read query against my own blog came back instantly with the right data. And then I tried to actually publish something, and the whole thing walked straight into a wall I would spend the rest of the afternoon trying to climb.

A 301 with no body

The publish step does one thing: it POSTs a GraphQL mutation to Hashnode's API endpoint. Instead of the JSON I expected, I got this:

HTTP/2 301
location: https://hashnode.com/announcements/graphql-api
server: cloudflare

The body was the small, anonymous Cloudflare stub that says 301 Moved Permanently and nothing else. It is worth sitting with how strange that response is, because the strangeness was a clue I did not read carefully enough at the time. A working API does not redirect you. It returns data, or it returns a 401, or it hands you a structured error object explaining what you did wrong. A 301 to a marketing announcement is the API declining to behave like an API at all — it is the front door of the building telling you the building has been replaced by a billboard. And the billboard had an address: /announcements/graphql-api. The redirect was, in effect, pointing at the reason it existed. I just read the wrong reason off it.

I paid for the wrong fix

The announcement, when I followed it, explained that Hashnode had retired free access to its GraphQL API a few weeks earlier. Every request — reads and writes alike — now required a Pro plan on the publication. Reads, the page noted almost in passing, used to be free; they were not anymore.

This fit the symptom so cleanly that it did not feel like a theory at all, it felt like a diagnosis. My request was being turned away, and here was an official page, which my request had been redirected to, explaining that requests now got turned away unless you paid. The redirect was practically holding up a sign with the answer on it. So I upgraded. I paid, I waited a few minutes for whatever needed to propagate to propagate, and I re-ran the exact same command — and the redirect came back identical to the byte.

That is the most dangerous kind of red herring there is: a symptom that lines up perfectly with a recent, loudly-announced change. It is plausible enough to spend money on. I want to be honest about the money here, because the honest version is more interesting than the self-deprecating one. The fifty dollars was not wasted — the API genuinely does require Pro now, so I would have needed it regardless of anything else. But it was also not the fix. I had been missing two different things at the same time, and buying Pro had quietly removed exactly one of them, which is the worst possible outcome, because it leaves the symptom completely unchanged and you with no way to tell that you made progress. My next move was to generate a brand-new access token from the now-paid account, on the reasonable theory that my old token predated the whole change. Same redirect. Not one byte different.

Three AIs and one blind spot

When I am properly stuck, I do a particular thing: I write down everything I have already ruled out, in as much detail as I can stand, and I hand that brief to a couple of other models. So I took the evidence to ChatGPT and to Gemini, alongside the Claude session I was already working in, and I laid it all out. The token did not matter — old and new both failed identically. The headers did not matter — I had tried it with and without an explicit Accept, with a real browser User-Agent glued on, no difference. The path did not matter — a trailing slash still redirected, a /graphql suffix timed out at the edge, a hostname I guessed at returned a flat 404. It was not my machine and not my country, because I had reproduced the same redirect from my laptop and from a second machine on another continent, landing on two different Cloudflare data centers, every single time.

The two models agreed with each other, and with me, on the shape of the thing. The endpoint was the documented one. Pro was supposed to switch the API on automatically. It was not a propagation delay. The redirect was happening at the edge, before authentication ever entered the picture. Where they split was on the cause, and the split is the part I keep thinking about. Gemini was confident and admirably specific: the breaking change, it told me, was the format of the Authorization header — Hashnode now expected a Bearer prefix, and the edge was bouncing anything that did not match the pattern. It cited integration write-ups to back the claim. ChatGPT was more cautious, noted that Hashnode's own documentation contradicted itself on whether the token wanted a Bearer prefix at all, and then made the observation that should have stopped me sooner than it did: a redirect that fires before authentication cannot possibly care what your token looks like.

I tested Gemini's fix anyway, because it was the only concrete lever anyone had handed me. Raw token, Bearer token, lowercase bearer token — three requests, three identical redirects. It was a beautifully constructed theory, with sources, and it was wrong the instant it met a curl.

The dashboard had it the whole time

I had a support email half-drafted and I was making my peace with publishing the next few posts by hand when I opened the Hashnode dashboard to copy the support address out of it. And there, on the first screen, in a panel I had apparently never once read, was a heading that said Your GraphQL API endpoint, and underneath it:

https://gql-beta.hashnode.com

Not gql.hashnode.com. gql-beta. The endpoint had moved. Every request I had made all afternoon — every request that the Claude session and ChatGPT and Gemini had so carefully reasoned about — had been aimed at a hostname Hashnode had quietly retired, and that retired hostname now does precisely one thing: it 301s everyone, paid or free, valid token or not, sensible headers or not, to the announcement explaining that the API had changed. The redirect had never been a paywall and never been an auth problem. It was a forwarding address on an old door.

One request to the new host:

curl -s -X POST https://gql-beta.hashnode.com \
  -H "Content-Type: application/json" \
  -d '{"query":"{ publication(host: \"ubeyd.dev\") { title } }"}'
{"data":{"publication":{"title":"ubeyd.dev"}}}

A flat 200, and the data sitting right there. A minute later a real publish mutation put a throwaway test post on the live site, and I opened it in the browser just to watch it exist. The thing that had eaten my afternoon, survived a paid subscription, and defeated three language models reasoning in concert was a four-character difference in a URL, displayed in a panel on the first screen of a dashboard I open every single time I publish.

What I'd been assuming

The uncomfortable part is not that I missed it. The uncomfortable part is that all four of us missed it in exactly the same way. The Claude session, ChatGPT, Gemini, and me — we reasoned carefully, and within the frame we had silently accepted we reasoned correctly. We ruled out tokens and headers and networks with real, methodical rigor. What not one of us did was interrogate the single thing the entire investigation was standing on: that the URL was still the right URL. The bug was not hiding in any of the variables we kept testing. It was sitting in the one constant we never thought to make a variable.

There is a sharper edge here for anyone who, like me, has started reaching for a second and third model when the first one gets stuck. Adding ChatGPT and Gemini did not help, and — this is the part worth internalizing — it could not have helped, because all three models shared the identical blind spot. They could multiply my reasoning, but they could not multiply my access. The single surface that held the answer was the product's own dashboard, behind my own login, and that was mine to go and look at and nobody else's. I had been treating "I am stuck" as a reasoning problem, to be solved by throwing more reasoning at it, when it was an access problem the whole time.

Three models can multiply your reasoning. They cannot multiply your access. The thing none of them can see is still yours to go and look at.

It rhymes, a little uncomfortably, with something I wrote here not long ago about an AI quietly building seed data that disagreed with production in ways no test could catch — that an AI collaborator can only ever be as correct as the ground truth you show it. This was that same lesson wearing a different hat. The ground truth was not in the documentation, which was stale. It was not in the changelog, which described the wrong half of the problem with great confidence. It was not in the agreement of three capable models, who were unanimous and unanimously working the wrong frame. It was on a panel I had never read, on a dashboard I look at every time I ship a post. I just had to be the one to actually look.


Related: Six Slash Commands I Built on Top of Claude Code — the two commands in this story, /draft-post and /publish-post, are two of the eight I didn't cover there.