The API answers in Safari on the same phone, and the app's request times out. That gap is usually not the tunnel: an app calling a private service makes an ordinary HTTPS request, and the device's existing VPN only decides whether that request has a path. Writing a second tunnel into the app is a product decision, not a repair for a timeout. What follows classifies the failure by where it happens, using the error surface the app already has.

Start from the error, not the tunnel

URLSession reports transport failures as NSError values in NSURLErrorDomain, and the code is the cheapest evidence available. Before changing any configuration, log that code with the failing hostname and the task's elapsed time. A timeout, a name failure and a trust failure look identical on screen and take three different repairs. Classify first, then read the section for that class.

Task metrics answer the follow-up question, whether a connection was ever established, which separates a request that never had a route from one the server took too long to answer. None of this needs the tunnel changed; it needs the request's own diagnostics printed somewhere you can read.

Name resolution fails before anything else can

A short office name such as api.corp.example usually depends on a search domain the phone does not carry, so the app resolves a name that exists only inside the building. NSURLErrorCannotFindHost is the signature. The full hostname has to resolve through whatever resolver the VPN installs, and the app should request the fully qualified form whenever the search domain is not guaranteed. Swapping the name for an address and disabling certificate checks moves the failure rather than fixing it.

A private DNS record and a certificate issued for that exact name are the supported pair. Everything else is a workaround that ships to users.

A timeout is a path question, not a name question

NSURLErrorTimedOut means the name resolved and nothing answered, which points at the route, the packet policy or the service. When the tunnel is assigned per application, a different app reaching the same server says nothing about your build, because that mapping is not shared. Check that the bundle you are running is inside that assignment, then check that the service path is permitted for it. A development build installed outside the assignment can sit outside the tunnel entirely and look like a broken API.

A browser comparison shows that some app on the device has a path. It never proves that your build does, which is why the two tests have to be run separately.

Trust failures are about the name on the certificate

NSURLErrorServerCertificateUntrusted, taken together with the TLS error underneath it, means the chain or the identity check failed. An internal CA has to be trusted on the device, and a certificate issued for a different hostname is still wrong after any amount of network reconfiguration. Apple's App Transport Security guidance continues to apply to these URL-loading connections, and a tunnel around a plain HTTP service does not make that service end-to-end secure.

Fix the identity or the trust anchor and keep the exception list empty. A blanket arbitrary-load exception hides the failure during development and leaves it in place for every user afterwards.

An HTTP rejection belongs to the app, not the network

A 401 or 403 on a completed handshake means the request arrived and the service declined it, so the remaining question is an account, a token audience, a scope or a key. Log the status code and the hostname, never the bearer token or the response body. At that point the tunnel is doing its job and further network changes only delay the real fix.

The table is the classification to keep beside the terminal: one row per failure the app can actually report. Print the code before you read it, because the row you pick decides which section you work through.

Where it failsWhat the app seesFirst check
name resolutionNSURLErrorCannotFindHost (-1003)resolve the fully qualified name from the device itself
no path, or no answerNSURLErrorTimedOut (-1001)is this bundle inside the per-app mapping, and is the service path allowed
TLS trustNSURLErrorServerCertificateUntrusted (-1202)does the certificate name match the host, and is the internal CA trusted
service refusalHTTP 401 or 403 after a completed handshakeaccount, token audience, scope or key

Each row is cheaper to test than the one below it, and each rules out a different layer of the same request. Work down the table until a row matches, then stop changing the network.

Artiglyn mynjee