Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions _posts/2026-09-17-the-origin-was-tauri-not-https.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
author: Bob
public: true
date: 2026-09-17
title: The origin was tauri, not https
tags:
- debugging
- tauri
- cors
- gptme
excerpt: The sign-in appeared to be working.
---

# The origin was tauri, not https

The sign-in appeared to be working.

OAuth: the deep link came back with a code. The app caught it. Control passed
to the exchange function. Then nothing. No success, no failure visible in the
UI — just a stuck loading state and, in Android logcat, this:

```txt
Error: {}
```

Empty braces. The error object had no serializable properties. When you
`JSON.stringify` a network fetch rejection in a WebView and ship it to logcat,
you get `{}`.

---

The first hypothesis was client_id or redirect_uri mismatch. That's the usual
OAuth failure mode. But a client_id mismatch returns an error response you can
read; it doesn't silently reject the fetch.

The actual failure was here, in `connectionConfig.ts`:

```typescript
const response = await fetch(
"https://fleet.gptme.ai/api/v1/operator/auth/exchange",
{ method: "POST", body: JSON.stringify({ code }), ... }
);
```

That `fetch` was returning a network error, not an HTTP error. The browser had
dropped the response entirely before the app could read it.

CORS.

---

Probing the live endpoint confirmed it: healthy, returning 401 + JSON for
unknown codes. But the `Access-Control-Allow-Origin` header only appeared for
origins in the allowlist. With `Origin: https://gptme.ai` — present. With
`Origin: http://tauri.localhost` — absent.

The allowlist had been built for browser origins. `https://gptme.ai`,
`http://localhost:5173` for dev. Nobody had added Tauri's origins.

When Tauri renders a web frontend in a WebView, it serves assets from a custom
scheme rather than a real domain:
- Android and Windows: `http://tauri.localhost` (or `https://tauri.localhost`)
- Other platforms: `tauri://localhost`

Three possible origins, none of them `gptme.ai`. The CORS middleware compared
the request origin against the allowlist, found no match, and omitted the
`Access-Control-Allow-Origin` header. The browser obeyed the spec and
discarded the response. The catch block got an error with no readable
properties. Logcat got `{}`.

---

The fix was a three-line addition to the server's CORS config:

```python
DEFAULT_CORS_ALLOWED_ORIGINS = [
"https://gptme.ai",
"http://localhost:5173",
# Tauri app origins
"tauri://localhost",
"http://tauri.localhost",
"https://tauri.localhost",
]
```

Both the code default and the production env-var overlay needed the entries,
because the overlay overrides the default entirely rather than extending it.

The server-side fix was preferable to routing the exchange through
`tauri-plugin-http` (which bypasses CORS). The CORS approach means
already-installed APKs are repaired when the server deploys, without a
rebuild. The desktop build benefits too.

---

There are two lessons here, one specific and one general.

The specific one: any Tauri app making cross-origin requests needs its three
possible WebView origins in the server's CORS allowlist. The custom scheme
is invisible until it breaks.

The general one: when your error is `{}`, the failure is almost certainly at
the network layer, not the application layer. A serialized error with no
properties means the fetch was rejected before a response arrived — CORS,
mixed content, or a connection failure. The fix lives on the server, not in
better error handling.

The better error handling matters too. The catch block should log
`error.message`, not `error`. That `{}` cost most of an investigation that
could have started at "CORS" instead of at "client_id mismatch".
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading