Common Deep Links Bugs and How to Catch Them

Common Deep Links Bugs and How to Catch Them is the exact phrase developers type when they want a practical checklist for validating deep link behavior before a release. Deep links—URIs that launch an

January 17, 2026 · 15 min read · Common Issues

Common Deep Links Bugs and How to Catch Them: Setting the Stage

Common Deep Links Bugs and How to Catch Them is the exact phrase developers type when they want a practical checklist for validating deep link behavior before a release. Deep links—URIs that launch an app directly to a specific screen—are a core part of modern mobile and web experiences. When they break, users land on a blank screen, see an error dialog, or are sent to the wrong flow, which instantly erodes trust and can cause abandonment.

The problem is not that deep links are hard to implement; it’s that the failure modes are subtle, environment‑dependent, and often invisible to scripted UI tests that follow a single, predetermined path. A deep link may work when the app is cold‑started from the home screen but fail when the app is already in the background, or when the link contains a query parameter that the code never validates. Persona‑driven autonomous exploration, such as that performed by the SUSATest agent, exercises the link matrix with varied user behaviors (curious, impatient, novice, adversarial, elderly, accessibility, power user) and therefore surfaces bugs that static test suites miss.

In the sections that follow we catalog the most common deep link bug patterns, explain why each occurs, show how it looks to a user, detail reproducible steps, and give concrete fixes. We also provide a test matrix that compares manual, automated, and autonomous approaches, a short release checklist, and real‑world case studies. By the end you will have a bookmark‑worthy guide you can apply to Android, iOS, or hybrid web‑native apps today.

---

Common Deep Links Bugs and How to Catch Them: Bug Patterns Catalog

Below are twelve real‑world bug patterns grouped by root cause. For each pattern we list the symptom, why it happens, how to reproduce it, and the fix.

1. Missing or Incorrect Intent‑Filter / URL Scheme Registration

Symptom: Tapping a deep link opens the app to the launcher activity or shows “App not found”.

Why it happens: The AndroidManifest.xml (or iOS Info.plist) lacks an with the correct tag, or the scheme/host is misspelled.

Reproduction:


adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123"  

If the app launches to the main screen instead of the product detail, the filter is wrong.

Fix: Ensure the manifest contains:


<intent-filter android:autoVerify="true">  
    <action android:name="android.intent.action.VIEW"/>  
    <category android:name="android.intent.category.DEFAULT"/>  
    <category android:name="android.intent.category.BROWSABLE"/>  
    <data android:scheme="https"  
          android:host="www.example.com"  
          android:pathPrefix="/product"/>  
</intent-filter>  

For iOS, verify the associated domains entitlement and the LSApplicationQueriesSchemes array.

2. Host‑Mismatch Between Manifest and Server

Symptom: Deep link works in a debug build but fails in production.

Why it happens: The manifest registers example.com while the server sends links to www.example.com (or vice‑versa). Android’s intent resolution is host‑exact unless you use wildcards.

Reproduction:


adb shell am start -W -a android.intent.action.VIEW -d "https://www.example.com/product/123"  

If the app does not open, check the host.

Fix: Either add both hosts to the manifest or use a wildcard:


<data android:scheme="https" android:host="*.example.com"/>  

3. Path‑Prefix Too Restrictive

Symptom: Links with extra path segments (e.g., /product/123/reviews) open the app but land on a blank screen.

Why it happens: The intent‑filter specifies android:path="/product" which only matches exactly that path. Longer paths are not delivered to the activity.

Reproduction:


adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/product/123/reviews"  

Fix: Use android:pathPrefix or a regular expression via android:pathPattern:


<data android:scheme="https" android:host="example.com" android:pathPrefix="/product"/>  

4. Query Parameter Not Handled or Mis‑Parsed

Symptom: Link opens the correct screen but shows default data (e.g., product ID = 0) or crashes with a NumberFormatException.

Why it happens: The activity extracts the query string with getIntent().getData().getQueryParameter("id") but never validates that the value is present or numeric.

Reproduction:


adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/product?id=abc"  

If the app crashes, the bug is present.

Fix: Add defensive parsing:


val idStr = intent.data?.getQueryParameter("id")  
val id = idStr?.toIntOrNull() ?: return  
showProduct(id)  

5. Missing Deep Link Handler in Activity Lifecycle

Symptom: Link works when the app is launched from scratch but does nothing when the app is already running in the foreground.

Why it happens: The developer only overrides onCreate() to read the intent. When the app is already alive, Android calls onNewIntent() instead, which is left unimplemented.

Reproduction:

  1. Open the app via launcher.
  2. Press Home, leave it in background.
  3. Execute:
  4. 
    adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/product/456"  
    

If the UI does not update, onNewIntent() is missing.

Fix: Override onNewIntent() and delegate to the same handling logic:


override fun onNewIntent(intent: Intent) {  
    super.onNewIntent(intent)  
    setIntent(intent) // required for subsequent getIntent() calls  
    handleDeepLink(intent)  
}  

6. Incorrect Intent Flags Causing Task Stack Issues

Symptom: After following a deep link, pressing Back shows the previous app instead of the expected screen within your app.

Why it happens: The intent is launched with FLAG_ACTIVITY_NEW_TASK without FLAG_ACTIVITY_CLEAR_TOP, causing a new task to be stacked on top of the existing one.

Reproduction:

Launch the deep link, then press Back repeatedly and observe the task switcher.

Fix: Add appropriate flags when constructing the intent (if you manually start it) or set them in the manifest:


<activity  
    android:name=".ProductDetailActivity"  
    android:launchMode="singleTop"  
    android:taskAffinity="">  
    <intent-filter> … </intent-filter>  
</activity>  

singleTop ensures that if the activity is already at the top of the stack, onNewIntent() receives the new data.

7. Universal Link Apple App Site Association (AASA) File Misconfiguration

Symptom: iOS universal link opens Safari instead of the app.

Why it happens: The AASA JSON file hosted at https://domain.com/apple-app-site-association is missing, malformed, or does not contain the correct appID and paths.

Reproduction:


curl -I https://example.com/apple-app-site-association  

Look for Content-Type: application/json and a valid JSON body.

Fix: Ensure the file is accessible over HTTPS, returns application/json, and contains:


{  
  "applinks": {  
    "apps": [],  
    "details": [  
      {  
        "appID": "TEAMID.com.example.app",  
        "paths": [ "/product/*", "/NOT /news/*" ]  
      }  
    ]  
  }  
}  

After updating, reinstall the app and test with:


xcrun simctl openurl booted "https://example.com/product/789"  

8. Missing Associated Domains Entitlement

Symptom: Universal link is correctly formatted but still opens Safari.

Why it happens: The app’s entitlements file lacks the com.apple.developer.associated-domains entry with the correct applinks: prefix.

Reproduction: Check the entitlements file in Xcode: it should contain applinks:example.com.

Fix: Add the entitlement, rebuild, and reinstall.

9. Deep Link Intercepted by Third‑Party App or Browser

Symptom: Tapping a link opens a browser or another app that claims to handle the same scheme.

Why it happens: Another installed app has registered the same scheme/host with higher priority (e.g., a social media app). Android resolves to the first match in the package manager list.

Reproduction: Install a test app that registers myapp:// and see which one opens.

Fix: Use HTTPS URLs with universal links/App Links instead of custom schemes, or add android:autoVerify="true" and host a Digital Asset Links file on your domain to prove ownership.

10. Session State Loss After Deep Link

Symptom: User logs in via deep link, but after navigation the app treats the user as logged out.

Why it happens: The deep link launches a new instance of the activity that does not inherit the existing SharedPreferences or singleton session manager because the process was killed and recreated without restoring state.

Reproduction:

  1. Log in normally.
  2. Kill the app via adb shell am force-stop.
  3. Launch deep link that requires auth.
  4. Observe that the login screen appears.

Fix: Persist auth token to secure storage and have the deep‑link target activity check that storage on resume, redirecting to login only if missing.

11. Improper Encoding/Decoding of Special Characters

Symptom: Link containing spaces, #, ?, or Unicode characters results in a 404‑like screen or garbled data.

Why it happens: The developer builds the URI by simple string concatenation instead of using Uri.Builder (Android) or URLComponents (iOS), leading to double‑encoding or missing encoding.

Reproduction:


adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/search?q=hello world"  

If the app receives q=hello%20world correctly, the bug is absent; if it receives q=hello world (space unencoded) or q=hello%2520world (double‑encoded), the bug exists.

Fix: Always use the URI builder:


val uri = Uri.Builder()  
    .scheme("https")  
    .host("example.com")  
    .appendPath("search")  
    .appendQueryParameter("q", "hello world")  
    .build()  

12. Accessibility Breakage – TalkBack/VoiceOver Does Not Announce Deep‑Link Result

Symptom: After a deep link lands on a screen, TalkBack reads “blank” or does not move focus to the newly loaded content.

Why it happens: The fragment or view is instantiated but the developer does not call announceForAccessibility() or does not shift focus to a meaningful element.

Reproduction: Enable TalkBack, trigger the deep link, and listen for spoken feedback.

Fix: In the deep‑link handling code, after UI updates, request focus on a heading or call:


findViewById<TextView>(R.id.product_title).requestFocus()  
findViewById<TextView>(R.id.product_title).announceForAccessibility("Product details loaded")  

---

Common Deep Links Bugs and How to Catch Them: Detection Strategies

Finding these bugs requires a mix of static analysis, runtime checks, and exploratory testing that mimics real user variability. Below we break down detection into four layers: manual spot checks, automated unit/UI tests, CI pipelines, and persona‑driven autonomous exploration.

Manual Spot‑Check Checklist

  1. Manifest / Entitlements audit – grep for tags and verify scheme/host/path against known links.
  2. ADB / Simulator launch – run adb shell am start -W -a android.intent.action.VIEW -d "" and xcrun simctl openurl booted "".
  3. Observe UI – confirm the correct screen appears, data is populated, and Back navigation behaves as expected.
  4. Check logs – look for ActivityNotFoundException, NullPointerException, or warnings about intent resolution.
  5. Test state – force‑stop the app, then launch the link to verify session restoration.

While manual checks catch obvious misconfigurations, they are tedious and error‑prone for large link matrices.

Automated Unit Tests

*Validate intent‑filter parsing* – Use Android’s PackageManager to query resolveIntent() and assert the returned component matches expectations.

*Validate URI building* – Unit test helper methods that construct deep links with Uri.Builder to ensure proper encoding.

*Validate deep‑link handler* – Invoke onNewIntent() directly with a test intent and assert that the view model receives the correct parameters.

Instrumented UI Tests (Espresso / XCUITest)

Create a test that:

  1. Starts the app from a cold state.
  2. Sends an intent via adb shell am start (or XCUITest’s addInterrupt).
  3. Waits for a specific element (e.g., product title) to appear.
  4. Asserts text matches the expected ID from the query parameter.

Example (Espresso):


@Test  
fun deepLinkShowsProduct() {  
    val testUri = Uri.parse("https://example.com/product/42")  
    val intent = Intent(Intent.ACTION_VIEW).apply {  
        data = testUri  
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)  
    }  
    ActivityScenario.launch(ProductDetailActivity::class.java, intent)  

    onView(withId(R.id.product_title)).check(matches(withText("Product 42")))  
}  

Continuous Integration Gates

Integrate the above instrumented tests into your CI pipeline (GitHub Actions, Bitrise, GitLab CI). Additionally, run a *link lint* step that:

Tools like deep-link-test (open‑source) or Firebase App Indexing validation can automate this.

Persona‑Driven Autonomous Exploration

Scripted tests follow a single, happy‑path flow. Real users, however, may:

The SUSATest agent, when pointed at an APK or a web URL, builds a behavior model for each persona (curious, impatient, novice, adversarial, elderly, accessibility, power user). It then:

  1. Generates thousands of URI variations (scheme, host, path, query, fragment).
  2. Sends them via adb shell am start or universal link calls while the app is in different lifecycle states (cold, foreground, background, not‑running).
  3. Monitors for crashes, ANRs, unresponsive UI, incorrect screen, missing accessibility announcements, and unexpected task stack changes.
  4. Learns which screens are dead ends and avoids re‑exploring them in subsequent runs, increasing efficiency.

Because the agent varies the *timing* of link delivery (e.g., after a rotation, after a network delay, while a dialog is showing), it surfaces bugs such as missing onNewIntent() handling or race conditions in session restoration that deterministic tests never see.

---

Common Deep Links Bugs and How to Catch Them: Prevention and Fixes

Preventing deep link defects is cheaper than debugging them post‑release. The following practices address the root causes identified above.

Centralized Deep Link Registry

Create a single source of truth (e.g., a Kotlin object or Swift enum) that lists every supported deep link pattern with its constituent parts:


object DeepLinkRegistry {  
    const val BASE = "https://example.com"  
    data class Product(val id: Int)  
    data class Search(val query: String)  

    fun fromUri(uri: Uri): SealClass {  
        return when {  
            uri.host == "www.example.com" && uri.pathSegments.contains("product") ->  
                Product(uri.getQueryParameter("id")?.toIntOrNull() ?: return null)  
            uri.host == "www.example.com" && uri.pathContains("search") ->  
                Search(uri.getQueryParameter("q") ?: return null)  
            else -> null  
        }  
    }  
}  

All UI layers consume this sealed class, guaranteeing that any new link must be added to the registry and thus reviewed.

Use App Links / Universal Links Exclusively

Migrate away from custom schemes (myapp://) to HTTPS URLs backed by Apple App Links and Android App Links. This eliminates scheme collisions and provides a verification mechanism (Digital Asset Links file, AASA).

Automated Link Validation in CI

Add a step that:

  1. Extracts all links from your markdown, HTML, and JSON payloads.
  2. Checks each against the registry (or a Swagger‑like OpenAPI spec for links).
  3. Fails if a link is undefined or if the registry does not contain a matching handler.

Example (bash + jq):


grep -oP '(https?://[^\s"]+)' docs/**/*.md | sort -u > links.txt  
while read -r link; do  
  if ! ./validate-link.sh "$link"; then  
    echo "Invalid link: $link"  
    exit 1  
  fi  
done < links.txt  

Defensive Handler Design

Every activity or view model that processes a deep link should:

Kotlin example:


fun handleDeepLink(intent: Intent?) {  
    val uri = intent?.data ?: return Log.w(TAG, "Null intent")  
    val productId = uri.getQueryParameter("pid")  
        ?: return Log.e(TAG, "Missing pid in $uri")  
    val id = productId.toIntOrNull()  
        ?: return Log.e(TAG, "Non‑numeric pid: $productId")  
    viewModel.loadProduct(id)  
}  

Accessibility‑First Deep Link Testing

When a deep link lands, automatically move focus to a heading or announce the result. Write a unit test that uses AccessibilityDelegate to verify that announceForAccessibility() is called with a non‑empty string after link processing.

Versioned Link Contracts

Treat deep links as an API. Assign a version (e.g., v1/product/:id) and store the contract in a version‑controlled file. Consumer teams (marketing, email, external partners) must reference the exact version. When you need to change the path, bump the version and keep the old one active for a deprecation window.

---

Common Deep Links Bugs and How to Catch Them: Test Matrix and Tooling

The table below contrasts four approaches to deep link validation, showing effort, coverage, and typical catch‑rate for the bug patterns described earlier.

ApproachSetup EffortExecution SpeedBug Patterns DetectedTypical False‑Negative RateIdeal Use
Manual Spot‑CheckLow (adb commands)Slow (human)1‑4, 7‑9 (config)High (misses timing, state)Quick sanity checks before a build
Unit / Instrumented TestsMedium (write test code)Fast (seconds)1‑6, 10‑12 (logic)Low (deterministic)CI gate, regression safety
CI Link LintLow‑Medium (script)Fast (ms per link)1‑3, 7‑9 (syntax/registry)Medium (misses runtime)Pre‑merge link validation
Persona‑Driven Autonomous (SUSA)Low (pip install susatest-agent)Medium‑High (depends on depth)All 1‑12 (including lifecycle, timing, accessibility)Very Low (explores state space)Release‑candidate validation, regression learning

Key observations

Tooling Snippets

Install SUSA agent


pip install susatest-agent  

Run a basic deep‑link crawl on an APK


susatest-agent run \
    --apk path/to/app.apk \
    --personas curious impatient elderly accessibility \
    --deep-link-scheme https \
    --deep-link-host example.com \
    --max-steps 2000 \
    --output report.json  

Extract failures from the JSON report


jq '.failures[] | select(.type == "deepLinkMismatch")' report.json  

Espresso test that validates intent‑filter resolution


@Test  
fun manifestContainsCorrectFilter() {  
    val pm = ApplicationProvider.getApplicationContext().packageManager  
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com/product/99"))  
    val resolved = pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)  
    assertNotNull(resolved)  
    assertEquals("com.example.app.ProductDetailActivity", resolved.activityInfo.name)  
}  

---

Common Deep Links Bugs and How to Catch Them: Real‑World Examples

Example 1: E‑Commerce App – Missing onNewIntent()

Symptom: Users who receive a promotional push notification with a deep link to a sale item see the app open to the home screen instead of the product page.

Root Cause: The ProductDetailActivity only read the intent in onCreate(). When the app was already running in the background, Android delivered the new intent via onNewIntent(), which was unimplemented, so the activity retained its old intent (home screen).

Detection:

Fix: Added onNewIntent() that called setIntent() and delegated to the same view‑model loader.

Example 2: News Reader – Universal Link AASA Misconfiguration

Symptom: After updating the iOS app, tapping a link from an email opened Safari instead of the app.

Root Cause: The new build omitted the applinks: entry from the associated domains entitlement, while the AASA file remained correct.

Detection:

Fix: Re‑added applinks:news.example.com to the entitlements, rebuilt, and re‑submitted to TestFlight.

Example 3: Banking App – Query Parameter Encoding Bug

Symptom: A deep link containing an accented character in the ref parameter (ref=café) caused a crash with IllegalArgumentException: Illegal character in query.

Root Cause: The link was built via string concatenation: "https://bank.example.com/transfer?ref=" + ref. The accented é was not percent‑encoded, producing a raw byte in the URI that Android’s Uri.parse() rejected.

Detection:

Fix: Refactored to use Uri.Builder.appendQueryParameter("ref", ref) which automatically percent‑encodes.

Example 4: Travel App – Session Loss After Deep Link from Notification

Symptom: After a user logged in, closed the app, and later tapped a “Check-in” link from a push notification, the app prompted for login again.

Root Cause: The deep link launched a fresh process that did not read the persisted auth token from EncryptedSharedPreferences because the token‑loading code resided only in the MainActivity’s onCreate(), which was never called for the deep‑link target activity.

Detection:

Fix: Moved token initialization to a singleton AuthRepository that is lazily instantiated on first access, called from every activity’s onCreate().

---

Common Deep Links Bugs and How to Catch Them: Checklist for Release

Before you tag a release, run through this concise list. Each item maps directly to one or more bug patterns from the catalog.

✅ ItemWhat to VerifyRelated Bug #
Manifest / Entitlements auditAll tags match the domains and paths you publish; iOS associated‑domains entitlement contains applinks: for each domain.1, 2, 7, 8
Link‑builder unit testsEvery helper that constructs a URI uses Uri.Builder/URLComponents and passes a suite of valid/invalid inputs (empty, Unicode, spaces, special chars).4, 11, 12
onNewIntent() coverageEvery activity that declares an intent‑filter overrides onNewIntent() and calls setIntent(intent) before processing.5
Task‑stack flagsActivities handling deep links have launchMode="singleTop" or appropriate flags in manifest; no FLAG_ACTIVITY_NEW_TASK without CLEAR_TOP.6
AASA / Digital Asset Links file reachablehttps://domain.com/apple-app-site-association returns valid JSON; https://domain.com/.well-known/assetlinks.json returns correct SHA256.7, 8
Query‑parameter validationHandler checks for presence, type, and range of each expected parameter; logs malformed input and shows user‑friendly error.3, 4, 10
Session persistence checkAfter adb shell am force-stop, launching a deep link that requires auth does not show login screen if a second time.9, 10
Accessibility announcementAfter deep‑link load, TalkBack/VoiceOver reads a meaningful message (e.g., “Product details loaded”).12
CI link lint passesNo link in documentation, emails, or deep‑link config fails the registry validation.1, 2, 3, 7
Autonomous exploration report zero high‑severity failuresRun SUSA agent with all personas; ensure no crashes, ANRs, or mis‑routed deep links remain.1‑12 (all)

If any item fails, treat it as a blocker and fix before promoting the build to staging.

---

Common Deep Links

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free