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
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:
- Open the app via launcher.
- Press Home, leave it in background.
- Execute:
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:
- Log in normally.
- Kill the app via
adb shell am force-stop. - Launch deep link that requires auth.
- 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
- Manifest / Entitlements audit – grep for
tags and verify scheme/host/path against known links. - ADB / Simulator launch – run
adb shell am start -W -a android.intent.action.VIEW -d ""andxcrun simctl openurl booted "". - Observe UI – confirm the correct screen appears, data is populated, and Back navigation behaves as expected.
- Check logs – look for
ActivityNotFoundException,NullPointerException, or warnings about intent resolution. - 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:
- Starts the app from a cold state.
- Sends an intent via
adb shell am start(orXCUITest’saddInterrupt). - Waits for a specific element (e.g., product title) to appear.
- 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:
- Pulls all
tags from your web pages or marketing emails. - Extracts the URI and validates it against a schema (scheme, host, allowed paths, required query params).
- Fails the build if any link does not resolve to a registered activity.
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:
- Click a link while the app is in the background.
- Receive a link via a notification that launches the app from a locked screen.
- Paste a malformed link into the app’s share sheet.
- Use assistive technologies that change timing and focus.
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:
- Generates thousands of URI variations (scheme, host, path, query, fragment).
- Sends them via
adb shell am startor universal link calls while the app is in different lifecycle states (cold, foreground, background, not‑running). - Monitors for crashes, ANRs, unresponsive UI, incorrect screen, missing accessibility announcements, and unexpected task stack changes.
- 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:
- Extracts all links from your markdown, HTML, and JSON payloads.
- Checks each against the registry (or a Swagger‑like OpenAPI spec for links).
- 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:
- Verify required parameters exist and are of correct type.
- Provide a fallback UI (e.g., show an error screen) rather than crashing.
- Log the incoming intent for diagnostics.
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.
| Approach | Setup Effort | Execution Speed | Bug Patterns Detected | Typical False‑Negative Rate | Ideal Use |
|---|---|---|---|---|---|
| Manual Spot‑Check | Low (adb commands) | Slow (human) | 1‑4, 7‑9 (config) | High (misses timing, state) | Quick sanity checks before a build |
| Unit / Instrumented Tests | Medium (write test code) | Fast (seconds) | 1‑6, 10‑12 (logic) | Low (deterministic) | CI gate, regression safety |
| CI Link Lint | Low‑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
- Manual checks catch obvious manifest errors but cannot reproduce race conditions.
- Unit tests verify logic but rely on the developer to write them for each new link.
- CI lint guarantees that every link in your documentation maps to a registered handler, preventing “link rot”.
- Autonomous exploration adds the dimension of *user state* (background, foreground, locked screen, accessibility settings) and *link variations* (malformed queries, extra path segments, Unicode). It is the only method that reliably surfaces bugs #5 (missing
onNewIntent), #6 (task‑stack flags), #10 (session loss), and #11 (encoding edge cases).
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:
- Manual test: launch app, press Home, then
adb shell am start -W -a android.intent.action.VIEW -d "https://shop.example.com/sale/123". - Autonomous test: SUSA agent sent the same link while the app was in the background with a power‑user persona (rapid taps, background/foreground toggles).
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:
- Manual:
xcrun simctl openurl booted "https://news.example.com/article/456"showed Safari launch. - CI link lint caught the missing entitlement because the script validates the entitlements file against the domain list.
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:
- Unit test for the link‑builder caught the issue when fed a string with Unicode.
- SUSA agent’s adversarial persona generated random Unicode query values and triggered the crash.
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:
- Manual: force‑stop the app, then send the deep link.
- Autonomous: SUSA agent’s “elderly persona” (slow interactions, frequent app kills) reproduced the issue consistently.
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.
| ✅ Item | What to Verify | Related Bug # |
|---|---|---|
| Manifest / Entitlements audit | All tags match the domains and paths you publish; iOS associated‑domains entitlement contains applinks: for each domain. | 1, 2, 7, 8 |
| Link‑builder unit tests | Every 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() coverage | Every activity that declares an intent‑filter overrides onNewIntent() and calls setIntent(intent) before processing. | 5 |
| Task‑stack flags | Activities 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 reachable | https://domain.com/apple-app-site-association returns valid JSON; https://domain.com/.well-known/assetlinks.json returns correct SHA256. | 7, 8 |
| Query‑parameter validation | Handler checks for presence, type, and range of each expected parameter; logs malformed input and shows user‑friendly error. | 3, 4, 10 |
| Session persistence check | After adb shell am force-stop, launching a deep link that requires auth does not show login screen if a second time. | 9, 10 |
| Accessibility announcement | After deep‑link load, TalkBack/VoiceOver reads a meaningful message (e.g., “Product details loaded”). | 12 |
| CI link lint passes | No link in documentation, emails, or deep‑link config fails the registry validation. | 1, 2, 3, 7 |
| Autonomous exploration report zero high‑severity failures | Run 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