Программирование с ИИ
ENRU
14.3

Кейс — сайт на React к этому API

работает в curl, не работает в браузере

Что это

Третий кейс продолжает второй: API из статьи 14.2 уже работает, теперь к нему нужен сайт. Задача выглядит простой — показать список, — и именно на ней вылезает ошибка, которой нет ни в одном руководстве по React, потому что она не про React.

Шаг 1. Запрос

Лучше
React 19 + TypeScript, сборка Vite.
Один экран: список городов с температурой.
Данные: GET http://localhost:8102/cities,
ответ [{name: string, temp: number | null}].

Нужны все четыре состояния: загрузка, ошибка,
пустой список, данные. Тип ответа опиши явно,
any и as не использовать.
Стили простые, без библиотек компонентов.

Требование про четыре состояния — из статьи 10.5, и оно здесь не для галочки: именно оно спасёт на следующем шаге.

Шаг 2. Что пришло

type City = { name: string; temp: number | null }

export default function App() {
  const [cities, setCities] = useState<City[]>([])
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('http://localhost:8102/cities')
      .then((r) => {
        if (!r.ok) throw new Error(`HTTP ${r.status}`)
        return r.json()
      })
      .then(setCities)
      .catch((e) => setError(String(e)))
      .finally(() => setLoading(false))
  }, [])

  if (loading) return <p className="state">Загружаем…</p>
  if (error) return <p className="state err">Не удалось загрузить: {error}</p>
  if (cities.length === 0) return <p className="state">Городов пока нет</p>

  return (
    <main>
      <h1>Погода</h1>
      <ul>
        {cities.map((c) => (
          <li key={c.name}>
            <span>{c.name}</span>
            <b>{c.temp === null ? 'нет данных' : `${c.temp}°`}</b>
          </li>
        ))}
      </ul>
    </main>
  )
}

Код правильный. Зависимости у эффекта пустые, key на месте, четыре состояния есть, any нет. Собирается:

измерено
✓ built in 1.82s

Шаг 3. Баг, которого нет в коде

Открываем в браузере:

измерено
Не удалось загрузить: TypeError: Failed to fetch

При этом API жив и прекрасно отвечает:

$ curl -s -o /dev/null -w "%{http_code}" localhost:8102/cities
200

Работает в curl, не работает в браузере. Причина — в консоли разработчика (статья 8.4), и только там:

измерено
Access to fetch at 'http://localhost:8102/cities' from origin
'http://localhost:5190' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Это CORS — правило браузера, по которому страница с одного адреса не может читать ответы с другого, если тот явно не разрешил. У curl такого правила нет: оно существует только в браузере и только ради защиты пользователя.

Два вывода, которые стоят этой статьи.

Первый: ошибка на экране не называет причину. Браузер намеренно не отдаёт подробности коду страницы — fetch получает только TypeError: Failed to fetch. Искать надо в консоли, а не в коде React. Запросы вида «почини TypeError: Failed to fetch» бесполезны: в показанном коде ошибки нет.

Второй: чинить надо на сервере. CORS настраивается там, откуда берут данные:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5190"],   # адрес сайта, не "*"
    allow_methods=["GET", "POST", "DELETE"],
    allow_headers=["Content-Type"],
)

После перезапуска API заголовок появляется:

$ curl -D- -H "Origin: http://localhost:5190" localhost:8102/cities
access-control-allow-origin: http://localhost:5190

И страница оживает — ошибок в консоли нет:

Три города пришли из API статьи 14.2. Снимок сделан в браузере после настройки CORS.
Три города пришли из API статьи 14.2. Снимок сделан в браузере после настройки CORS.

Шаг 4. Что проверять глазами

Собранный сайт — не то же самое, что сайт при разработке (глава 12). Минимум проверок перед тем, как считать кейс закрытым:

  • Четыре состояния. Выключить API — должна появиться строка про ошибку, а не пустой экран. Это и была проверка на шаге 3, просто вынужденная.
  • Узкое окно. Сузить до ширины телефона.
  • Собранная версия. vite build и открыть результат, а не только режим разработки.
  • Адрес API не вшит намертво. На сервере он другой; значит, переменная окружения — и помнить, что она попадёт в артефакт открытым текстом (статья 12.1).

Шпаргалка

  • «Работает в curl, не работает в браузере» — почти всегда CORS.

  • Настоящая причина видна только в консоли браузера, а не на экране.

  • TypeError: Failed to fetch — это не ошибка в коде React.

  • CORS чинится на сервере, который отдаёт данные, а не на сайте.

  • allow_origins=["*"] — не решение, а отказ от решения.

  • Четыре состояния экрана окупились в первый же день.

  • Проверять надо собранную версию и узкое окно, а не только режим разработки.