Skip to content
Back to skills

Fastly Compute Well Known Spa Fallback

ASecurity

Fix .well-known files (apple-app-site-association, assetlinks.json) being served as HTML by the @fastly/compute-js-static-publish SPA fallback instead of JSON. Use when: (1) iOS Universal Links or Android App Links are broken because the verification files return text/html instead of application/json, (2) PublisherServer with spaFile config intercepts /.well-known/ paths and returns index.html (200, text/html) instead of 404 for missing files, (3) apple-app-site-association (no file extension...

  • 265 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added May 27, 2026
developmentjavascriptgojavabashtestingdocumentation

Works with

  • claude code
  • cli

Security analysis

A92/100
  • mediumUses curl or wget to download content

Pro shows the line behind each finding and how to fix it

Scanned May 27, 2026

npx -y skills add divinevideo/divine-mobile --skill fastly-compute-well-known-spa-fallback --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Fastly Compute Well Known Spa Fallback?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Fastly Compute Well Known Spa Fallback
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/divinevideo-fastly-compute-well-known-spa-fallback/badge)](https://www.skillsdirectory.com/skills/divinevideo-fastly-compute-well-known-spa-fallback)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: fastly-compute-well-known-spa-fallback
description: |
  Fix .well-known files (apple-app-site-association, assetlinks.json) being served as HTML
  by the @fastly/compute-js-static-publish SPA fallback instead of JSON. Use when: (1) iOS
  Universal Links or Android App Links are broken because the verification files return
  text/html instead of application/json, (2) PublisherServer with spaFile config intercepts
  /.well-known/ paths and returns index.html (200, text/html) instead of 404 for missing
  files, (3) apple-app-site-association (no file extension) gets wrong content type even
  when correctly stored in KV. Covers both apex domain and subdomain handler patterns.
author: Claude Code
version: 1.0.0
date: 2026-02-23
---

# Fastly Compute: .well-known Files Intercepted by SPA Fallback

## Problem

When using `@fastly/compute-js-static-publish` with SPA fallback configured (`spaFile: "/index.html"`), the `PublisherServer.serveRequest()` returns `index.html` with status 200 and Content-Type `text/html` for ANY path not found in the KV store. This includes `/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json`, which iOS and Android require to be served as `application/json`.

Symptoms:
- iOS Universal Links don't work (Apple's verification fetches `.well-known/apple-app-site-association` and gets HTML)
- Android App Links don't work (Google's verifier fetches `.well-known/assetlinks.json` and gets HTML)
- `curl -I https://yourdomain.com/.well-known/apple-app-site-association` shows `Content-Type: text/html`
- The files ARE published to KV (confirmed by `npm run fastly:publish`) but still return HTML

## Non-Obvious Root Causes

1. **SPA fallback returns 200, not 404**: The publisher's SPA mode returns `index.html` with HTTP 200 for missing paths. You cannot distinguish "file served" from "fallback served" by status code alone - you must inspect the Content-Type.

2. **`apple-app-site-association` has no file extension**: The static publisher infers MIME type from file extension. With no extension, it cannot detect `application/json`, so even when the file IS published to KV, it may get `application/octet-stream` or be served incorrectly.

3. **`includeWellKnown: true` in `publish-content.config.js` is necessary but not sufficient**: It ensures the files are uploaded to KV, but doesn't prevent the SPA fallback from intercepting the requests, and doesn't fix the content-type for extension-less files.

4. **Both apex domain and subdomain handlers need the fix**: If your Compute handler has separate code paths for subdomains vs apex, both paths must intercept `.well-known/` requests before reaching the SPA fallback.

## Solution

### Step 1: Ensure Files Are Published

In `publish-content.config.js`, confirm `includeWellKnown: true` is set:

```javascript
// publish-content.config.js
module.exports = {
  // ...
  includeWellKnown: true,   // Must be true to include /.well-known/ files
  // ...
};
```

Then publish static content:
```bash
npm run fastly:publish
```

### Step 2: Intercept .well-known Paths Before SPA Fallback

In your Compute entry point (`compute-js/src/index.js`), add a `.well-known` handler
BEFORE any call to `publisherServer.serveRequest(request)` that has SPA fallback enabled.

**Critical guard**: Check that the publisher response is NOT `text/html` - if it is,
the SPA fallback fired (file not in KV), so return 404 instead of the HTML.

```javascript
// In your main handleRequest function, BEFORE the final publisherServer.serveRequest() call:

// Handle .well-known requests (must come before SPA fallback)
if (url.pathname.startsWith('/.well-known/')) {
  // Handle NIP-05 or other dynamic .well-known endpoints first
  if (url.pathname === '/.well-known/nostr.json') {
    return handleNip05(url);  // Your custom handler
  }

  // For all other .well-known files: fetch from static publisher
  const wkResponse = await publisherServer.serveRequest(request);

  // CRITICAL: Guard against SPA fallback. The publisher returns index.html (text/html)
  // for files not in KV. We must detect this and return 404 instead.
  if (
    wkResponse != null &&
    wkResponse.status === 200 &&
    !wkResponse.headers.get('Content-Type')?.includes('text/html')
  ) {
    const headers = new Headers(wkResponse.headers);

    // Explicitly set correct content type.
    // apple-app-site-association has no extension, so the publisher may not detect JSON.
    const isJsonFile =
      url.pathname.endsWith('.json') ||
      url.pathname.endsWith('/apple-app-site-association') ||
      url.pathname === '/.well-known/apple-app-site-association';

    headers.set(
      'Content-Type',
      isJsonFile ? 'application/json' : (headers.get('Content-Type') || 'application/octet-stream')
    );
    headers.set('Cache-Control', 'public, max-age=3600');
    headers.append('Vary', 'X-Original-Host');  // If using multi-service routing

    return new Response(wkResponse.body, { status: 200, headers });
  }

  // File not in KV (or publisher returned SPA fallback) - return proper 404
  return new Response('Not Found', { status: 404 });
}
```

### Step 3: Apply the Same Fix in Subdomain Handlers

If you have separate handling for subdomain requests, add the same guard there too.
Subdomain paths hit a different code branch before reaching the apex domain handler:

```javascript
if (subdomain) {
  if (url.pathname.startsWith('/.well-known/')) {
    if (url.pathname === '/.well-known/nostr.json') {
      return handleSubdomainNip05(subdomain);
    }

    // Same pattern: intercept, guard against SPA fallback, force JSON content type
    const wkResponse = await publisherServer.serveRequest(request);
    if (
      wkResponse != null &&
      wkResponse.status === 200 &&
      !wkResponse.headers.get('Content-Type')?.includes('text/html')
    ) {
      const headers = new Headers(wkResponse.headers);
      const contentType =
        url.pathname.endsWith('.json') || url.pathname.endsWith('/apple-app-site-association')
          ? 'application/json'
          : headers.get('Content-Type') || 'application/octet-stream';
      headers.set('Content-Type', contentType);
      headers.set('Cache-Control', 'public, max-age=3600');
      return new Response(wkResponse.body, { status: 200, headers });
    }
    return new Response('Not Found', { status: 404 });
  }

  // ... rest of subdomain handling
}
```

## Verification

```bash
# Should return application/json, NOT text/html
curl -sI https://yourdomain.com/.well-known/apple-app-site-association | grep -i content-type

# Should return JSON body
curl -s https://yourdomain.com/.well-known/apple-app-site-association | head -c 100

# Android assetlinks.json
curl -sI https://yourdomain.com/.well-known/assetlinks.json | grep -i content-type

# Verify the SPA fallback guard works (path that does NOT exist in KV)
curl -sI https://yourdomain.com/.well-known/nonexistent-file
# Should return 404, not 200
```

## Complete Working Example

From `compute-js/src/index.js` in divine-web:

```javascript
// 4. Handle .well-known requests
if (url.pathname.startsWith('/.well-known/')) {
  // 4a. NIP-05 from KV store
  if (url.pathname === '/.well-known/nostr.json') {
    return await handleNip05(url);
  }

  // 4b. Serve other .well-known files (apple-app-site-association, assetlinks.json)
  // These must be served as JSON, not the SPA fallback.
  // apple-app-site-association has no file extension, so the static publisher
  // cannot detect its content type - we handle it explicitly here.
  const wkResponse = await publisherServer.serveRequest(request);
  // Guard: if publisher returns text/html, it's the SPA fallback, not the real file
  if (wkResponse != null && wkResponse.status === 200 && !wkResponse.headers.get('Content-Type')?.includes('text/html')) {
    const headers = new Headers(wkResponse.headers);
    // Ensure correct content type for app association files
    const contentType = url.pathname.endsWith('.json') || url.pathname.endsWith('/apple-app-site-association')
      ? 'application/json'
      : headers.get('Content-Type') || 'application/octet-stream';
    headers.set('Content-Type', contentType);
    headers.set('Cache-Control', 'public, max-age=3600');
    headers.append('Vary', 'X-Original-Host');
    return new Response(wkResponse.body, {
      status: 200,
      headers,
    });
  }
  // File not found in KV - return 404 instead of SPA fallback
  return new Response('Not Found', { status: 404 });
}
```

## Deployment Checklist

After making code changes:

```bash
# 1. Publish static content first (uploads .well-known files to KV)
npm run fastly:publish

# 2. Deploy the edge worker code (with the .well-known interception logic)
npm run fastly:deploy

# NOTE: Order matters if files weren't in KV before. If you deploy code first,
# it will correctly return 404 for missing files. Then publish uploads the files.
# Either order works - the guard handles both cases.
```

## Notes

- This pattern applies to any Fastly Compute service using `@fastly/compute-js-static-publish` with `spaFile` configured.
- The SPA fallback is intentional for client-side routing, but it breaks any path that needs a real 404 (like `.well-known` verification files).
- The content-type detection by file extension is a fundamental limitation of static publishing - extension-less files always need explicit handling.
- If you serve multiple domains (apex + subdomains), each code path that can call `publisherServer.serveRequest()` needs the `.well-known` interception guard.
- After `fastly:publish`, allow up to 2-3 minutes for KV propagation before testing.

## References

- [Apple Universal Links documentation](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app)
- [Android App Links documentation](https://developer.android.com/training/app-links/verify-android-applinks)
- [@fastly/compute-js-static-publish on npm](https://www.npmjs.com/package/@fastly/compute-js-static-publish)
- [Fastly Compute KV Store](https://www.fastly.com/documentation/guides/compute/javascript/working-with-kv-store/)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…