
Technology
Our iOS Deep Link Looked Correct — Until AWS Amplify Returned a 301
iOS Universal Links can fail even when the app configuration looks correct. In this case, an AWS Amplify redirect on Apple’s extensionless association file quietly broke the trust chain. Here’s how I diagnosed and fixed it.
Deep linking sounds straightforward until several platforms have to agree on the same URL.
A user taps an HTTPS link. The operating system recognises the domain. The native app opens. The user lands on the right screen.
At least, that is how it is supposed to work.
Recently, I worked on the infrastructure side of an iOS Universal Links implementation where the configuration initially looked correct. The required association file existed. The domain was live over HTTPS. The mobile team knew which domain needed to be associated with the app.
But the link still was not behaving the way we expected.
The issue turned out not to be in the mobile application's routing logic.
It was an HTTP redirect.
More specifically, it was the way AWS Amplify handled an extensionless path that Apple expects to be served exactly as requested.
That small difference between “the file exists” and “the file is served exactly the way Apple expects” was the real problem.
What iOS Universal Links actually depend on
Universal Links allow a normal HTTPS URL to open a native iOS application instead of Safari.
For that to work, iOS needs to establish trust between two sides: the website and the application.
On the website side, the domain hosts an apple-app-site-association file, commonly called the AASA file.
On the application side, the app declares the same domain using Apple’s Associated Domains entitlement.
Those two sides must agree.
If the website does not correctly identify the application, the association fails. If the application does not declare the domain correctly, the association fails.
And if the AASA file is not delivered correctly over HTTP, the association can fail even when the contents of the file are perfectly valid.
In this case, my responsibility was the website and infrastructure side: make sure the association file was available from the expected location and returned correctly. The mobile team would handle the application entitlement and routing logic inside the app.
The infrastructure requirement sounded simple.
Apple expects the file at a path similar to:
Notice something unusual about that URL.
There is no .json extension.
That detail became important very quickly.
The first clue was not inside the JSON
The association file itself was not the first thing I needed to debug.
The HTTP response was.
Instead of immediately looking at the contents of the file, I checked what the server was actually returning.
I used curl to inspect three things:
The HTTP status code.
The number of redirects.
The Content-Type.
For this endpoint, the result I wanted was effectively:
status=200 redirects=0 content-type=application/json
Those three values matter.
The file should return HTTP 200.
The response should be JSON.
And critically, there should be no redirect.
But the server was not giving me that clean response.
The extensionless path was redirecting.
That was the real clue.
AWS Amplify saw a directory where Apple expected a file
The application frontend was hosted using AWS Amplify.
Amplify was perfectly happy serving normal files such as:
assetlinks.json
That file has an extension.
The Apple association file was different.
Apple expects:
apple-app-site-association
with no .json suffix in the public URL.
In this environment, Amplify treated that extensionless request more like a directory-style path.
The request received a redirect to a path with a trailing slash. From there, it could fall through to the application’s normal single-page application routing behaviour and eventually return the wrong response.
So the request effectively behaved like this:
/.well-known/apple-app-site-association
301 Redirect
/.well-known/apple-app-site-association/
Wrong response
The file existed.
The JSON could be correct.
HTTPS could be valid.
And the implementation could still fail because the request itself was not being served in the format expected by Apple.
That was the part I found most interesting.
The bug was not really:
“The AASA file is missing.”
It was:
“The hosting platform and Apple disagree about how this path should behave.”
The workaround: keep Apple’s URL, change how Amplify serves it
The solution required separating the public URL from the physical file that Amplify served internally.
Apple still needed to request:
/.well-known/apple-app-site-association
But Amplify was much happier serving:
/.well-known/apple-app-site-association.json
So I kept both ideas.
A .json version of the association file was included in the frontend’s public assets.
Then I added an Amplify rewrite rule so that the Apple-required extensionless URL internally resolved to the JSON file.
Conceptually, the rule was:
Source:
/.well-known/apple-app-site-association
Target:
/.well-known/apple-app-site-association.json
Status:
200
This distinction mattered.
The browser or Apple service still requested the extensionless path.
There was no visible redirect.
Amplify internally served the JSON file.
And the client received HTTP 200.
That preserved Apple’s expected public URL while working around the hosting platform’s behaviour.
The rewrite rule order mattered too
There was another small detail that could easily have turned this into a second debugging session.
The frontend was a single-page application.
Like many SPAs, it already had a catch-all rule similar to:
/<*> → /index.html
That rule exists so client-side routes still load the application.
But a catch-all is exactly what the name suggests.
It catches everything.
If the SPA rule runs before the AASA-specific rule, the special association-file request can be swallowed by the frontend routing behaviour.
So the order had to be:
First: the AASA-specific rewrite.
Second: the SPA catch-all.
Not the other way around.
This is one of those infrastructure details that looks trivial after you solve it.
During troubleshooting, however, it can be the difference between a working Universal Link and several more hours of investigating the mobile app.
Verification mattered more than the deployment result
Once the rewrite was deployed, I did not treat a successful deployment as proof that the problem was solved.
I went back to the HTTP response.
The important result was:
status=200 redirects=0 content-type=application/json
That gave me much more confidence than a green deployment pipeline.
It told me what an external client actually saw.
I also inspected the returned content separately to make sure the association file was being served as expected.
At that point, the infrastructure side was complete.
The remaining work belonged to the mobile application.
Knowing where DevOps responsibility ends
This incident was also a useful reminder that deep linking is a cross-team feature.
The infrastructure team can make the domain correct.
That does not automatically make the application respond to the link.
From the DevOps side, my responsibilities were primarily to make the association file reachable over HTTPS, ensure the endpoint returned the correct content, remove the redirect, make sure the hosting platform did not send the request through the SPA fallback, and verify the public endpoint externally.
The mobile team then needed to configure the Associated Domains entitlement and handle the incoming URL in the application’s routing layer.
That separation is useful during incidents because it gives everyone a clear diagnostic boundary.
If curl returns a 301, I still have an infrastructure problem.
If curl returns 200, zero redirects, and application/json, and the association file contains the expected application identifiers, then the investigation can move further into the mobile configuration and routing behaviour.
What this incident reinforced for me
The main lesson was not simply that AWS Amplify needed a rewrite rule.
It was that platform integrations often depend on details below the application layer.
A file existing in a deployment artifact does not guarantee that an external platform receives it correctly.
A valid JSON document does not guarantee that the URL serving it behaves correctly.
A successful frontend deployment does not prove that a standards-sensitive endpoint returns the required HTTP semantics.
And when several systems are involved, each one can be individually “working” while the integration between them is still broken.
In this case, Apple’s Universal Links expected an extensionless endpoint. Amplify treated that extensionless path differently from a normal JSON file. SPA routing introduced another layer capable of intercepting the request. A small rewrite rule reconciled those behaviours.
The code change itself was small.
Understanding why it was necessary was the real engineering work.
The debugging pattern I would use next time
The next time I troubleshoot Universal Links or another platform-verification endpoint, I will not begin by assuming the JSON is wrong.
I will start one layer lower.
What HTTP status code is actually returned?
Was there a redirect?
What is the Content-Type?
What does the external URL return without browser assumptions?
Is a reverse proxy, CDN, hosting platform, or SPA rewrite modifying the request?
Only after those questions are answered would I move further into the application configuration.
Because sometimes the most useful debugging tool is not Xcode, Flutter, or even the AWS console.
Sometimes it is just:
curl
And one unexpected 301 tells you almost everything you need to know.
110








