1. Duas saídas: navegação e dados
Se um desenvolvedor Next.js comum ouve “precisa chamar o servidor”, a mão automaticamente vai para o fetch ou para o cliente HTTP favorito. No mundo dos ChatGPT Apps, essa reação reflexa leva à dor.
Na parte do curso dedicada à segurança de widgets em ChatGPT Apps, propomos desde o início quebrar esse velho reflexo. O widget não vive na internet aberta: ele fica em isolamento rígido, e o acesso de rede é filtrado e limitado pelas políticas do host.
O widget tem apenas três janelas básicas para fora:
- Navegação: levar o usuário para algum lugar no mundo externo. Para isso existe o openExternal.
- Troca de dados: receber/enviar JSON, conversar com o backend. Isso é feito via fetch, mas podem existir fortes limitações.
- MCP tool call: chamada de ferramentas (MCP / backend), que não têm quaisquer limitações.
Nesta aula, focamos no primeiro e mais seguro caminho (navegação) e apresentamos com cuidado o fetch controlado. Nos próximos módulos, veremos MCP e ferramentas como o principal meio de diálogo sério com o servidor.
2. openExternal: o “teleporte” seguro do usuário
Por que não é possível simplesmente usar window.open
Em um aplicativo web comum, você faria algo assim:
window.open("https://example.com", "_blank");
Na sandbox do ChatGPT, isso ou não vai funcionar, ou vai funcionar de maneira muito estranha. O widget é um iframe isolado com sandbox rígido, que não tem os mesmos direitos que a aba do navegador.
Além disso, o host do ChatGPT quer controlar para onde e quando você leva o usuário, para:
- evitar rastreamento oculto;
- mostrar ao usuário uma UI de confirmação clara (especialmente nos clientes mobile/desktop);
- garantir comportamento consistente de links em diferentes ambientes (web, desktop, aplicativo móvel).
Por isso, foi criada a API especial openExternal, acessível via window.openai ou pelo hook React mais conveniente useOpenExternal.
Como é o useOpenExternal
Nos exemplos oficiais do Apps SDK, o hook useOpenExternal é implementado mais ou menos assim:
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;
}
A ideia principal é simples. Primeiro, tentamos usar o mecanismo nativo do ChatGPT (window.openai.openExternal). Se o widget, de repente, for renderizado fora do ChatGPT (por exemplo, você o abriu no navegador durante o desenvolvimento), caímos com elegância no window.open.
No seu app, esse hook já está no template (se você pegou o repositório padrão da OpenAI) e é assim que deve ser usado — e não mexendo diretamente em window.openai.
Exemplo: botão “Ver na loja” no GiftGenius
Vamos imaginar que no toolOutput do nosso GiftGenius chegam recomendações com o campo productUrl. Vamos adicionar a cada card um botão que abre o produto no seu 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>Ainda não há recomendações…</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)}>
Abrir
</button>
</div>
))}
</div>
);
}
Do ponto de vista do usuário: ele clica no botão, o ChatGPT pode mostrar uma janela do sistema “Abrir site externo?”, e então abrirá sua página em uma nova aba ou no navegador padrão. Você não envia segredos, tokens etc.; apenas leva a pessoa “do chat para o site”.
3. window.fetch na sandbox: não é o fetch ao qual você está acostumado
O que o desenvolvedor front-end normalmente espera
Normalmente, a lógica é: “Se isso é o navegador, então posso chamar qualquer URL com CORS configurado. Na pior das hipóteses, recebo um erro, mas dá para tentar.”
No ecossistema de ChatGPT Apps, isso é um equívoco perigoso. A sandbox ao redor do widget não é apenas “frescura”, mas um requisito de segurança fundamental: para que o widget não possa rastrear o usuário, chamar domínios arbitrários, escanear a rede local e, no geral, se comportar como um mini‑navegador dentro do navegador.
Nesse mesmo relatório, enfatiza-se que o widget no Apps SDK tem acesso de rede arbitrário inexistente ou fortemente limitado — e isso não é bug, mas uma decisão arquitetural consciente.
Como isso se parece na prática
No ambiente típico do ChatGPT:
- fetch pode estar disponível, mas apenas para uma lista limitada de domínios (geralmente o seu domínio, onde roda o App, e, possivelmente, alguns APIs explicitamente permitidos);
- as requisições podem passar por um proxy especial do host, que filtra cabeçalhos e URLs;
- alguns métodos (PUT, DELETE) ou cabeçalhos não padronizados podem ser bloqueados por políticas de segurança.
Mesmo assim, você ainda tem um caminho conveniente: se o seu widget e seu backend vivem no mesmo domínio (como no template em Next.js, onde o servidor MCP e a UI são servidos pela mesma aplicação), as requisições internas fetch("/api/...") normalmente serão permitidas.
O principal é não contar que o widget poderá chamar qualquer API na internet. Todo o diálogo “pesado” com serviços externos (Stripe, Notion, CRM etc.) deve acontecer no lado do MCP/backend, para o qual o ChatGPT faz chamadas como um recurso confiável.
Insight
No widget do ChatGPT, é preciso esquecer de caminhos relativos e usar URLs absolutas. O motivo é simples: seu HTML não roda no mesmo domínio que o backend. O ChatGPT lê seu HTML, hospeda em seu próprio domínio e renderiza dentro de um iframe isolado. Qualquer "/api/..." ou "/static/logo.png" de repente passa a ser resolvido relativamente ao domínio do ChatGPT, e não ao do seu app — e tudo quebra.
<base> quase não ajuda aqui. Foi constatado experimentalmente que, se o widget não tiver widgetCSP definido, você pode declarar <base href="https://my-app.dev/">: os recursos serão buscados do seu domínio, mas os scripts, pelas regras da sandbox, ainda não vão funcionar. Mas isso funciona apenas no Dev Mode.
Assim que você define um openai/widgetCSP normal (e em produção você terá de defini-lo para o review), a plataforma ignora o <base>, e o jogo acaba: recursos e scripts carregam apenas de domínios permitidos no CSP, e já por links absolutos.
Recomendação: no widget do ChatGPT, tudo que sai para fora — fetch, imagens, CSS, suas páginas para openExternal — deve sempre ser construído como URL completo a partir do domínio base da aplicação, que você controla via config/ENV, e não por caminhos relativos e <base>.
4. Arquitetura: UI fina, backend robusto
Das limitações do fetch e da sandbox em geral, decorre um princípio arquitetural mais amplo, importante para todo o curso. Já repetimos esse mantra algumas vezes, mas agora é hora de fixar: o widget é uma camada de UI fina. Ele renderiza o que já foi preparado pelo backend (via MCP/tools), mostra reações às ações do usuário e, no máximo, faz algumas poucas requisições públicas.
Tudo que envolve autenticação, acesso a dados pessoais, segredos e lógica de negócio não trivial deve viver no lado do servidor. A documentação de segurança do curso enfatiza: o front-end (o widget React) é um “espaço público”, uma zona de confiança zero; segredos não devem viver ali.
Toda a minha pesquisa sobre o tema formula o objetivo de forma dura: “cravar o último prego no caixão da ideia de ‘cliente pesado’” para ChatGPT Apps. O widget é apenas a cabeça; o corpo e o cérebro estão no MCP/backend.
Portanto:
- openExternal — para levar o usuário ao seu site “normal”, onde você pode rodar o SPA de sempre, área logada etc.;
- callTool (próximo módulo) — o principal meio de passar para o modelo uma tarefa que seu backend executará;
- fetch a partir do widget — um herói raro para requisições auxiliares, seguras e, de preferência, públicas ao seu próprio app.
5. Prática: openExternal no nosso GiftGenius
Vamos integrar o openExternal um pouco mais cuidadosamente no nosso App de estudo e, ao mesmo tempo, pensar no UX.
Regra de UX rápida
Se você leva o usuário para fora, é útil:
- deixar claro para onde exatamente ele irá;
- evitar “saltos” inesperados sem explicação no texto (ou o GPT informa “Vou abrir o site da loja…”, ou você rotula o botão).
Exemplo de rótulo do botão:
<button onClick={() => openExternal(gift.url)}>
Abrir no site da loja
</button>
O usuário entende que será levado do chat para o mundo real, com carrinho e pagamento.
Uma pequena refatoração do componente de lista
Anteriormente já fizemos um GiftListWidget simples. Suponha que nas aulas anteriores você já implementou um widget que mostra uma lista de presentes com base no toolOutput. Agora vamos fazer uma versão um pouco mais caprichada: adicionar o tipo Gift com o campo url e o botão 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>Ainda não encontrei nada. Tente alterar a consulta.</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)}>
Ver
</button>
</div>
))}
</div>
);
}
Continuamos sem mexer diretamente em window.openai, usando o hook conveniente — ele já faz o fallback para window.open nos casos em que o ambiente do ChatGPT não está presente. A estrutura de Gift aqui é apenas um exemplo — no seu App você vai ajustá-la ao seu backend.
6. Prática: fetch cuidadoso para o nosso backend
Agora vamos lidar com o fetch. Lembro mais uma vez: operações complexas ou sensíveis são melhores via ferramentas/MCP. Mas às vezes você quer, a partir do widget, buscar algo leve e público no seu próprio servidor, por exemplo, uma lista de categorias populares de presentes.
Uma rota de API pública simples no Next.js
Adicione ao nosso projeto Next.js este handler:
// app/api/public/popular-tags/route.ts
import { NextResponse } from "next/server";
const tags = ["Para crianças", "Para viajantes", "Para jogadores"];
export async function GET() {
return NextResponse.json({ tags });
}
Essa rota não sabe nada sobre o usuário, não exige tokens, não chama serviços externos — ela apenas retorna um array estático. Esse tipo de código pode ir para produção e para a sandbox quase sem risco.
Chamando essa rota a partir do widget via fetch
Agora, no componente do widget, vamos adicionar o carregamento desses tags. Considerando as restrições da sandbox, é mais conveniente fazer a requisição para uma URL absoluta: o mesmo domínio onde roda seu App — aquele que você expõe via túnel e registra no Dev Mode do ChatGPT (configuramos isso no módulo sobre Dev Mode e túnel).
Importante: o domínio do seu widget será algo como https://genius.web-sandbox.oaiusercontent.com, portanto não use caminhos relativos para carregar dados, apenas absolutos. Exemplo:
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("Não foi possível carregar as categorias populares");
}
}
loadTags();
return () => {
cancelled = true;
};
}, []);
if (error) return <p>{error}</p>;
if (!tags) return <p>Carregando categorias populares…</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>
);
}
Importa que:
- lidamos com erros com cuidado e mostramos ao usuário uma mensagem compreensível;
- não presumimos que o fetch “vai funcionar com certeza” — as políticas da sandbox podem bloquear o acesso a qualquer momento, se você mudar o domínio ou começar a fazer requisições estranhas;
- não passamos tokens/segredos; se precisar de autenticação — isso já é tarefa do MCP e dos módulos sobre Auth.
7. openExternal vs fetch vs ferramentas (callTool): quem faz o quê
Para não se confundir, é útil manter na cabeça essa “matriz de responsabilidades”:
| Cenário | O que usar | Por quê |
|---|---|---|
| Abrir landing/produto/painel | openExternal | Transição explícita do usuário, controlada pelo host |
| Obter dados públicos do App | fetch("my.com/api/...") | JSON leve, mesmo domínio, sem segredos |
| Buscar dados do usuário, banco de dados | callTool/MCP | Requer autorização, lógica, backend seguro |
| Acessar APIs externas (Stripe…) | MCP/servidor | Front não vê segredos, políticas atendidas |
Neste módulo, é importante aprender a escolher conscientemente a ferramenta. É preciso sair do pensamento “o widget é front-end, então dá para fazer tudo via fetch” e ir para a arquitetura “o widget é uma camada de UI controlada sobre o backend LLM+MCP”.
Insight
A interação com o servidor no ChatGPT App faz sentido ser dividida em dois níveis:
- ChatGPT ↔ servidor MCP: o modelo chama ferramentas MCP. Cada tool-call é a execução ou troca de um cenário de negócio (seleção de presentes, criação de pedido, cálculo de custo etc.). Aqui vive a lógica “pesada”, o trabalho com dados, APIs externas e a autorização.
- Widget ↔ servidor: o widget faz requisições fetch() leves ao seu backend e/ou aciona as mesmas ferramentas MCP via callTool() já dentro do cenário ativo. São passos locais: carregar dados auxiliares, atualizar uma parte da UI, esclarecer o estado.
Ou seja, MCP-tool = iniciar/gerenciar o processo de negócio, enquanto fetch()/callTool() a partir do widget são operações pequenas dentro do cenário já escolhido, sem pretensão de alterar a “história” geral do diálogo.
8. Um pequeno exercício prático
Para fixar o tema na prática, você pode implementar uma pequena feature no GiftGenius.
Cenário sugerido:
- Na lista de presentes, adicione um botão “Ir para o checkout”, que, via openExternal, abre a página de finalização de compra no seu site de desenvolvimento.
- Acima da lista de presentes, renderize o PopularTags do exemplo acima, para mostrar as categorias populares. Em caso de erro de carregamento, faça um texto de fallback e não quebre todo o widget.
- Preste atenção ao UX: no texto da resposta do GPT ou na UI do widget, explique ao usuário que “ao clicar no botão, abrirei a página da loja em uma nova aba”.
Essa feature, em miniatura, mostra ambos os canais:
- openExternal para navegação explícita;
- fetch para um pequeno API público, que vive ao lado do seu App.
9. Erros comuns ao trabalhar com window.fetch e openExternal
Erro nº 1: tentar usar o widget como um cliente SPA completo para todas as suas APIs.
Hábitos antigos puxam forte para “vamos só chamar nosso REST/GraphQL direto do React”. No mundo de ChatGPT Apps, isso leva a um choque com a sandbox: parte das requisições simplesmente não vai passar, parte será bloqueada por políticas e a segurança do projeto ficará em xeque. Lógica complexa e acesso a dados do usuário devem ir via MCP/ferramentas, e não diretamente do widget.
Erro nº 2: guardar segredos e tokens no código do widget.
Às vezes dá vontade de “prototipar rápido” e colocar no front-end uma chave de API de algum serviço (“estou só testando”). Isso é uma má ideia até para um SPA comum; para ChatGPT Apps — é terminantemente proibido. O widget é um ambiente público; segredos devem viver na configuração do servidor ou em sistemas de gestão de segredos (Vercel env, KMS etc.).
Erro nº 3: presumir que fetch para qualquer domínio “simplesmente funciona”.
Mesmo que no Dev Mode alguma requisição passe (por exemplo, porque o túnel foi exposto de forma diferente), em produção ela quase certamente vai quebrar: o ChatGPT restringe as saídas de rede e domínios externos arbitrários não estão acessíveis ao widget. Parta da premissa de que o widget pode acessar com segurança apenas seu próprio domínio e uma lista branca muito pequena de recursos explicitamente permitidos.
Erro nº 4: usar window.open em vez de openExternal.
Tecnicamente, às vezes window.open pode funcionar, especialmente no preview no navegador, e cria-se a ilusão de que “está tudo certo”. Mas no ambiente real do ChatGPT, especialmente nos clientes nativos, o comportamento será imprevisível. O usuário pode nem ver a navegação ou receber um erro estranho. O caminho correto é usar openExternal (via hook useOpenExternal), que sabe abrir o link corretamente no ambiente atual.
Erro nº 5: não tratar erros do fetch e não mostrar o estado de carregamento.
Na sandbox, erros de rede não são exceção, mas regra: o túnel pode cair, o domínio pode mudar, as políticas podem bloquear algo. Se você simplesmente faz await fetch(...) e depois renderiza a UI assumindo que os dados existem, você terá uma interface meio quebrada, que “às vezes funciona, às vezes não”. Sempre use try/catch, verifique res.ok, mostre “Carregando…” e uma mensagem de erro cuidadosa.
Erro nº 6: transformar openExternal em redirecionamento oculto.
Às vezes surge a vontade de, ao clicar em qualquer botão, já levar o usuário para um site externo, especialmente para o checkout, sem qualquer contexto no texto. Isso soa estranho tanto para o usuário quanto para os revisores da Store. Boa prática é escrever de forma explícita o que vai acontecer: ou o modelo GPT informa “Vou abrir a página da loja…”, ou o próprio botão tem um rótulo suficientemente claro (“Ir para o pagamento no site da loja”).
Erro nº 7: esquecer que o widget não é o único “dono” do diálogo.
Se sua UI tenta impor ao usuário um cenário complexo com um monte de links e requisições de rede, ignorando o próprio chat e os follow-ups, o resultado é um UX pior e menor qualidade do trabalho do modelo. Lembre a arquitetura: o GPT decide quando mostrar o App, como usar seus resultados, e o widget apenas auxilia e visualiza. Navegação e chamadas de rede devem ser projetadas para se encaixar no diálogo geral, e não para puxar toda a conversa para si.
GO TO FULL VERSION