← All notes

"Loading indefinitely". How Apple rejected my app

· Not by Prompt Alone · episode 2 · Czytaj po polsku

Two words from Apple's reviewer: "loading indefinitely". In other words - it spins and nothing happens.

That was mid-July. Zakupomat was just about to stop being an app only for us and was going to the App Store - for the first time! On my phone everything worked. On Marta's too. For Apple's reviewer, creating a new family ended with a never-ending spinner.

In the previous episode I wrote that for a while I didn't know why. Today I'll tell you the details. Because this is exactly the part of building an app you won't see in the "app in 10 minutes" video.

First hypothesis: the reviewer turned off the internet

The first thought was very convenient - the reviewer turned off the internet on the tablet to see how the app behaves offline. Testing 101. And a spinner that spins forever suggested the rest - the request hangs and nobody told it when to give up. And sure enough - the create-family request never reached the server, and the app's HTTP client had no timeout set.

The plan was ready: a timeout on requests, a "no connection" message and a new build for Apple. A few minutes of work. Every API call got a time limit of 25 seconds. Build 10.

We very nearly stopped there. Except that wasn't a fix, it was half a fix. The app stopped hanging forever - now after 25 seconds it showed "no connection". We'd have been convinced the case was closed, and you still couldn't create a family.

And here's the first lesson that lives in the project memory today: a missing timeout doesn't look like a network error. It looks like "the app doesn't work". Neither a user nor a reviewer will report "timeout". They'll report that it hangs.

Logs don't lie

So I looked at the server logs. And this is where it got interesting.

The reviewer logged in and browsed the app normally. The server saw every request. Only the create-family request never reached the backend at all. A strange moment for Apple's reviewer to be testing timeouts. That got me thinking.

We started going through exactly the path the reviewer took... and BOOM! The app hangs!

The problem showed up ONLY in one specific sequence: log in → log out → create a new family. Who normally does something like that? Nobody! Apple's reviewer - always! ;-)

A checkout blocked by a customer who never finishes shopping

Zakupomat syncs the list live. Marta adds milk, and it shows up on my list in the store right away. That's done by a long-lived connection to the server (SSE), which waits for changes the whole time the app is running (by the way, maybe in the next version I'll kill it when the app is in the background ;-)).

And that connection shared a single HTTP client with the rest of the app. On iOS that means one connection pool. The long-lived stream took it over for good, and the create-family request was stuck in a queue that never moved.

Put simply - the store has one checkout, and at it stands a customer who never finishes shopping. Everyone behind them waits. Forever.

The fix? Two separate clients: one for the API, the other for the live stream. Build 11. And in build 12, retries for login and sign-up when the connection stalls.

Three fix builds. The first at 17:45, the last at 18:22, the same day. You can work like that with Claudius - but only when someone knows what to look for and is willing to go beyond what the agent itself suggests.

What a test can't catch

The most unsettling part of this story is that no automated test caught it. And none will! The HTTP client tests use a fake network, and a fake network has no connection pool. In the tests everything was green.

So instead of pretending a test would handle it, we did two things:

  1. A manual smoke test before every store submission. One of the checklist items reads exactly: "login → logout → create family". The sequence that got us rejected is now checked every single time.
  2. A record in the project memory. The decision "the live stream has its own HTTP client" is marked as critical, and the field "what would reverse this decision" says: "Nothing sensible. Merging these clients to save resources is a regression."

That second point matters more than it looks.

Why I write this down

Six months from now I won't remember it. Claude doesn't remember anything between sessions (OK, fine! Claude Code has shared project memory now, but it works the way it works). And two HTTP clients where one could do look like an obvious candidate for "simplification". Any decent AI agent that sees code like this without context will suggest merging them. And it will sound very convincing.

The project memory is there so the agent sees the context before it writes the first line. So it says: "this breaks decision D-007", instead of quietly reintroducing the bug that got the app rejected by Apple.

That's exactly the difference between a prompt and a product. A prompt fixes the symptom. A product remembers why the symptom appeared and takes it into account.

What this means for your team

An AI agent will very quickly fix what's visible. Adding a timeout took literally a moment. But the symptom ("it hangs") looked nothing like the cause ("two kinds of traffic share one connection pool"), and what led to the cause were the server logs and understanding how the platform works. Teams working with agents need not only faster code writing, but also a place where they write down why the code is the way it is. And at the same time don't write down anything that can be read from the code and may change quickly.

At ANSLAN we help build a second brain for existing products - we rebuild the knowledge about the system and write it down so that both people and AI agents can use it.

And Zakupomat itself? You can just grab it and use it: zakupomat.app. Free, no ads, no selling data, no subscription. Android, iPhone and the browser.

See you next week :-)