1. Анатомия деплоя: где вообще может ломаться
Полезно сначала увидеть цепочку целиком. Деплой ChatGPT App в вашей архитектуре можно мысленно развернуть в такую линию:
flowchart TD A[Ваш ноутбук
git commit] --> B[Git-репозиторий
GitHub/GitLab] B --> C[Vercel Build
npm run build] C --> D[Vercel Deploy
Preview/Prod] D --> E[HTTP endpoint
/mcp, /api/...] E --> F[ChatGPT / Dev Mode
tool calls, widgets]
Ошибка может появиться на любом из этих шагов, но симптомы в ChatGPT выглядят примерно одинаково: "Error talking to app", "Network error" или просто молчание. Ваша задача — не стрелять наугад, а сначала понять: это упало на этапе сборки, во время выполнения, или ChatGPT смотрит не туда.
Удобно разделять проблемы на три крупные категории:
- Build‑ошибки: Vercel вообще не смог собрать проект. Продакшен не обновился — это «хорошо», но вы видите красный билд.
- Runtime‑ошибки: билд прошёл, но при запросах приходят 500/502, таймауты или странное поведение.
- Config drift (дрейф конфигурации): локально всё ок, на Vercel всё ок по логам, но ChatGPT ходит на старый URL, работает со старым манифестом или с пустыми env‑переменными.
Мы будем идти по этим трём слоям и параллельно развивать общую стратегию отладки.
2. Build‑ошибки: когда проект не собирается
Это первый тип проблем из введения — build‑ошибки: проект вообще не собирается, потому что Vercel не может успешно собрать ваш Next.js‑проект.
Node и Next.js: другая среда, другие требования
На локалке вы можете (к сожалению) жить с устаревшим Node, а Vercel попытается собрать ваш Next.js 16 проект с поддерживаемой версией Node (минимум 18.18.0). Если в package.json явно указана несовместимая версия, сборка может отвалиться в проде, хотя dev‑сервер у вас крутился.
Простой способ защищаться — в package.json явно указывать "engines":
{
"engines": {
"node": ">=18.18.0"
}
}
Тогда и локально, и в CI/на Vercel вы заранее увидите, что Node слишком старый.
«У меня работает!» и забытые зависимости
Классика: вы установили библиотеку через npm install some-lib, но не закоммитили обновлённый package-lock.json или вообще у вас часть зависимостей установлена глобально. На Vercel приложение собирается «с нуля», оно честно выполняет npm install на основе манифеста, а вашей любимой some-lib там нет — получаем build error.
Здесь помогает жёсткая дисциплина:
- любые новые зависимости добавляются и сразу коммитятся;
- перед пушем в main/production вы запускаете npm run build локально. Если локальный build падает, на Vercel будет только хуже.
Case‑sensitive файловая система
Локально многие сидят на macOS или Windows, где файловая система по умолчанию не различает регистр в имени файла. На Vercel сборка идёт в Linux‑среде, там Widget.tsx и widget.tsx — разные файлы.
Типичный баг:
// Импорт в коде
import { AppWidget } from "@/components/Widget";
// А в репозитории лежит файл components/widget.tsx
На вашем компьютере всё работает, на Vercel — ошибка модуля «Cannot find module '@/components/Widget'». Лечится наведением порядка в именах и внимательным отношением к регистру.
Env‑переменные на этапе сборки
Ещё один источник сюрпризов — использование process.env.* в коде, который выполняется на этапе сборки (например, в next.config.mjs или в модулях, импортируемых при build). Если вы локально подцепили .env.local, а на Vercel забыли завести эти переменные для build‑окружения, сборка либо упадёт, либо — что неприятнее — пройдёт с undefined и «запечёт» невалидные значения в бандл.
Для ChatGPT App это особенно критично, если вы, например, формируете baseURL для MCP‑эндпоинта или URL внешних API прямо на этапе сборки.
Хорошая практика — явно валидировать критичные env‑переменные ещё до старта приложения (о чём мы поговорим в отдельном разделе), чтобы build падал громко и предсказуемо.
3. Runtime‑ошибки: когда всё собралось, но не работает
Теперь переходим ко второму слою из введения — runtime‑ошибкам: билд прошёл, но при выполнении всё ломается.
Сборка прошла, Vercel радостно показал зелёный деплой, вы переключили ChatGPT App на прод‑URL — и получили в чате "Error talking to app". Значит, проблемы перешли на уровень выполнения.
Нулевые или пустые env‑переменные
Чаще всего прод‑инцидент в мире ChatGPT App начинается со слова undefined. На локалке у вас аккуратный .env.local с OPENAI_API_KEY, MCP_BASE_URL и прочим, а на Vercel вы забыли завести эти переменные или перепутали названия.
Например, вы читаете:
const apiKey = process.env.OPENAI_API_KEY;
а на Vercel завели OPENAI_APIKEY или OPENAI_API_KEY_PROD. В результате при первом же вызове MCP‑инструмента ваш route‑handler падает с ошибкой аутентификации.
Гораздо приятнее, когда приложение падает сразу и понятно. Хороший паттерн — отдельный модуль в вашем Next.js‑проекте, который валидирует env‑переменные при импорте:
// app/lib/env.ts
const required = ["OPENAI_API_KEY", "MCP_BASE_URL"] as const;
type RequiredKey = (typeof required)[number];
function getEnv(key: RequiredKey): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required env var: ${key}`);
}
return value;
}
export const env = {
OPENAI_API_KEY: getEnv("OPENAI_API_KEY"),
MCP_BASE_URL: getEnv("MCP_BASE_URL"),
};
Теперь, если вы забыли настроить переменные на Vercel, Next.js упадёт при первом же импорте env, а в логах будет человеческое сообщение "Missing required env var: ...".
Важно помнить, что на Vercel изменения env‑переменных не подхватываются автоматически. После правки значений нужно сделать новый деплой (redeploy), иначе рантайм продолжит жить со старыми значениями.
Ошибки в route‑handler’ах и MCP‑эндпоинте
В официальном шаблоне ChatGPT App MCP‑сервер обычно реализован как app/mcp/route.ts. Внутри у вас код, который парсит JSON‑RPC запрос, роутит его к инструменту и возвращает ответ. Если где‑то в цепочке происходит throw без обработки — пользователю в ChatGPT прилетит 500.
Стоит всегда оборачивать верхний уровень MCP‑хендлера в try/catch, логировать ошибку и возвращать структурированный ответ:
// app/mcp/route.ts
import { NextRequest, NextResponse } from "next/server";
export const dynamic = "force-dynamic";
export const maxDuration = 30; // секунд
export async function POST(req: NextRequest) {
try {
const body = await req.json();
// здесь обработка MCP-запроса
const result = await handleMcpRequest(body);
return NextResponse.json(result);
} catch (error) {
console.error("MCP route error", error);
return NextResponse.json(
{ error: "Internal MCP error" },
{ status: 500 }
);
}
}
Пара моментов:
- dynamic = "force-dynamic" помогает избежать неожиданной статической генерации и кэширования для MCP‑маршрутов в Next.js 16.
- maxDuration = 30 явно говорит Vercel, что route‑handler может работать до 30 секунд, что важно при долгих LLM‑запросах.
Таймауты и «Network error» в ChatGPT
Vercel ограничивает время выполнения serverless‑функций: на бесплатных тарифах это обычно около 10 секунд, на платных может быть больше (до нескольких минут). Если ваш MCP‑инструмент делает длинный запрос к базе или внешнему API, он может не успеть ответить вовремя, и ChatGPT получит "Network error" или оборванный стрим.
Если вы используете стриминг (SSE) для частичных результатов, особенно важно отправить первые байты ответа до истечения таймаута. Тогда сама передача может идти дольше, но платформа не посчитает функцию «зависшей».
Мини‑приём: измеряйте время вызова инструментов и логируйте его вместе с именем инструмента. Тогда в логах будет видно, что, например, search_flights стабильно занимает 12 секунд и чуть‑чуть не помещается в ограничение.
export async function safeToolCall<TInput, TOutput>(
name: string,
handler: (input: TInput) => Promise<TOutput>,
input: TInput
): Promise<TOutput> {
const started = Date.now();
try {
const result = await handler(input);
console.log("[tool] ok", name, { ms: Date.now() - started });
return result;
} catch (error) {
console.error("[tool] fail", name, {
ms: Date.now() - started,
error,
});
throw error;
}
}
Потом вместо handler(args) вы вызываете safeToolCall("search_flights", handler, args).
Сеть и внешние сервисы
Иногда всё дело в банальном https:// вместо http:// или устаревшем baseURL. Особенно если вы сначала тестировали на локальной машине с одним URL, а на проде у вас уже другой домен или другой порт.
Полезно вынести базовые URL в конфигурацию (зависящую от окружения) и не шить их напрямую в код инструмента. Тогда при смене окружения вы меняете один env, а не вспоминаете, в каких пяти местах в коде у вас был http://localhost:3001.
4. Конфигурация и дрейф окружений
И, наконец, третий тип из нашей схемы — конфигурационный дрейф между окружениями.
Даже если build прошёл и runtime по логам здоров, ChatGPT может вести себя «как будто работает не та версия приложения». Это как раз тот случай, когда проблема не столько в коде, сколько в конфигурации и согласованности окружений.
Dev Mode против продакшена
В Dev Mode ChatGPT смотрит на Connector URL, который вы указали вручную: обычно это туннельный URL (https://myapp-dev.ngrok-free.app/mcp или что‑то подобное) или staging‑URL на Vercel. В продакшене (через Store или настройки организации) App должен указывать на стабильный прод‑эндпоинт, например https://myapp.vercel.app/mcp.
Ошибка, которую делает почти каждый: вы задеплоили на Vercel, но в настройках ChatGPT App всё ещё указан старый туннельный URL. Локальный сервер выключен, туннель давно умер, а ChatGPT честно стучится туда и получает 502. В интерфейсе это выглядит как "Error talking to app", и студент начинает чинить MCP‑код, который вообще не исполняется.
Лечится дисциплиной: после любого изменения окружения (туннель → staging, staging → prod) вы проверяете, какой именно URL прописан в Dev Mode и в прод‑конфигурации App.
Старый манифест и кэш ChatGPT
ChatGPT будет кэшировать информацию о вашем App: список инструментов, их описания, метаданные. Поэтому ситуация «я поменял схему инструмента, а модель всё ещё считает, что аргумент называется по‑старому» — реальность.
При серьёзных изменениях инструментов полезно:
- убедиться, что вы действительно задеплоили новую версию (посмотреть commit hash в логах, вывести его в startup‑лог);
- пересоздать или пере‑подключить App в Dev Mode, чтобы заставить платформу перечитать манифест;
- на время отладки работать через MCP Inspector, где вы точно видите актуальный список инструментов и схем.
Env‑конфиг: dev/staging/prod
Мы уже говорили о том, как env‑переменные могут завалить build и runtime. Здесь — взгляд сверху на dev/staging/prod и согласованность значений между ними.
Частая боль: .env.local у вас идеальный, а в Vercel‑окружениях — зоопарк. В итоге:
- локально у вас один API‑ключ и один URL внешнего сервиса;
- на staging — вообще другие значения;
- на prod — половина переменных не заданы.
Очень помогает простой текстовый файл docs/env.md в репозитории, где вы перечисляете: какие переменные нужны, в каких окружениях они обязательны, какие значения примерные. Это может показаться бюрократией, но в момент инцидента такой список экономит часы.
5. Как выглядят ошибки на стороне ChatGPT
Теперь давайте посмотрим на ситуацию глазами пользователя ChatGPT. Он видит только интерфейс и не знает ничего о Vercel, Node и MCP. А вы, к сожалению, пока тоже не знаете, что именно там сломалось.
Типичные симптомы в ChatGPT:
- сообщение "Error talking to [App Name]" сразу после попытки использования;
- бесконечный спиннер без видимой ошибки;
- красный текст "I encountered an error while running the tool";
- виджет не появляется или появляется пустой.
Каждый из этих симптомов обычно соответствует определённому уровню поломки:
- если App вообще недоступен (не тот URL, туннель упал, SSL‑ошибка), ChatGPT не может достучаться до вашего MCP‑эндпоинта — смотрите доступность домена в браузере и логи Vercel с кодами 4xx/5xx;
- если MCP отвечает валидным JSON‑RPC с полем error, ChatGPT честно пишет, что инструмент вернул ошибку — это уже вопрос к бизнес‑логике или валидации аргументов;
- если MCP отвечает успешно, но в ответе ломаный HTML виджета или JS‑ошибка, то в консоли виджета (DevTools → iframe виджета) будет видно, что именно упало.
Поэтому хорошая привычка: как только увидели странное поведение в чате, сразу берёте timestamp (вплоть до минуты) и идёте в логи Vercel искать запросы в это время.
6. Стратегия отладки: как не паниковать, а действовать
Сейчас соберём из всего сказанного небольшой «playbook» — сценарий действий, когда что‑то пошло не так. Цель — заменить бег по кругу на спокойный алгоритм.
Шаг 1: определяем тип проблемы
Если build на Vercel красный — радуемся: ошибка поймана до продакшена. Открываем build‑логи, смотрим первую реальную ошибку (а не 200 строк ворнингов) и воспроизводим локально командой npm run build.
Если build зелёный, а ChatGPT жалуется — это рантайм или конфиг. Проверяем:
- доступен ли прод‑URL вашего App из браузера (https://myapp.vercel.app/mcp отдаёт ли хоть что‑то);
- возвращает ли MCP‑эндпоинт 200/500 или вообще не резолвится;
- совпадает ли URL в настройках App с тем, что вы только что проверили.
Шаг 2: читаем логи, а не мысли
Следующая остановка — логи Vercel: серверные логи для нужного деплоя и нужного окружения (Preview/Production).
Ищем:
- ошибки Error: Missing required env var ... — значит, проблема в конфигурации;
- stack trace из MCP‑хендлера — значит, падает бизнес‑логика или парсинг входных данных;
- сообщения о таймаутах или превышении длительности функции.
Параллельно не забываем про MCP Inspector. Если подключиться к тому же MCP‑эндпоинту через инспектор и вручную вызвать инструменты, быстро станет понятно, проблема в самом MCP или в связке ChatGPT ↔ MCP.
Шаг 3: быстрый rollback или hotfix?
Если вы видите, что прод‑деплой явно сломан (например, MCP route постоянно кидает одну и ту же ошибку на каждый запрос), а предыдущий деплой был здоров, правильным решением будет откатиться. Vercel позволяет быстро переключиться на предыдущий успешный деплой без пересборки — это по сути смена активной версии.
Это лучше, чем пытаться чинить продакшен «на лету», особенно если вы не до конца понимаете причину инцидента.
Когда ситуация стабилизирована, вы спокойно разбираете причину, пишете тесты, фиксируете в коде и уже потом заливаете следующую версию.
Шаг 4: закрепляем знания документацией
Любой серьёзный инцидент — повод обновить внутренний README:
- добавить в список обязательную env‑переменную, без которой всё падает;
- зафиксировать, какой именно кейс привёл к ошибке (например, «импорт с неправильным регистром имён файлов»);
- описать краткий алгоритм действий, который помог быстро всё починить.
Это кажется скучным, но через пару месяцев вы сами скажете себе спасибо.
7. Небольшие практические приёмы в коде
Теперь возьмём несколько шагов из нашего playbook’а и закрепим их небольшими кодовыми приёмами в нашем учебном приложении (ChatGPT App).
Единый модуль конфигурации
Мы уже писали простой валидатор env‑переменных. Его можно дополнить, чтобы различать окружения:
// app/lib/config.ts
type NodeEnv = "development" | "test" | "production";
const nodeEnv = (process.env.NODE_ENV || "development") as NodeEnv;
const requiredBase = ["OPENAI_API_KEY"] as const;
const requiredProd = ["MCP_BASE_URL"] as const;
function ensure(keys: readonly string[]) {
for (const key of keys) {
if (!process.env[key]) {
throw new Error(`Missing env var ${key} for NODE_ENV=${nodeEnv}`);
}
}
}
ensure(requiredBase);
if (nodeEnv === "production") {
ensure(requiredProd);
}
export const config = {
nodeEnv,
openaiApiKey: process.env.OPENAI_API_KEY!,
mcpBaseUrl: process.env.MCP_BASE_URL ?? "http://localhost:3000/mcp",
};
Такой модуль сразу подсветит вам, если прод запущен без нужной переменной.
Логирование входящих MCP‑запросов
Простая, но очень полезная обвязка для MCP‑хендлера:
// app/lib/mcp-logger.ts
export function logMcpRequest(body: unknown) {
console.log("[mcp] request", {
time: new Date().toISOString(),
// не логируем чувствительные данные
keys: typeof body === "object" && body !== null
? Object.keys(body as Record<string, unknown>)
: typeof body,
});
}
И используем в app/mcp/route.ts:
import { logMcpRequest } from "@/app/lib/mcp-logger";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
logMcpRequest(body);
const result = await handleMcpRequest(body);
return NextResponse.json(result);
} catch (error) {
console.error("MCP route error", error);
return NextResponse.json({ error: "Internal error" }, { status: 500 });
}
}
В логах вы будете видеть, что вообще приходит от ChatGPT: хотя бы по ключам ("jsonrpc", "method", "params"), и легче поймёте, какой именно вызов падает.
Простая проверка доступности MCP‑эндпоинта
Иногда полезно иметь небольшой route‑handler вроде «healthcheck» для MCP‑сервера, который ChatGPT напрямую не зовёт, но вы можете быстро открыть его в браузере и понять, жив ли сервер и видит ли он свои env‑переменные:
// app/api/health/route.ts
import { NextResponse } from "next/server";
import { config } from "@/app/lib/config";
export async function GET() {
return NextResponse.json({
status: "ok",
env: config.nodeEnv,
hasOpenAiKey: !!config.openaiApiKey,
});
}
Если https://myapp.vercel.app/api/health отвечает status: "ok", значит хотя бы базовый пайплайн до вашего Node‑кода жив.
8. Типичные ошибки при деплое и отладке
Ошибка №1: Деплой без локального npm run build.
Когда разработчик никогда не запускает build локально, он узнаёт о несовместимой версии Node, проблеме с путями или TS‑ошибке только на Vercel. Это удлиняет цикл «сломал → починил», потому что каждый эксперимент — это новый деплой. Привычка прогонять npm run build перед пушем в main экономит много времени (см. также раздел 2 и шаг 6.1 про локальный npm run build).
Ошибка №2: Секреты остались только в .env.local.
Проект прекрасно работает на машине автора, но в проде падает из‑за process.env.OPENAI_API_KEY === undefined. Причина банальна: env‑переменные забыли добавить в настройках Vercel (и иногда даже назвали по‑другому). Особенно часто забывают про разделение Development/Preview/Production и удивляются, что staging и prod ведут себя по‑разному (подробнее — разделы 3.1, 4.3 и 7.1).
Ошибка №3: Использование NEXT_PUBLIC_* для секретов.
В Next.js все переменные с префиксом NEXT_PUBLIC_ попадают в браузерный бандл. Если вы по невнимательности назвали API‑ключ NEXT_PUBLIC_OPENAI_API_KEY, он уедет к пользователю в браузер и может быть вытащен из девтулов. Так делать нельзя. Публичными должны быть только безопасные значения (например, идентификаторы фич‑флагов, но не токены).
Ошибка №4: Игнорирование логов Vercel и попытка «починить через ChatGPT».
Иногда разработчик видит в чате "Error talking to app" и часами меняет промпты, описания инструментов, что‑то правит в Dev Mode, но ни разу не заглядывает в serverless‑логи. А там стоит вполне понятная ошибка: "Missing env var", "Cannot find module" или stack trace конкретного инструмента. Хороший инженер сначала смотрит логи, а потом уже спорит с моделью.
Ошибка №5: Путаница Dev Mode и production‑App.
После первого успешного деплоя на Vercel легко забыть, что Dev Mode по‑прежнему может смотреть на старый туннель или на preview‑URL. В результате вы уверены, что тестируете продакшен‑версию, а на самом деле общаетесь с локальной веткой, которую давно пора удалить. Или наоборот: думаете, что тестируете черновые правки, а ChatGPT стучится на боевой endpoint. Нужно регулярно проверять, какой URL указан в настройках App и Dev Mode (см. также раздел 4.1 про Dev Mode и продакшен).
Ошибка №6: Ожидание, что изменение env‑переменной на Vercel сработает «на лету».
Некоторые студенты меняют значения переменных в панели Vercel и тут же бегут в ChatGPT проверять результат. Но рантайм всё ещё использует старые значения, потому что не было redeploy. Любое изменение env‑переменных требует нового деплоя, иначе функция не увидит обновление (подробнее — раздел 3.1).
Ошибка №7: Отсутствие простой стратегии rollback.
В момент инцидента соблазн велик «быстро запушить фикс» прямо в main. Но это добавляет ещё один потенциально сломанный деплой, а пользователи всё это время страдают. Куда спокойнее иметь привычку: при серьёзной ошибке сразу откатываться на предыдущий успешный деплой, фиксить проблему в отдельной ветке и только потом выпускать новую версию. Vercel даёт для этого удобный интерфейс, грех не пользоваться.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ