CodeGym /Cours /ChatGPT Apps /Interaction avec le serveur : window.fetch et openExterna...

Interaction avec le serveur : window.fetch et openExternal

ChatGPT Apps
Niveau 3, Leçon 4
Disponible

1. Deux voies vers l’extérieur : navigation et données

Lorsqu’un développeur Next.js entend « il faut appeler le serveur », sa main se dirige automatiquement vers fetch ou vers son client HTTP préféré. Dans le monde des ChatGPT Apps, ce réflexe mène à des douleurs.

Dans la partie du cours consacrée à la sécurité des widgets dans ChatGPT Apps, nous proposons de briser ce vieux réflexe dès le départ. Le widget ne vit pas sur l’internet libre : il est enfermé dans une isolation stricte, et son accès réseau est filtré et limité par les politiques de l’hôte.

Le widget ne dispose que de trois « fenêtres » de base vers l’extérieur :

  1. Navigation : envoyer l’utilisateur quelque part à l’extérieur. Pour cela, il y a openExternal.
  2. Échange de données : recevoir/envoyer du JSON, dialoguer avec le back‑end. Cela se fait via fetch, mais avec des restrictions potentiellement fortes.
  3. Appel d’outil MCP : appel d’outils (MCP / back‑end), qui ne subissent aucune des limitations précédentes.

Dans cette leçon, nous nous concentrons sur la première voie, la plus sûre (la navigation), et nous découvrons prudemment le fetch « contrôlé ». Dans les modules suivants, nous verrons MCP et les outils comme principal moyen d’échange sérieux avec le serveur.

2. openExternal : le « téléport » utilisateur sécurisé

Pourquoi ne pas simplement faire window.open

Dans une application web classique, vous feriez quelque chose comme :


window.open("https://example.com", "_blank");

Dans le bac à sable de ChatGPT, cela ne fonctionnera pas, ou fonctionnera de manière très étrange. Le widget est un iframe isolé avec un sandbox strict, qui n’a pas les mêmes droits qu’un onglet de navigateur.

De plus, l’hôte ChatGPT veut contrôler où et quand vous envoyez l’utilisateur, afin de :

  • ne pas permettre de tracking furtif ;
  • afficher à l’utilisateur une UI de confirmation claire (surtout sur les clients mobiles/desktop) ;
  • assurer un comportement homogène des liens dans des environnements différents (web, desktop, application mobile).

C’est pourquoi une API spéciale openExternal a été conçue, accessible via window.openai ou via le hook React plus pratique useOpenExternal.

À quoi ressemble useOpenExternal

Dans les exemples officiels de l’Apps SDK, le hook useOpenExternal est implémenté à peu près ainsi :


export function useOpenExternal() {
  const openExternal = useCallback((href: string) => {
    if (typeof window === "undefined") return;

    if (window?.openai?.openExternal) {
      try {
        window.openai.openExternal({ href });
        return;
      } catch (error) {
        console.warn("openExternal failed, falling back to window.open", error);
      }
    }

    window.open(href, "_blank", "noopener,noreferrer");
  }, []);

  return openExternal;
}

L’idée principale est simple. On essaye d’abord d’utiliser le mécanisme natif de ChatGPT (window.openai.openExternal). Si le widget est rendu en dehors de ChatGPT (par exemple, vous l’avez simplement ouvert dans le navigateur pendant le développement), on revient proprement sur window.open.

Dans votre application, ce hook est déjà présent dans le template (si vous avez pris le dépôt standard d’OpenAI), et il faut l’utiliser tel quel — et non pas manipuler directement window.openai.

Exemple : bouton « Voir dans la boutique » dans GiftGenius

Imaginons que dans le toolOutput de notre GiftGenius arrivent des recommandations avec un champ productUrl. Ajoutons à chaque carte un bouton qui ouvrira le produit sur votre site :

import { useWidgetProps } from "../hooks/use-widget-props";
import { useOpenExternal } from "../hooks/use-open-external";

export function GiftListWidget() {
  const { toolOutput } = useWidgetProps<{
    recommendations: { id: string; title: string; price: string; url: string }[];
  }>();
  const openExternal = useOpenExternal();

  if (!toolOutput) return <p>Pas encore de recommandations…</p>;

  return (
    <div>
      {toolOutput.recommendations.map((gift) => (
        <div key={gift.id} className="flex justify-between gap-2">
          <div>
            <div>{gift.title}</div>
            <div className="text-sm text-muted-foreground">{gift.price}</div>
          </div>
          <button onClick={() => openExternal(gift.url)}>
            Ouvrir
          </button>
        </div>
      ))}
    </div>
  );
}

Du point de vue de l’utilisateur : il clique sur le bouton, ChatGPT peut afficher une fenêtre système « Ouvrir un site externe ? », puis ouvre votre page dans un nouvel onglet ou dans le navigateur par défaut. Vous n’y envoyez aucun secret, token, etc., vous redirigez simplement la personne « du chat vers le site ».

3. window.fetch dans le bac à sable : ce n’est pas le fetch auquel vous êtes habitué

Ce à quoi le front-end s’attend habituellement

Logique habituelle : « Puisque c’est un navigateur, on peut appeler n’importe quelle URL avec CORS activé. Au pire j’aurai une erreur, mais je peux toujours essayer. »

Dans l’écosystème des ChatGPT Apps, c’est une croyance dangereuse. Le bac à sable autour du widget n’est pas un simple « pinaillage », c’est une exigence de sécurité fondamentale : pour que le widget ne puisse pas traquer l’utilisateur, appeler des domaines arbitraires, scanner le réseau local et, d’une manière générale, se comporter comme un mini‑navigateur dans le navigateur.

Le même rapport souligne que, dans l’Apps SDK, l’accès réseau arbitraire du widget est soit absent, soit fortement limité — et ce n’est pas un bug, mais un choix architectural délibéré.

À quoi cela ressemble en pratique

Dans un environnement ChatGPT typique :

  • fetch peut être disponible, mais uniquement vers une liste restreinte de domaines (généralement votre domaine où tourne l’App, et éventuellement quelques API explicitement autorisées) ;
  • les requêtes peuvent passer par un proxy spécial de l’hôte, qui filtre les en‑têtes et les URL ;
  • certaines méthodes (PUT, DELETE) ou des en‑têtes non standards peuvent être bloqués par des politiques de sécurité.

Vous disposez néanmoins d’une voie pratique : si votre widget et votre back‑end sont sur le même domaine (comme dans le template Next.js, où le serveur MCP et l’UI sont servis par la même application), les requêtes internes fetch("/api/...") seront généralement autorisées.

L’essentiel est de ne pas compter sur la possibilité pour le widget d’appeler n’importe quelle API sur internet. Toute interaction « lourde » avec des services externes (Stripe, Notion, CRM, etc.) doit se faire côté MCP/back‑end, que ChatGPT contacte comme une ressource de confiance.

Insight

Dans un widget ChatGPT, il faut oublier les chemins relatifs et vivre avec des URL absolues. Raison simple : votre HTML ne fonctionne pas sur le même domaine que le back‑end. ChatGPT lit votre HTML, le place sur son hôte et le rend dans un iframe isolé. Tout "/api/..." ou "/static/logo.png" se met soudain à se résoudre par rapport au domaine de ChatGPT, et non au vôtre — et tout s’écroule.

<base> n’aide quasiment pas ici. Il a été établi expérimentalement que si le widget n’a pas de widgetCSP défini, vous pouvez configurer <base href="https://my-app.dev/"> : les ressources seront chargées depuis votre domaine, mais les scripts, selon les règles du bac à sable, ne fonctionneront quand même pas. Mais cela ne marche qu’en Dev Mode.

Dès que vous définissez un openai/widgetCSP correct (et en production il faudra de toute façon le définir pour la validation), la plateforme réinitialise <base>, et la partie est terminée : les ressources et scripts ne se chargent que depuis les domaines autorisés dans la CSP, et via des liens déjà absolus.

Recommandation : dans un widget ChatGPT, tout ce qui part vers l’extérieur — fetch, images, CSS, vos pages pour openExternal — doit toujours être construit comme une URL complète à partir du domaine de base de l’application, que vous contrôlez via la configuration/ENV, et non via des chemins relatifs et <base>.

4. Architecture : UI fine, back‑end épais

Des limites de fetch et du bac à sable découle un principe architectural plus général, essentiel pour tout le cours. Nous l’avons déjà répété plusieurs fois, mais c’est le moment d’enfoncer le clou : le widget est une couche UI fine. Il rend ce que le back‑end a déjà préparé (via MCP/tools), affiche les réactions aux actions de l’utilisateur et, au pire, effectue quelques petites requêtes publiques.

Tout ce qui concerne l’autorisation, l’accès aux données personnelles, les secrets et la logique métier non triviale doit vivre côté serveur. Les documents de sécurité du cours soulignent séparément que le front‑end (le widget React) est un « espace public », une zone de confiance nulle, et les secrets n’ont rien à y faire.

Toutes mes recherches sur ce sujet formulent l’objectif de manière stricte : « planter le dernier clou dans le cercueil de l’idée du “client lourd” » pour les ChatGPT Apps. Le widget — seulement la tête ; le corps et le cerveau — dans le MCP/back‑end.

Donc :

  • openExternal — pour envoyer l’utilisateur vers votre « vrai » site, où l’on peut faire tourner un SPA habituel, un espace personnel, etc. ;
  • callTool (module suivant) — la méthode principale pour confier une tâche au modèle, que votre back‑end exécutera ;
  • fetch depuis le widget — un héros rare pour des requêtes auxiliaires, sûres et idéalement publiques, vers votre propre application.

5. Pratique : openExternal dans notre GiftGenius

Intégrons openExternal un peu plus proprement dans notre App pédagogique et réfléchissons au UX.

Mini‑règle UX

Si vous envoyez l’utilisateur vers l’extérieur, il est utile de :

  • indiquer clairement où il va atterrir ;
  • éviter les « sauts » inattendus sans explication dans le texte (soit GPT signale « J’ouvre le site de la boutique… », soit vous explicitez le bouton).

Exemple de titre et de libellé :

<button onClick={() => openExternal(gift.url)}>
  Ouvrir sur le site du magasin
</button>

L’utilisateur comprend qu’il va quitter la chaleur du chat pour le monde réel avec panier et paiement.

Petit refactoring du composant de liste

Nous avons déjà créé un GiftListWidget simple. Supposons que, dans les leçons précédentes, vous ayez déjà implémenté un widget qui affiche une liste de cadeaux à partir du toolOutput. Faisons maintenant une version un peu plus propre : ajoutons un type Gift avec un champ url et un bouton openExternal.

type Gift = {
  id: string;
  title: string;
  priceLabel: string;
  url: string;
};

export function GiftListWidget() {
  const { toolOutput } = useWidgetProps<{ gifts: Gift[] }>();
  const openExternal = useOpenExternal();

  if (!toolOutput || toolOutput.gifts.length === 0) {
    return <p>Je n’ai encore rien trouvé. Essayez de modifier la requête.</p>;
  }

  return (
    <div>
      {toolOutput.gifts.map((gift) => (
        <div key={gift.id} className="flex justify-between gap-2">
          <div>
            <div>{gift.title}</div>
            <div className="text-sm text-muted-foreground">
              {gift.priceLabel}
            </div>
          </div>
          <button onClick={() => openExternal(gift.url)}>
            Voir
          </button>
        </div>
      ))}
    </div>
  );
}

Nous ne manipulons toujours pas directement window.openai, mais utilisons le hook pratique — qui sait déjà revenir à window.open quand l’environnement ChatGPT n’est pas présent. La structure de Gift ici est indicative — adaptez‑la à votre back‑end dans votre App.

6. Pratique : un fetch soigné vers notre back‑end

Voyons maintenant fetch. Rappel : les opérations complexes ou sensibles sont à faire via des outils/MCP. Mais il arrive qu’on veuille, depuis le widget, récupérer quelque chose de léger et public depuis son propre serveur, par exemple la liste des catégories populaires de cadeaux.

Un routeur d’API publique simple dans Next.js

Ajoutons ce handler à notre projet Next.js :

// app/api/public/popular-tags/route.ts
import { NextResponse } from "next/server";

const tags = ["Pour les enfants", "Pour les voyageurs", "Pour les joueurs"];

export async function GET() {
  return NextResponse.json({ tags });
}

Cette route ne connaît pas l’utilisateur, ne requiert pas de tokens, n’appelle pas de services externes — elle renvoie simplement un tableau statique. Un tel code peut passer presque sans risque en production et dans le bac à sable.

Appeler cette route depuis le widget via fetch

Ajoutons maintenant le chargement de ces tags dans le composant du widget. Compte tenu des limites du bac à sable, il est plus pratique d’appeler une URL absolue : le même domaine où tourne votre App — celui que vous exposez via le tunnel et enregistrez en Dev Mode dans ChatGPT (nous l’avons configuré dans le module sur le Dev Mode et le tunnel).

Important : le domaine de votre widget ressemblera à https://genius.web-sandbox.oaiusercontent.com, donc n’utilisez pas de chemins relatifs pour charger des données, uniquement des URL absolues. Exemple :

import { useEffect, useState } from "react";

export function PopularTags() {
  const [tags, setTags] = useState<string[] | null>(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function loadTags() {
      try {
        const res = await fetch("https://giftgenius.app/api/public/popular-tags");
        if (!res.ok) throw new Error("Bad status");
        const data: { tags: string[] } = await res.json();
        if (!cancelled) setTags(data.tags);
      } catch (e) {
        if (!cancelled) setError("Impossible de charger les catégories populaires");
      }
    }

    loadTags();
    return () => {
      cancelled = true;
    };
  }, []);

  if (error) return <p>{error}</p>;
  if (!tags) return <p>Chargement des catégories populaires…</p>;

  return (
    <div className="flex flex-wrap gap-2 text-sm">
      {tags.map((tag) => (
        <span key={tag} className="rounded border px-2 py-1">
          {tag}
        </span>
      ))}
    </div>
  );
}

Points importants :

  • nous gérons proprement les erreurs et affichons un message compréhensible à l’utilisateur ;
  • nous ne supposons pas que fetch « va forcément marcher » — les politiques du bac à sable peuvent couper l’accès à tout moment si vous changez de domaine ou commencez à faire des requêtes bizarres ;
  • nous n’y passons aucun token/secret ; si une authentification est nécessaire — c’est l’affaire de MCP et des modules sur l’Auth.

7. openExternal vs fetch vs outils (callTool) : qui fait quoi

Pour éviter les confusions, gardez en tête cette « matrice des responsabilités » :

Scénario Ce qu’on utilise Pourquoi ainsi
Ouvrir une landing/un produit/un espace client openExternal Transition explicite de l’utilisateur, contrôlée par l’hôte
Obtenir des données publiques depuis l’App fetch("my.com/api/...") JSON léger, même domaine, sans secrets
Récupérer les données utilisateur, la base de données callTool/MCP Besoin d’autorisation, de logique, d’un back‑end sécurisé
Appeler des API externes (Stripe…) MCP/serveur Le front ne voit pas les secrets, respect des politiques

Dans ce module, il est important d’apprendre à choisir consciemment l’outil. Il faut passer de l’idée « le widget est du front‑end, donc on peut tout faire via fetch » à l’architecture « le widget est une couche UI pilotée au‑dessus d’un back‑end LLM+MCP ».

Insight

L’interaction avec le serveur dans une ChatGPT App se divise logiquement en deux niveaux :

  • ChatGPT ↔ serveur MCP : le modèle appelle des outils MCP. Chaque appel d’outil est le lancement ou le basculement d’un scénario métier (sélection de cadeaux, création de commande, calcul de coût, etc.). C’est là que vit la logique « lourde », le travail avec les données, les API externes et l’autorisation.
  • Widget ↔ serveur : le widget effectue de petites requêtes fetch() vers son propre back‑end et/ou appelle les mêmes outils MCP via callTool() déjà dans le scénario actif. Ce sont des étapes locales : charger des données auxiliaires, mettre à jour une partie de l’UI, préciser l’état.

Ainsi, MCP‑tool = démarrage/pilotage du processus métier, tandis que fetch()/callTool() depuis le widget — de petites opérations à l’intérieur d’un scénario déjà choisi, sans prétendre changer l’« histoire » globale du dialogue.

8. Petit exercice pratique

Pour ancrer le sujet en pratique, implémentez une petite fonctionnalité dans GiftGenius.

Scénario proposé :

  1. Dans la liste des cadeaux, ajoutez un bouton « Passer à la commande » qui ouvre, via openExternal, la page de commande sur votre site de dev.
  2. Au‑dessus de la liste des cadeaux, rendez PopularTags de l’exemple ci‑dessus pour afficher les catégories populaires. En cas d’erreur de chargement, affichez un texte de repli et ne cassez pas tout le widget.
  3. Faites attention à l’UX : dans le texte de la réponse GPT ou dans l’UI du widget, expliquez à l’utilisateur que « en appuyant sur le bouton, j’ouvrirai la page de la boutique dans un nouvel onglet ».

Cette fonctionnalité illustre en miniature les deux canaux :

  • openExternal pour la navigation explicite ;
  • fetch pour une petite API publique, vivant à côté de votre App.

9. Erreurs courantes avec window.fetch et openExternal

Erreur n°1 : essayer d’utiliser le widget comme un client SPA complet pour toutes vos API.
Les vieilles habitudes poussent fort vers « et si on appelait juste notre REST/GraphQL directement depuis React ». Dans le monde des ChatGPT Apps, cela conduit à un choc frontal avec le bac à sable : certaines requêtes ne passeront tout simplement pas, d’autres seront bloquées par des politiques, et la sécurité du projet sera compromise. La logique complexe et l’accès aux données utilisateur doivent passer par MCP/outils, et non directement depuis le widget.

Erreur n°2 : stocker des secrets et des tokens dans le code du widget.
On a parfois envie de « prototyper vite » et de coller dans le code front‑end une clé d’API pour un service (« je ne fais que tester »). Mauvaise idée même pour un SPA classique, et pour les ChatGPT Apps — un non catégorique. Le widget est un environnement public ; les secrets doivent vivre dans la configuration du serveur ou dans des systèmes de gestion de secrets (env Vercel, KMS, etc.).

Erreur n°3 : penser que fetch vers n’importe quel domaine « va juste marcher ».
Même si, en Dev Mode, une requête est passée (par exemple parce que le tunnel était configuré de manière atypique), en production elle se cassera presque à coup sûr : ChatGPT restreint les requêtes sortantes, et un domaine externe arbitraire n’est pas accessible au widget. Partez du principe que le widget peut de manière fiable appeler uniquement son propre domaine et un très petit ensemble de ressources explicitement autorisées.

Erreur n°4 : utiliser window.open au lieu de openExternal.
Techniquement, window.open peut parfois fonctionner, surtout dans un aperçu navigateur, et cela crée l’illusion que « tout va bien ». Mais dans un vrai environnement ChatGPT, en particulier dans les clients natifs, le comportement sera imprévisible. L’utilisateur peut ne pas voir la navigation du tout ou recevoir une erreur étrange. La bonne voie — utiliser openExternal (via le hook useOpenExternal), qui sait ouvrir correctement un lien dans l’environnement courant.

Erreur n°5 : ne pas gérer les erreurs de fetch et ne pas afficher l’état de chargement.
Dans le bac à sable, les erreurs réseau ne sont pas l’exception mais la norme : le tunnel peut tomber, le domaine peut changer, les politiques peuvent bloquer quelque chose. Si vous faites simplement await fetch(...) puis rendez l’UI en supposant que les données existent, vous obtiendrez une interface bancale qui « fonctionne parfois, parfois non ». Mettez toujours un try/catch, vérifiez res.ok, affichez « Chargement… » et un message d’erreur soigné.

Erreur n°6 : transformer openExternal en redirection cachée.
On voit parfois la tentation d’envoyer l’utilisateur sur un site externe au clic sur n’importe quel bouton, surtout vers le checkout, sans contexte dans le texte. C’est étrange pour l’utilisateur comme pour les personnes qui valident le Store. La bonne pratique — écrire explicitement ce qui va se passer : soit le modèle GPT indique « J’ouvrirai la page de la boutique… », soit le bouton est libellé assez clairement (« Passer au paiement sur le site du magasin »).

Erreur n°7 : oublier que le widget n’est pas l’unique « maître » du dialogue.
Si votre UI tente d’imposer à l’utilisateur un scénario compliqué avec plein de liens et d’appels réseau, en ignorant le chat et les follow‑ups, vous obtiendrez un UX dégradé et une qualité moindre du travail du modèle. Rappelez‑vous l’architecture : GPT décide quand afficher l’App, comment utiliser ses résultats, et le widget ne fait que suggérer et visualiser. La navigation et les appels réseau doivent être conçus pour s’insérer dans le dialogue global, sans « tirer toute la couverture à soi ».

1
Mission
ChatGPT Apps, niveau 3, leçon 4
Bloqué
Boutons CTA pour la navigation externe via openExternal
Boutons CTA pour la navigation externe via openExternal
1
Mission
ChatGPT Apps, niveau 3, leçon 4
Bloqué
Route API publique + fetch absolu depuis un widget
Route API publique + fetch absolu depuis un widget
1
Étude/Quiz
Widget (Apps SDK), niveau 3, leçon 4
Indisponible
Widget (Apps SDK)
Widget (Apps SDK) : état, UI et sandbox
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION