Missing Access-Control-Allow-Origin header on JSON API preflight

I’m playing about making a simple web client using the bearer token for auth using APP TOKENS, and I’ve a question about the JSON API endpoint configurations.

My way of calling them is identical in a try/catch:

const res = await fetch(`${API_BASE}/posts/bookmarks`, {
      headers: apiHeaders(),
    });
    const data = await res.json();

and

const res = await fetch(`${API_BASE}/posts/timeline`, {
      headers: apiHeaders(),
    });
    const data = await res.json();

and

const res = await fetch(`${API_BASE}/posts/photos`, {
      headers: apiHeaders(),
    });
    const data = await res.json();

and

const configRes = await fetch(`${API_BASE}/micropub?q=config`, {
    headers: apiHeaders(),
  });
  const config = await configRes.json();
  

with the apiHeaders containing the bearer token - you get the idea.

Problem

Both the timeline, bookmark, photos and config endpoints work fine in an API test tool, sending the same bearer token, however when requesting using JS in a browser only the timeline and bookmark endpoints work, and the photos and config both seem to fail.

when I request https://micro.blog/posts/photos

I get back in the dev console:

Access to fetch at ‘https://micro.blog/posts/photos’ from origin ‘http://localhost:8000’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

I’ve asked Claude and it reckons:

API tools like Postman send the GET request directly and see Access-Control-Allow-Origin: * on the response. But browsers don’t work that way.

Because our request includes an Authorization header (not a “simple” header), the browser first sends a preflight OPTIONS request before the actual GET. If the server doesn’t return proper CORS headers on that OPTIONS response, the browser blocks the request entirely — it never even sends the GET.

So the likely situation is:

  • GET /posts/photos → returns Access-Control-Allow-Origin: * (what the API tool sees)
  • OPTIONS /posts/photos → missing CORS headers (what the browser hits first, and fails on)
  • OPTIONS /posts/timeline → returns CORS headers correctly (which is why that works)
    API tools never send the preflight, so they don’t encounter the problem.

Unfortunately there’s no client-side workaround — the Authorization header always triggers a preflight, and we can’t skip it.
This is a Micro.blog server configuration issue where the OPTIONS handler isn’t set up consistently across all endpoints.

Questions/ Suggestion

I guess my questions is:
a) is this correct and
b) why would some JSON endpoints be configured in a certain way and others not?
c) If so is it possible for the JSON API endpoints to be configured similarly so they work consistently? or at least setup so any PREFLIGHT checks work the same?

Sorry about that. Many of the APIs were originally designed for native apps and I’ve been going back and sprinkling in CORS headers so that they work from JavaScript in the browser too. I’ll fix these today. Thanks!

1 Like

Thank you for looking into it!

I can see you’ve fixed the /photos endpoint - any chance you could do the same for the https://micro.blog/micropub?q=config endpoint? (I want to make a blog picker and I currently cannot get the list of blogs back in-browser querying this).

Edit: I think the same applies to GETs to https://micro.blog/micropub?q=source and POSTs to https://micro.blog/micropub from a web browser using javascript, as I can happily send things via an API test tool, but it fails in the browser sending the same posting with a 400 error.