DEV Community

조세윤
조세윤

Posted on

My Gemini calls failed from Korea with "User location is not supported". A Durable Object fixed it.

I’m building hihu, an app for learning how to prompt AI. It’s just me working on it, from Korea. Each lesson includes a chat with Gemini, with the requests going through a Cloudflare Worker so the API key stays out of the app.

While testing on my phone, I kept getting this error:

400 User location is not supported
Enter fullscreen mode Exit fullscreen mode

About half my messages failed. I could send a sentence, get the error, then send the exact same sentence again and get a normal reply.

It turned out to depend on where the request went through Cloudflare. My requests from Korea were going through either Hong Kong or Tokyo. The Tokyo requests worked. The Hong Kong ones failed because the Gemini API isn’t available there.

Placement didn’t fix it

I first tried the [placement] setting in wrangler.toml, hoping it would move the Worker somewhere Gemini would accept requests from.

The cf-placement response header showed remote-OMA, which suggested the Worker was running in Omaha. That looked promising, but the Gemini calls were still leaving from Hong Kong and failing.

What finally worked

I moved the Gemini call into a Durable Object and gave it a locationHint of wnam, for western North America. The Worker now passes requests to that object instead of calling Gemini itself.

Here’s the relevant config:

[[durable_objects.bindings]]
name = "US_EXIT"
class_name = "UsExit"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["UsExit"]
Enter fullscreen mode Exit fullscreen mode

The object doesn’t do much. It takes the request body and forwards it to Gemini:

export class UsExit {
  constructor(state, env) {
    this.env = env;
  }

  async fetch(request) {
    return fetch('https://generativelanguage.googleapis.com/v1beta/interactions', {
      method: 'POST',
      headers: {
        'x-goog-api-key': this.env.GEMINI_API_KEY,
        'content-type': 'application/json',
      },
      body: await request.text(),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, in the Worker:

const exit = env.US_EXIT.get(
  env.US_EXIT.idFromName('us'),
  { locationHint: 'wnam' }
);

const response = await exit.fetch('https://exit/', {
  method: 'POST',
  body: JSON.stringify(body),
});
Enter fullscreen mode Exit fullscreen mode

Before this change, all 12 requests I tested through Hong Kong failed. After the change, all 12 succeeded. Replies took about four seconds.

That’s a small test, but it fixed the issue I was seeing on my phone.

A few details if you’re trying this yourself:

  • This works on the free plan with SQLite-backed Durable Objects (new_sqlite_classes in the config).
  • The location hint is a preference, not a guarantee, and it only applies when the object is first created. Adding it to an existing object won’t move that object.
  • Using idFromName('us') sends everything through the same object. That’s enough for my app right now. I haven’t tested it under heavy traffic.

The app is hihu. There are only two lessons so far, both in English, and each takes a few minutes. This was the bug I had to sort out to get the chat working reliably.
!I built hihu with a lot of help from AI coding tools, and AI helped me write this post in English.

Top comments (1)

Collapse
 
alexshev profile image
Alex Shev •

The locationHint caveat matters: it is a placement preference established at object creation, not a routing guarantee. I’d add a lightweight health probe that records the provider result by exit region and alerts on the original 400, plus document the data-residency implication of intentionally relaying user prompts through North America. That makes the workaround observable instead of a one-time fix that can silently drift.