Кэширование ответов публичного API

CloudflareBeginner
Практиковаться сейчас

Введение

Публичный каталог поддержки получает повторяющиеся запросы для одного и того же языка и категории. Повторное использование ответов уменьшает лишнюю работу, но кэш не должен смешивать данные разных клиентов или превращать ошибку в кэшированный публичный контент. Вы увидите генерацию ответов без кэширования, добавите явную политику кэширования, проверите истечение срока действия и адресную инвалидацию, а затем развернёте приложение и проверите границы его работы.

Эта независимая лабораторная работа начинается в /home/labex/project/public-cache. В среде установлены Node.js 22.22.0, локальный Wrangler 4.131.1, Miniflare 4.20260730.0 для изолированной проверки и фикстура с синтетическими ответами. Используйте собственную учебную учётную запись и ранее изученные процедуры авторизации, развёртывания и работы с секретами. Предыдущая виртуальная машина, ресурс, купленный домен, база данных или платное расширение не требуются. Запросы учитываются в рамках обычного использования учётной записи.

Оставьте один терминал открытым. Все данные каталога и учётные данные являются синтетическими. Содержимое Cache API локально для конкретного места обслуживания; глобальная сеть не означает глобально реплицированный кэш. В конце удалите Worker, удалите локальный секрет и выйдите из системы.

Просмотр новых ответов публичного каталога

На этом шаге вы изучите предоставленный синтетический каталог и проверите его поведение без кэширования. Фикстура создаёт новый UUID для каждого ответа, поэтому повторное использование можно наблюдать без предположений о времени ожидания или базе данных.

cd /home/labex/project/public-cache
node --version
npx wrangler --version
cat src/catalog.js

Ожидайте Node.js v22.22.0 и Wrangler 4.131.1. При настройке были установлены точные зависимости проекта; чтобы воспроизвести существующую установку по lock-файлу, используется npm ci. Среда проверки также использует Miniflare 4.20260730.0, соответствующий дате совместимости. Фикстура различается по языку, категории и синтетическому клиенту; с помощью X-Demo-Failure: 1 можно смоделировать ответ 503. Это тестовые входные данные, а не реальные учётные данные.

WORKER_NAME="labex-cache-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false
}
CONFIG
cat > src/index.js <<'JS'
import {catalog} from './catalog.js';

function deliver(response, cacheStatus) {
  const headers = new Headers(response.headers);
  headers.set('X-Lab-Cache', cacheStatus);
  // This lab caches inside the Worker, not in the caller's browser.
  headers.set('Cache-Control', 'no-store');
  return new Response(response.body, {status: response.status, headers});
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/api/catalog') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const language = url.searchParams.get('lang') || 'en';
    const category = url.searchParams.get('category') || 'network';
    if (!['en', 'fr'].includes(language) || !['network', 'printer'].includes(category) ||
        [...url.searchParams.keys()].some(key => !['lang', 'category'].includes(key)) ||
        url.searchParams.getAll('lang').length > 1 || url.searchParams.getAll('category').length > 1) {
      return Response.json({error: 'invalid_query'}, {status: 400, headers: {'Cache-Control': 'no-store'}});
    }
    if (request.method !== 'GET') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'GET'}});
    }
    return deliver(catalog(request, language, category), 'BYPASS');
  }
};
JS
npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log

Перед отправкой запросов дождитесь готовности сервера. Если запуск ещё продолжается, снова выполните cat dev.log. Оставьте этот терминал открытым, чтобы его переменные оболочки оставались доступными.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

Оба запроса возвращают 200, значение audiencepublic, а UUID в поле generation — разные. X-Lab-Cache: BYPASS означает, что этот обработчик не искал запись в кэше и не записывал её. Клиентский Cache-Control: no-store исключает кэш браузера или другого клиента из эксперимента. Выполните проверку, прежде чем заменять исходную реализацию.

Кэширование только допустимых публичных ответов

На этом шаге вы добавите поиск и сохранение данных через Cache API. Остановите текущий процесс разработки, указанный командой jobs; в примере предполагается, что это задание 1.

jobs
kill %1
cat > src/index.js <<'JS'
import {catalog} from './catalog.js';

function deliver(response, cacheStatus) {
  const headers = new Headers(response.headers);
  headers.set('X-Lab-Cache', cacheStatus);
  // This lab caches inside the Worker, not in the caller's browser.
  headers.set('Cache-Control', 'no-store');
  return new Response(response.body, {status: response.status, headers});
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/api/catalog') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const language = url.searchParams.get('lang') || 'en';
    const category = url.searchParams.get('category') || 'network';
    if (!['en', 'fr'].includes(language) || !['network', 'printer'].includes(category) ||
        [...url.searchParams.keys()].some(key => !['lang', 'category'].includes(key)) ||
        url.searchParams.getAll('lang').length > 1 || url.searchParams.getAll('category').length > 1) {
      return Response.json({error: 'invalid_query'}, {status: 400, headers: {'Cache-Control': 'no-store'}});
    }
    const keyUrl = new URL('/api/catalog', url.origin);
    keyUrl.searchParams.set('category', category);
    keyUrl.searchParams.set('lang', language);
    const key = new Request(keyUrl, {method: 'GET'});
    const cache = caches.default;
    if (request.method !== 'GET') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'GET'}});
    }
    // Decide eligibility before lookup: a warm public entry must not mask private work or errors.
    const bypass = ['Authorization', 'Cookie', 'X-Demo-Customer', 'X-Demo-Failure']
      .some(name => request.headers.has(name));
    if (bypass) return deliver(catalog(request, language, category), 'BYPASS');
    const cached = await cache.match(key);
    if (cached) return deliver(cached, 'HIT');
    const response = catalog(request, language, category);
    if (response.status !== 200 || response.headers.has('Set-Cookie')) {
      return deliver(response, 'BYPASS');
    }
    const stored = response.clone();
    stored.headers.set('Cache-Control', 'public, max-age=10');
    // Await completion here so the next request can observe the write.
    await cache.put(key, stored);
    return deliver(response, 'MISS');
  }
};
JS

Ключ использует текущий источник, фиксированный маршрут, категорию и язык. Порядок параметров канонический, а оба измерения содержимого остаются раздельными. Неизвестные параметры и повторяющиеся измерения отклоняются, поэтому смысл ключа не меняется незаметно.

Проверка допустимости выполняется до поиска в кэше. Заголовки Authorization, Cookie и синтетического клиента обходят уже существующую публичную запись. Заголовок фикстуры ошибки также приводит к обходу поиска, чтобы ошибка не скрывалась кэшированным успешным ответом. Сохраняется только успешный ответ без Set-Cookie. Мы клонируем его, потому что тела ответов являются потоками, задаём сохраняемой копии TTL 10 секунд и дожидаемся завершения записи. Возвращаемые клиенту ответы по-прежнему имеют no-store; у внутренней записи Cache API собственная политика кэширования.

npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log

Перед отправкой запросов дождитесь готовности сервера. Если запуск ещё продолжается, снова выполните cat dev.log. Оставьте этот терминал открытым, чтобы его переменные оболочки оставались доступными.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?category=network&lang=en"
curl -i "http://127.0.0.1:8080/api/catalog?lang=fr&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=printer"

Выполните первые два запроса в течение десяти секунд. Первый запрос без кэшированной записи показывает MISS, повторный — HIT и сохраняет тот же generation. Изменение порядка параметров запроса не меняет ключ. Варианты для французского языка и принтеров содержат запрошенные измерения и используют независимые записи. Если во время проверки TTL истёк, быстро повторите пару запросов; не считайте, что содержимое кэша хранится постоянно.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "X-Demo-Customer: alice"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "X-Demo-Customer: bob"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "Authorization: Bearer synthetic"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "Cookie: demo=synthetic"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "X-Demo-Failure: 1"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

Запросы с данными клиента или идентификацией возвращают BYPASS и соответствующую синтетическую аудиторию, не выдавая результат другого клиента. Смоделированная ошибка возвращает 503 BYPASS, даже если публичные данные уже находятся в кэше. Следующий публичный запрос по-прежнему возвращает публичные данные, а не ошибку. Выполняйте проверку при работающем локальном сервере. Она также запускает обработчик в изолированной локальной среде; облачный кэш при этом не изменяется.

Истечение срока действия и инвалидация локальной записи кэша

На этом шаге вы добавите аутентифицированную операцию инвалидации для того же канонического ключа. Это удаление в локальном центре обработки данных, а не глобальная очистка. Перед редактированием остановите фактический процесс разработки.

jobs
kill %1
umask 077
PURGE_TOKEN=$(openssl rand -hex 24)
printf 'PURGE_TOKEN=%s\n' "$PURGE_TOKEN" > .dev.vars
cat .gitignore

Не добавляйте синтетический секрет в Git, публичную конфигурацию, URL или журналы. Он защищает операцию DELETE в этой лабораторной работе; это не токен Cloudflare API.

cat > src/index.js <<'JS'
import {catalog} from './catalog.js';

function deliver(response, cacheStatus) {
  const headers = new Headers(response.headers);
  headers.set('X-Lab-Cache', cacheStatus);
  // This lab caches inside the Worker, not in the caller's browser.
  headers.set('Cache-Control', 'no-store');
  return new Response(response.body, {status: response.status, headers});
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({status: 'ok'}, {headers: {'Cache-Control': 'no-store'}});
    }
    if (url.pathname !== '/api/catalog') {
      return Response.json({error: 'not_found'}, {status: 404});
    }
    const language = url.searchParams.get('lang') || 'en';
    const category = url.searchParams.get('category') || 'network';
    if (!['en', 'fr'].includes(language) || !['network', 'printer'].includes(category) ||
        [...url.searchParams.keys()].some(key => !['lang', 'category'].includes(key)) ||
        url.searchParams.getAll('lang').length > 1 || url.searchParams.getAll('category').length > 1) {
      return Response.json({error: 'invalid_query'}, {status: 400, headers: {'Cache-Control': 'no-store'}});
    }
    const keyUrl = new URL('/api/catalog', url.origin);
    keyUrl.searchParams.set('category', category);
    keyUrl.searchParams.set('lang', language);
    const key = new Request(keyUrl, {method: 'GET'});
    const cache = caches.default;
    if (request.method === 'DELETE') {
      if (!env.PURGE_TOKEN) return Response.json({error: 'purge_unconfigured'}, {status: 503});
      if (request.headers.get('Authorization') !== `Bearer ${env.PURGE_TOKEN}`) {
        return Response.json({error: 'unauthorized'}, {status: 401, headers: {'Cache-Control': 'no-store'}});
      }
      const invalidated = await cache.delete(key);
      return Response.json({invalidated, scope: 'this-location'}, {
        headers: {'Cache-Control': 'no-store', 'X-Lab-Cache': 'BYPASS'}
      });
    }
    if (request.method !== 'GET') {
      return Response.json({error: 'method_not_allowed'}, {status: 405, headers: {Allow: 'GET, DELETE'}});
    }
    // Decide eligibility before lookup: a warm public entry must not mask private work or errors.
    const bypass = ['Authorization', 'Cookie', 'X-Demo-Customer', 'X-Demo-Failure']
      .some(name => request.headers.has(name));
    if (bypass) return deliver(catalog(request, language, category), 'BYPASS');
    const cached = await cache.match(key);
    if (cached) return deliver(cached, 'HIT');
    const response = catalog(request, language, category);
    if (response.status !== 200 || response.headers.has('Set-Cookie')) {
      return deliver(response, 'BYPASS');
    }
    const stored = response.clone();
    stored.headers.set('Cache-Control', 'public, max-age=10');
    // Await completion here so the next request can observe the write.
    await cache.put(key, stored);
    return deliver(response, 'MISS');
  }
};
JS

DELETE проверяет учётные данные перед вызовом cache.delete с тем же GET-ключом, который используется для поиска и сохранения. Возвращаемое логическое значение показывает, существовала ли запись в этом месте. Неавторизованное удаление не должно её затронуть. В другом месте обслуживания может находиться собственная запись, которую запрос всё ещё сможет получить.

npx wrangler dev --ip 0.0.0.0 --port 8080 > dev.log 2>&1 &
cat dev.log

Перед отправкой запросов дождитесь готовности сервера. Если запуск ещё продолжается, снова выполните cat dev.log. Оставьте этот терминал открытым, чтобы его переменные оболочки оставались доступными.

curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i -X DELETE "http://127.0.0.1:8080/api/catalog?lang=en&category=network"
curl -i -X DELETE "http://127.0.0.1:8080/api/catalog?lang=en&category=network" -H "Authorization: Bearer $PURGE_TOKEN"
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

Неавторизованный DELETE возвращает 401. Корректный DELETE возвращает scope: this-location и обычно invalidated: true, если запись ещё не истекла. Значение false также имеет смысл, если короткий TTL уже закончился. Следующий GET возвращает MISS с новым значением generation. Чтобы гарантированно получить true, выполните GET непосредственно перед авторизованным DELETE.

sleep 11
curl -i "http://127.0.0.1:8080/api/catalog?lang=en&category=network"

Через одиннадцать секунд новый MISS показывает истечение срока действия без явного удаления. Выполняйте проверку при работающем сервере: изолированная среда проверяет повторное использование, разделение измерений, исключение приватных ответов и ошибок, отклонение удаления, успешное адресное удаление, сохранение другого ключа и истечение срока действия. Эти контролируемые локальные проверки дают воспроизводимые доказательства и не требуют предположений о глобальном состоянии кэша.

В документации Cache API описаны область действия центра обработки данных, поведение заголовков ответа и cache.delete. Cache API и кэширование платформы, при котором выполнение Worker пропускается, — это разные механизмы.

Развёртывание и проверка границ кэша

На этом шаге вы развернёте готовый обработчик в своей учебной учётной записи. Остановите локальный процесс, авторизуйте эту новую виртуальную машину, а затем проверьте идентификатор учётной записи.

jobs
kill %1
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_tail:read

Откройте показанную ссылку для устройства, введите код в браузере, где выполнен вход, проверьте неизменённые разрешения и Background Access, затем выберите свою учебную учётную запись. Дождитесь сообщения об успешном завершении в терминале.

npx wrangler whoami --json

Проверьте имя нужной учётной записи. Замените YOUR_ACCOUNT_ID ниже её фактическим идентификатором, сохранив уникальное имя.

cat > wrangler.jsonc <<CONFIG
{
  "name": "$WORKER_NAME",
  "main": "src/index.js",
  "compatibility_date": "2026-07-30",
  "workers_dev": true,
  "preview_urls": false,
  "account_id": "YOUR_ACCOUNT_ID"
}
CONFIG
npx wrangler deploy
npx wrangler secret bulk .dev.vars
npx wrangler secret list

Локальный файл секретов не загружается при развёртывании; явная команда bulk создаёт PURGE_TOKEN как secret_text. После развёртывания подождите немного, пока изменения распространятся. Скопируйте фактический публичный URL, показанный Wrangler.

APP_URL="https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev"
curl -i "$APP_URL/api/catalog?lang=en&category=network"
curl -i "$APP_URL/api/catalog?category=network&lang=en"
curl -i "$APP_URL/api/catalog?lang=fr&category=network"
curl -i "$APP_URL/api/catalog?lang=en&category=network" -H "X-Demo-Customer: alice"
curl -i "$APP_URL/api/catalog?lang=en&category=network" -H "X-Demo-Customer: bob"
curl -i "$APP_URL/api/catalog?lang=en&category=network" -H "X-Demo-Failure: 1"

Публичные ответы должны содержать запрошенные язык и категорию, а также публичную аудиторию. Повторные запросы из одного места в течение TTL могут показать HIT и сохранить значение generation; в другом месте или после истечения срока действия MISS является нормальным результатом. По двум запросам нельзя утверждать, что содержимое глобально общее. Приватные запросы всегда должны обходить кэш, а запрос с ошибкой должен возвращать 503 BYPASS.

В той же учётной записи Dashboard откройте Compute → Workers & Pages и проверьте точный Worker и его URL workers.dev. Выполните проверку, чтобы убедиться во владении, привязке развёрнутого секрета и границах ответов. Эта проверка не выполняет облачную инвалидацию. Поведение инвалидации проверялось локально; cache.delete не является механизмом глобальной очистки. Если развёртывание ещё распространяется, немного подождите и повторите проверки ответов; устойчивое несоответствие нужно исследовать, а не принимать без проверки.

Удаление временного Worker

На этом шаге удалите развёртывание лабораторной работы, пока авторизация ещё действует. Проверьте уникальное имя и учётную запись, затем удалите только этот Worker.

cat wrangler.jsonc
npx wrangler delete

Когда появится запрос с совпадающим именем, нажмите одну клавишу y. После удаления Wrangler 4.131.1 может вывести известное диагностическое сообщение об аутентификации устаревшей функции Workers Sites KV. Не расширяйте разрешения и не считайте эту ошибку доказательством результата. Обновите Dashboard и выполните проверку: успешный аутентифицированный список должен показывать отсутствие именно этого Worker. Сохраните учебную учётную запись и её поддомен. Удаление Worker не означает, что все записи кэша были глобально очищены; синтетические записи имеют TTL десять секунд, и работающего приложения больше не должно оставаться.

Удаление локального секрета и отключение

На этом шаге после проверки удаления удалите локальные временные учётные данные, а затем отключите эту виртуальную машину.

rm .dev.vars
unset PURGE_TOKEN
npx wrangler logout
npx wrangler whoami --json

Убедитесь, что явно указано loggedIn: false; команда со структурированным выводом без аутентификации может завершиться с ненулевым кодом. Выполните проверку и завершите работу виртуальной машины. Вход в браузере может остаться активным. Выход из системы или завершение работы виртуальной машины не удаляет облачное развёртывание автоматически.

Итоги

Вы заменили генерацию каталога без кэширования явным кэшированием публичных ответов, сохранили раздельные ключи для языка и категории и организовали обход приватных запросов и запросов с ошибками до поиска в кэше. Вы проверили короткоживущие записи и аутентифицированную инвалидацию в контролируемой локальной среде, а затем проверили идентификатор развёрнутого приложения и границы его ответов, не предполагая глобального общего содержимого кэша.

TTL сохраняемой копии и политика кэширования для вызывающей стороны служат разным целям. Явные правила допустимости, полные ключи и наблюдаемые поколения ответов позволяют проверить это различие. Перед отключением виртуальной машины вы удалили временное развёртывание и локальные учётные данные.