fastly-compute-well-known-spa-fallback

Par divinevideo · divine-mobile

Corrige les fichiers `.well-known` (`apple-app-site-association`, `assetlinks.json`) servis en HTML par le fallback SPA de `@fastly/compute-js-static-publish` au lieu de JSON. À utiliser quand : (1) les Universal Links iOS ou App Links Android sont cassés parce que les fichiers de vérification renvoient `text/html` au lieu de `application/json`, (2) `PublisherServer` avec la config `spaFile` intercepte les chemins `/.well-known/` et retourne `index.html` (200, `text/html`) au lieu de 404 pour les fichiers manquants, (3) `apple-app-site-association` (sans extension de fichier) reçoit un content type incorrect même lorsqu'il est correctement stocké dans le KV. Couvre les patterns de handler pour domaine apex et sous-domaine.

npx skills add https://github.com/divinevideo/divine-mobile --skill fastly-compute-well-known-spa-fallback

Fastly Compute : Fichiers .well-known Interceptés par la Fallback SPA

Problème

Lors de l'utilisation de @fastly/compute-js-static-publish avec la fallback SPA configurée (spaFile: "/index.html"), la méthode PublisherServer.serveRequest() retourne index.html avec le statut 200 et le Content-Type text/html pour N'IMPORTE QUEL chemin non trouvé dans le KV store. Cela inclut /.well-known/apple-app-site-association et /.well-known/assetlinks.json, que iOS et Android exigent d'être servis en tant que application/json.

Symptômes :

  • Les Universal Links iOS ne fonctionnent pas (la vérification d'Apple récupère .well-known/apple-app-site-association et obtient du HTML)
  • Les App Links Android ne fonctionnent pas (le vérificateur de Google récupère .well-known/assetlinks.json et obtient du HTML)
  • curl -I https://yourdomain.com/.well-known/apple-app-site-association affiche Content-Type: text/html
  • Les fichiers SONT publiés vers KV (confirmé par npm run fastly:publish) mais retournent quand même du HTML

Causes Racines Non Évidentes

  1. La fallback SPA retourne 200, pas 404 : Le mode SPA du publisher retourne index.html avec le HTTP 200 pour les chemins manquants. Vous ne pouvez pas distinguer « fichier servi » de « fallback servie » par le code de statut seul — vous devez inspecter le Content-Type.

  2. apple-app-site-association n'a pas d'extension de fichier : L'éditeur statique déduit le type MIME de l'extension de fichier. Sans extension, il ne peut pas détecter application/json, donc même si le fichier EST publié vers KV, il peut obtenir application/octet-stream ou être servi incorrectement.

  3. includeWellKnown: true dans publish-content.config.js est nécessaire mais pas suffisant : Cela garantit que les fichiers sont téléchargés vers KV, mais n'empêche pas la fallback SPA d'intercepter les demandes, et ne corrige pas le content-type pour les fichiers sans extension.

  4. Les gestionnaires du domaine apex et des sous-domaines ont besoin du correctif : Si votre gestionnaire Compute a des chemins de code séparés pour les sous-domaines par rapport à l'apex, les deux chemins doivent intercepter les demandes /.well-known/ avant d'atteindre la fallback SPA.

Solution

Étape 1 : Assurer que les Fichiers sont Publiés

Dans publish-content.config.js, confirmez que includeWellKnown: true est défini :

// publish-content.config.js
module.exports = {
  // ...
  includeWellKnown: true,   // Doit être true pour inclure les fichiers /.well-known/
  // ...
};

Puis publiez le contenu statique :

npm run fastly:publish

Étape 2 : Intercepter les Chemins .well-known Avant la Fallback SPA

Dans votre point d'entrée Compute (compute-js/src/index.js), ajoutez un gestionnaire .well-known AVANT tout appel à publisherServer.serveRequest(request) qui a la fallback SPA activée.

Garde critique : Vérifiez que la réponse du publisher n'est PAS text/html — si elle l'est, la fallback SPA a été déclenchée (fichier non dans KV), donc retournez 404 à la place du HTML.

// Dans votre fonction handleRequest principale, AVANT l'appel final à publisherServer.serveRequest() :

// Gérer les demandes .well-known (doit venir avant la fallback SPA)
if (url.pathname.startsWith('/.well-known/')) {
  // Gérer d'abord les points de terminaison dynamiques .well-known comme NIP-05
  if (url.pathname === '/.well-known/nostr.json') {
    return handleNip05(url);  // Votre gestionnaire personnalisé
  }

  // Pour tous les autres fichiers .well-known : récupérer du publisher statique
  const wkResponse = await publisherServer.serveRequest(request);

  // CRITIQUE : Protection contre la fallback SPA. Le publisher retourne index.html (text/html)
  // pour les fichiers non dans KV. Nous devons détecter cela et retourner 404 à la place.
  if (
    wkResponse != null &&
    wkResponse.status === 200 &&
    !wkResponse.headers.get('Content-Type')?.includes('text/html')
  ) {
    const headers = new Headers(wkResponse.headers);

    // Définir explicitement le type de contenu correct.
    // apple-app-site-association n'a pas d'extension, donc le publisher peut ne pas détecter 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');  // Si vous utilisez un routage multi-service

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

  // Fichier non dans KV (ou publisher a retourné la fallback SPA) — retourner un 404 approprié
  return new Response('Not Found', { status: 404 });
}

Étape 3 : Appliquer le Même Correctif dans les Gestionnaires de Sous-domaine

Si vous avez un traitement séparé pour les demandes de sous-domaine, ajoutez la même protection aussi. Les chemins de sous-domaine passent par une branche de code différente avant d'atteindre le gestionnaire du domaine apex :

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

    // Même motif : intercepter, protection contre la fallback SPA, forcer le type de contenu JSON
    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 });
  }

  // ... reste du traitement des sous-domaines
}

Vérification

# Doit retourner application/json, PAS text/html
curl -sI https://yourdomain.com/.well-known/apple-app-site-association | grep -i content-type

# Doit retourner un corps JSON
curl -s https://yourdomain.com/.well-known/apple-app-site-association | head -c 100

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

# Vérifier que la protection de la fallback SPA fonctionne (chemin qui N'EXISTE PAS dans KV)
curl -sI https://yourdomain.com/.well-known/nonexistent-file
# Doit retourner 404, pas 200

Exemple Complet Fonctionnant

Depuis compute-js/src/index.js dans divine-web :

// 4. Gérer les demandes .well-known
if (url.pathname.startsWith('/.well-known/')) {
  // 4a. NIP-05 depuis le KV store
  if (url.pathname === '/.well-known/nostr.json') {
    return await handleNip05(url);
  }

  // 4b. Servir les autres fichiers .well-known (apple-app-site-association, assetlinks.json)
  // Ceux-ci doivent être servis en tant que JSON, pas la fallback SPA.
  // apple-app-site-association n'a pas d'extension de fichier, donc l'éditeur statique
  // ne peut pas détecter son type de contenu — nous le gérons explicitement ici.
  const wkResponse = await publisherServer.serveRequest(request);
  // Protection : si le publisher retourne text/html, c'est la fallback SPA, pas le vrai fichier
  if (wkResponse != null && wkResponse.status === 200 && !wkResponse.headers.get('Content-Type')?.includes('text/html')) {
    const headers = new Headers(wkResponse.headers);
    // Assurer le type de contenu correct pour les fichiers d'association d'app
    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,
    });
  }
  // Fichier non trouvé dans KV — retourner 404 au lieu de la fallback SPA
  return new Response('Not Found', { status: 404 });
}

Checklist de Déploiement

Après avoir effectué les modifications de code :

# 1. Publier d'abord le contenu statique (télécharge les fichiers .well-known vers KV)
npm run fastly:publish

# 2. Déployer le code du worker edge (avec la logique d'interception .well-known)
npm run fastly:deploy

# REMARQUE : L'ordre a de l'importance si les fichiers n'étaient pas dans KV auparavant. Si vous déployez d'abord le code,
# il retournera correctement 404 pour les fichiers manquants. Puis la publication télécharge les fichiers.
# N'importe quel ordre fonctionne — la protection gère les deux cas.

Notes

  • Ce motif s'applique à tout service Fastly Compute utilisant @fastly/compute-js-static-publish avec spaFile configuré.
  • La fallback SPA est intentionnelle pour le routage côté client, mais elle casse tout chemin qui a besoin d'un vrai 404 (comme les fichiers de vérification .well-known).
  • La détection du content-type par extension de fichier est une limitation fondamentale de la publication statique — les fichiers sans extension ont toujours besoin d'un traitement explicite.
  • Si vous servez plusieurs domaines (apex + sous-domaines), chaque chemin de code qui peut appeler publisherServer.serveRequest() a besoin de la protection d'interception .well-known.
  • Après fastly:publish, attendez jusqu'à 2-3 minutes pour la propagation KV avant de tester.

Références

Skills similaires