DEV Community

Cover image for Your iOS App Works on Wi-Fi but Fails on a VPN: How to Debug the Difference
KlarVPN
KlarVPN

Posted on AI-assisted

Your iOS App Works on Wi-Fi but Fails on a VPN: How to Debug the Difference

Your iOS app loads its dashboard over Wi-Fi. Turn on a VPN, repeat the request, and the app displays “Something went wrong.”

It is tempting to stop there and label the issue a VPN problem. But that message could hide a failed hostname lookup, a connection timeout, a TLS error or an HTTP response your app handles incorrectly.

A failure that appears when a VPN is enabled is a useful reproduction condition. It is not yet a diagnosis.

The next step is to identify where the request stops progressing.

1. Compare the same request on the same network

Wi-Fi and a VPN are not competing connection types. Your phone can use Wi-Fi as the underlying network while the VPN carries selected traffic over it.

Start with two conditions:

  • The same Wi-Fi network with the VPN disconnected.
  • The same Wi-Fi network with the VPN connected.

Keep the app build, device, account, endpoint and request payload unchanged.

For the iPhone VPN used in your test, record the app version, selected location and relevant connection settings. These details make the reproduction specific enough to investigate.

Also record whether another proxy, iCloud Private Relay or a device-management profile is involved.

Repeat the comparison. A single failure might coincide with an unrelated server error or an expired session.

Once the initial result is repeatable, compare cellular with and without the VPN. This helps establish whether the behaviour depends on the underlying network.

2. Preserve the actual error

A generic error banner is appropriate for some user-facing situations. It is inadequate as your only diagnostic information.

For a failed URLSession request, capture the error domain and code. In a development build, a small logging helper can preserve those details without printing credentials or response bodies:

import Foundation

func logRequestFailure(_ error: Error) {
    let nsError = error as NSError

    print("Error domain:", nsError.domain)
    print("Error code:", nsError.code)

    if let urlError = error as? URLError {
        print("URL error code:", urlError.code.rawValue)
    }
}
Enter fullscreen mode Exit fullscreen mode

Call this from the error path of your existing request.

Avoid logging complete URLs when query parameters contain tokens or personal information. Use an internal request identifier to connect client and server observations.

Different errors lead to different investigations:

  • cannotFindHost: investigate hostname resolution and the hostname itself.
  • cannotConnectToHost: investigate reachability and connection establishment.
  • secureConnectionFailed: investigate the secure connection and trust configuration.
  • timedOut: investigate which stage consumed the available time.

These are starting points. An error code does not identify the responsible component by itself.

3. Separate transport errors from HTTP responses

A request can reach a server and still fail at the application level.

For example, a VPN-on request might receive HTTP 403, 429 or 503. That is different from receiving no HTTP response because connection establishment failed.

With URLSession, inspect the response status explicitly:

if let httpResponse = response as? HTTPURLResponse {
    print("HTTP status:", httpResponse.statusCode)
}
Enter fullscreen mode Exit fullscreen mode

Place this in the response-handling path of your existing request.

A 403 does not automatically mean “VPN blocked.” It could reflect authentication, authorization, an edge-security rule or another policy.

A 429 suggests rate limiting, but the response alone does not establish which key triggered the limit. The system might use an account, token, address or a combination.

Find the matching request in your edge and application logs before changing the client.

4. Investigate DNS without changing the test

If the hostname cannot be resolved, establish whether it is intended to be publicly resolvable.

An internal development hostname may work through a local or corporate resolver but fail through another DNS path. That result differs from a public API hostname failing unexpectedly.

Check:

  • The exact hostname configured in the app.
  • Whether it requires a private network or corporate DNS.
  • Whether the VPN configuration changes DNS handling.
  • Whether the failure affects one hostname or several.

A lookup from your laptop is supporting evidence, not proof of the iPhone’s resolution path.

Avoid replacing an HTTPS hostname with a raw IP address as a quick fix. Doing so can change certificate validation and virtual-host routing, creating a different failure from the one you started with.

5. Measure request stages

An overall duration tells you how long the request took. It does not explain where the time went.

Apple’s URLSessionTaskMetrics provides transaction-level observations through the session delegate. Useful fields include:

  • domainLookupStartDate and domainLookupEndDate.
  • connectStartDate and connectEndDate.
  • secureConnectionStartDate and secureConnectionEndDate.
  • requestStartDate.
  • responseStartDate and responseEndDate.
  • isReusedConnection.

Compare VPN-off and VPN-on runs using the same instrumentation.

Look for the earliest meaningful difference. Does resolution fail to complete? Does connection establishment take longer? Does a response arrive promptly with an unexpected HTTP status?

Treat missing timestamps carefully. Reused connections or locally retrieved resources can leave connection-related fields empty. A missing value is not automatically evidence that a stage failed.

Redirects can also produce multiple transactions. Inspect the sequence instead of assuming one task represents one network exchange.

6. Review timeout and connectivity behaviour

A timeout setting should express an application requirement. Increasing it indefinitely can hide a failure while leaving users waiting.

Apple distinguishes two configuration properties:

  • timeoutIntervalForRequest controls the interval spent waiting for additional data.
  • timeoutIntervalForResource limits the overall time allowed for the resource transfer.

For an ordinary foreground session, waitsForConnectivity determines whether the session waits for suitable connectivity rather than failing immediately when it is unavailable.

A configuration experiment might look like this:

let configuration = URLSessionConfiguration.default

configuration.waitsForConnectivity = true
configuration.timeoutIntervalForRequest = 30
configuration.timeoutIntervalForResource = 60
Enter fullscreen mode Exit fullscreen mode

These values are illustrative. Choose values appropriate to the operation and create the session with that configuration.

Waiting for connectivity does not repair a DNS configuration, a rejected certificate or an HTTP authorization failure. It also does not automatically replay an interrupted operation safely.

If you add retries, distinguish reads from writes. Repeating an order-creation request after an ambiguous timeout can create duplicate work unless the server supports appropriate idempotency controls.

7. Correlate with server-side evidence

Attach a request identifier to a diagnostic request where your API supports it.

Then check the matching observation at each layer you control:

  • Did the edge receive the request?
  • Did it forward the request to the application?
  • Did an access rule or rate limit act on it?
  • Did the application return a response?
  • Did the client receive that response?

The absence of an application log entry does not prove the request never reached your infrastructure. A CDN, load balancer or gateway may have rejected it first.

If client addresses matter, use the address recorded by your trusted ingress configuration. Do not accept arbitrary client-supplied forwarding headers as evidence.

8. Avoid fixes that remove the evidence

Several changes can make a symptom disappear without explaining it:

  • Disabling certificate validation.
  • Adding broad App Transport Security exceptions.
  • Replacing every failure with an automatic retry.
  • Switching networks, VPN locations and app settings simultaneously.
  • Treating a successful Safari request as proof that the native app should behave identically.

Safari and your app can differ in authentication, cookies, caching, request headers and networking configuration.

Keep the reproduction narrow until you can describe the failure precisely.

Finish with a finding you can act on

“Works without a VPN” is the beginning of the investigation.

A useful finding identifies the failing stage and the evidence:

The VPN-on request reaches our edge and receives HTTP 403 before reaching the application. The matching request identifier appears in an access-rule event.

Or:

The app uses an internal hostname that resolves on the office network but fails under the tested VPN DNS configuration.

Those findings lead to specific changes. They also give you something concrete to retest.

Start with one repeatable request, preserve its error and status information, and follow it from the client to the service. Change the configuration only after the evidence points to the layer that needs attention.

Top comments (0)