本文へ移動
cccskills
無料GitHub で公開

fastly-compute-well-known-spa-fallback

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.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md9.9 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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:

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

Then publish static content:

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.

// 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:

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

# 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:

// 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:

# 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

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Fix ArgoCD ExternalSecret deployment failing with "namespace X is not permitted in project Y". Use when: (1) ExternalSecret shows OutOfSync in ArgoCD but won't sync, (2) ArgoCD application status shows "namespace X is not permitted in project 'infrastructure'", (3) ExternalSecret targets a namespace managed by a different ArgoCD project, (4) Using apps-of-apps pattern with separate infrastructure and application projects.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Art direction for any content — reads text, PDF, Word, HTML, PPT, then proposes 2-3 creative directions with photography style, mood, and visual language. After selection, generates AI image prompts and visual briefs section-by-section. Use when the user shares content and needs visual direction, image sourcing, or creative direction for any material.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Fix "Null check operator used on a null value" errors when an object is set to null during an async await. Use when: (1) Object reference is nullified while awaiting, (2) Code accesses object with ! after await returns, (3) Cancel/dispose operations run concurrently with async operations on same object. Solution: capture local reference before await.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Add custom metadata headers (x-amz-meta-*) to AWS v4 signed requests for GCS S3-compatible API. Use when: (1) Adding custom metadata to GCS uploads via S3 API, (2) Getting signature mismatch errors after adding new headers, (3) x-amz-meta-* headers being ignored or causing 403 errors. Custom headers MUST be included in canonical headers and signed headers list.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Fix password/secret authentication failures caused by trailing newlines when creating Google Cloud secrets (or similar) with bash here-strings. Use when: (1) Password authentication fails with correct password, (2) Secret created with `<<< "value"` syntax, (3) Error like "password authentication failed" or "invalid token" despite correct value. Bash here-strings (`<<<`) add a trailing newline that corrupts secrets.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

Fix silent video/media processing failures caused by URL extraction code that filters on file extensions (.mp4, .webm, .webp). Use when: (1) Media moderation, transcoding, or analysis silently skips files from Blossom or content-addressed storage servers, (2) URL extraction from Nostr event tags (imeta, r tags) drops URLs without recognized extensions, (3) CDN fallback URLs append .mp4 but the actual server uses extensionless content-addressed paths like /{sha256}. Common in Nostr video events (kind 34236) where different clients use different URL formats.

日本語の概要は準備中です。原文の説明を表示しています。

divinevideo/divine-mobile2662026年10月13日 更新

divinevideo のスキルをすべて見る

このスキルの問題を報告する