↳ Zápisky

Když web spadne, ať zákazník nevidí „Error 522“. Vlastní stránka výpadku přes Cloudflare Worker (Free plán)

Každý server občas spadne – po restartu kvůli aktualizaci, při přetížení nebo když má výpadek provider. Když máte web za Cloudflare proxy (oranžový mráček), návštěvník v tu chvíli místo vašeho webu uvidí tohle:

Výchozí chybová stránka Cloudflare: Connection timed out, Error code 522

Stránka je anglicky, technická, bez loga a bez kontaktu. Zákazník neví, jestli firma ještě existuje, a u e-shopu se nedozví, že by mohl objednat telefonem.

Na svém webu jsem to vyřešil jedním malým Cloudflare Workerem. Za normálního provozu nedělá nic a teprve když Cloudflare nedosáhne na server, vrátí stránku v barvách webu, česky a s kontaktem.

Vlastní stránka výpadku na hilgert.cz: logo, Za chvíli jsem zpět a e-mailový kontakt

Proč ne vestavěné „Error Pages“?

Cloudflare má v zóně sekci Error Pages (Custom Errors), která dělá přesně tohle, jenže jen na placených plánech Pro a vyšších. Na Free plánu tam uvidíte jen tlačítko „Upgrade plan“.

Sekce Error Pages v Cloudflare s tlačítkem Upgrade plan, dostupná jen na placených plánech

Worker je na Free plánu zdarma, jen má denní limit požadavků. Na ten si dejte pozor, víc o něm v kroku 5.

Jak to funguje

Worker stojí mezi návštěvníkem a serverem. Každý požadavek pošle beze změny dál na server (fetch(request)) a odpověď vrátí. Zasáhne, jen když přijde jedna z chyb, které generuje sám Cloudflare, protože se na server nedostal:

KódCo znamená
520server vrátil nesmysl nebo prázdnou odpověď
521server odmítá spojení (web server neběží)
522spojení vypršelo (server neodpovídá)
523server je nedosažitelný
524server neodpověděl včas
525, 526problém s SSL mezi Cloudflare a serverem
530chyba DNS originu

Důležité je, co Worker nedělá, aby stránku neukázal omylem:

  • Neřeší chyby z vašeho serveru (500, 502, 503). Když spadne PHP nebo databáze, má to být vidět. Jinak byste chybu aplikace zamaskovali a nevěděli o ní.
  • Zasáhne jen u GET/HEAD s Accept: text/html, tedy když prohlížeč chce stránku. Odeslaný formulář (POST), AJAX, wp-json, obrázky ani API klienti stránku výpadku nedostanou – dostanou původní chybu. Formulář tak nikdy nepředstírá, že se odeslal, a JSON klient nedostane HTML.
  • Vrací status 503 s Retry-After a Cache-Control: no-store. Google tak pozná dočasný výpadek a nic se neuloží do cache.

Krok 1: Vytvořte Worker

V levém menu účtu otevřete Compute → Workers & Pages a klikněte na Create application.

Přehled Workers & Pages s tlačítkem Create application

Zvolte Start with Hello World!.

Volba Start with Hello World! při vytváření aplikace

Pojmenujte Worker (třeba fallback) a klikněte na Deploy. Kód z ukázky teď nevadí, za chvíli ho přepíšeme. Worker zatím nikde neběží – dokud mu nepřiřadíte route, žádného webu se nedotkne.

Formulář Deploy Hello World s názvem Workeru a tlačítkem Deploy

Krok 2: Vložte kód

V detailu Workeru klikněte vpravo nahoře na Edit code, smažte ukázku, vložte kód níž a klikněte na Deploy.

Editor kódu Workeru v dashboardu Cloudflare

Tohle je zjednodušená verze pro jeden web. Upravte SITE (název, barvy, e-mail):

// Errors Cloudflare generates itself when it cannot reach the origin.
const ORIGIN_DOWN = new Set([520, 521, 522, 523, 524, 525, 526, 530]);

const SITE = {
  name: "Můj web",
  bg: "#ffffff",
  text: "#1a1a1a",
  accent: "#1e40af",
  contact: "[email protected]",
};

export default {
  async fetch(request) {
    let response = null;
    try {
      response = await fetch(request);
    } catch (e) {
      // Network error on the way to the origin - treat it as down.
    }
    if (response && !ORIGIN_DOWN.has(response.status)) return response;

    const wantsHtml = ["GET", "HEAD"].includes(request.method)
      && (request.headers.get("accept") || "").includes("text/html");
    if (!wantsHtml) {
      return response ?? new Response("Service Unavailable", {
        status: 503,
        headers: { "cache-control": "no-store" },
      });
    }

    return new Response(request.method === "HEAD" ? null : page(SITE), {
      status: 503,
      headers: {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-store",
        "retry-after": "300",
        "x-fallback": "1",
      },
    });
  },
};

function page(s) {
  return `<!doctype html>
<html lang="cs"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>${s.name} - za chvíli jsme zpět</title>
<style>
  body { margin: 0; min-height: 100vh; display: grid; place-items: center;
         background: ${s.bg}; color: ${s.text};
         font: 18px/1.6 system-ui, sans-serif; padding: 24px; box-sizing: border-box; }
  main { max-width: 34rem; }
  h1 { color: ${s.accent}; line-height: 1.2; }
  a { color: ${s.accent}; }
</style></head>
<body><main>
  <h1>Za chvíli jsme zpět</h1>
  <p>Web je dočasně nedostupný kvůli údržbě nebo technickému problému.
     Už na tom pracujeme, zkuste to prosím za pár minut.</p>
  <p>V naléhavém případě nám napište na
     <a href="mailto:${s.contact}">${s.contact}</a>.</p>
</main></body></html>`;
}

Pár poznámek ke kódu:

  • Všechno musí být uvnitř Workeru. Logo, CSS ani obrázky se z mrtvého serveru nenačtou. Logo vložte jako data URI (data:image/svg+xml;base64,...), fonty jde tahat z Google Fonts.
  • Texty si přizpůsobte. Kromě e-mailu tam můžete přidat telefon, odkaz na sociální sítě nebo cokoli dalšího, co se zákazníkovi při výpadku hodí.
  • Náhled v editoru vpravo posílá požadavky z IP adres Cloudflare. Pokud máte v zóně pravidla, která blokují IP adresy, uvidíte tam Error 1106 Access denied. To nevadí, testujte na skutečné adrese (krok 4).

Krok 3: Vypněte adresu workers.dev

Každý nový Worker dostane veřejnou adresu <jméno>.<účet>.workers.dev. Pro tenhle účel ji nepotřebujete a na ní by fetch(request) stejně nedával smysl. V detailu Workeru otevřete záložku Domains a v části Worker URL vypněte přepínače u Production i Preview.

Záložka Domains: vypnuté Worker URL pro Production i Preview a seznam route

Na stejné záložce pak uvidíte i route, které Worker obsluhuje.

Krok 4: Bezpečný test bez vypínání serveru

Shazovat kvůli testu ostrý web asi nechcete. Stačí v zóně založit testovací subdoménu, která vede na adresu, kde nikdy nic neběží, a Cloudflare na ní pak vždy vrátí 522.

V zóně otevřete DNS → Records → Add record a vyplňte:

  • Type: A
  • Name: fallback-test
  • IPv4 address: 192.0.2.1 (dokumentační adresa z RFC 5737, nikam nevede)
  • Proxy status: Proxied (oranžový mráček – bez něj Worker neběží)
Přidání DNS záznamu A fallback-test na 192.0.2.1 se zapnutou proxy
Uložený testovací DNS záznam fallback-test v seznamu záznamů

Krok 5: Přiřaďte Worker k webu (route) a nastavte fail open

V zóně otevřete Workers Routes a klikněte na Add route.

  • Route: nejdřív fallback-test.vasedomena.cz/*, po otestování vasedomena.cz/* a www.vasedomena.cz/* (každá jako samostatná route).
  • Worker: váš Worker.
  • Request limit failure mode: rozbalte a zvolte Fail open (proceed).
Formulář Add route s vybraným Fail open (proceed)

Tohle poslední nastavení je v celém návodu nejdůležitější. Free plán má limit 100 000 požadavků na Workery denně, a to za celý účet, ne za doménu. Výchozí volba je Fail closed (block): když limit vyčerpáte, Cloudflare začne požadavky na vaše weby blokovat. Pojistka proti výpadku by tak sama způsobila výpadek. S Fail open jdou požadavky po vyčerpání limitu rovnou na server, jako by Worker neexistoval.

Úprava route s nastaveným Fail open (proceed)

Route pište na konkrétní hostnamy, ne *.vasedomena.cz/*. Hvězdička na začátku by zachytila i vývojové a interní subdomény, kde Worker nechcete.

Workers Routes se třemi route na konkrétní hostnamy

Krok 6: Ověřte, že to funguje (a že to neruší)

Testovací host by měl vrátit stránku výpadku:

curl -sI -H "accept: text/html" https://fallback-test.vasedomena.cz/
# HTTP/2 503, x-fallback: 1, retry-after: 300, cache-control: no-store

API požadavek na stejném hostu má dostat původní chybu, ne HTML:

curl -sI -H "accept: application/json" https://fallback-test.vasedomena.cz/
# HTTP/2 522

A ostrý web se má po přidání route chovat přesně jako předtím. Projděte úvodní stránku, pár podstránek, administraci, odeslání formuláře. Hlavička x-fallback se na běžícím webu nesmí objevit nikdy.

Stránku si otevřete i v prohlížeči a na mobilu. Takhle vypadá stránka z ukázkového kódu výše:

Stránka výpadku z ukázkového kódu na mobilu: Za chvíli jsme zpět

Pozor na limit: počítá se každý požadavek, ne jen výpadky

Worker běží stále, ne jen při výpadku

Worker neví předem, že server spadl – zjistí to až z jeho odpovědi. Proto běží na každém požadavku, který route projde, i když web normálně funguje.

A návštěva jedné stránky není jeden požadavek: prohlížeč si zvlášť stáhne HTML, každý CSS a JS soubor, každý obrázek, fonty, AJAX volání. Na WordPressu to bývá 30-80 požadavků na jedno zobrazení. Nepomůže ani cache Cloudflare – Worker na route běží před ní.

Kolik to dělá reálně, uvidíte v detailu Workeru na záložce Metrics (Invocations) – i rozpad podle hostnamu:

Záložka Metrics s počtem volání Workeru a rozpadem podle hostnamu

Můj web hilgert.cz dělá zhruba 2 500-3 000 volání denně, takže se do limitu vejde s velkou rezervou. Pokud ale máte v účtu další Workery nebo chcete Worker přidat i na další weby, volání se sčítají. Web s větším provozem (e-shop, zpravodajský web) se přes 100 tisíc dostane snadno. Pak jsou dvě rozumné cesty:

  • Workers Paid za 5 USD měsíčně – 10 milionů požadavků měsíčně v ceně.
  • Zůstat na Free s fail open a smířit se s tím, že po vyčerpání limitu se při výpadku ukáže zase obyčejná chyba Cloudflare.

Optimalizace: vynechte statické soubory

Stránku výpadku potřebuje jen HTML. Obrázky, CSS a JS z wp-content a wp-includes Worker jen pošle dál beze změny, ale do limitu se počítají. Můžete je z Workeru vyjmout: v Workers Routes přidejte další route se stejným hostnamem a konkrétní cestou a jako Worker zvolte None:

  • www.vasedomena.cz/wp-content/* → Worker: None
  • www.vasedomena.cz/wp-includes/* → Worker: None

Cloudflare u každého požadavku použije nejkonkrétnější route, takže statika půjde mimo Worker a všechno ostatní dál přes něj. Při výpadku se pak u obrázků ukáže obyčejná chyba Cloudflare, ale tu stejně nikdo nevidí – prohlížeč je potřebuje jen pro stránku, která se nenačetla.

Kolik to ušetří, záleží na webu – na mých webech to bylo 10-70 % volání. Méně ušetří malý web, kde nejspíš většinu provozu tvoří roboti stahující jen HTML a vracející se návštěvníci mají statiku v cache prohlížeče. Víc e-shop se skutečnými zákazníky a spoustou produktových fotek. Změřte si to na svém webu v Metrics před změnou a den po ní.

Co to neřeší

  • Když server neodpovídá vůbec (522), Cloudflare čeká na spojení zhruba 20 sekund, než Worker dostane chybu (naměřil jsem 19,7 s). Zkrátit to bezpečně nejde – Worker nepozná pomalou stránku od mrtvého serveru.
  • Worker běží jen na záznamech s oranžovým mráčkem. Weby, které proxy nemají (třeba kvůli 100s limitu Cloudflare na dlouhé požadavky), stránku výpadku neukážou. Dlouhé crony je lepší pouštět z příkazové řádky na serveru a dlouhá API dát na subdoménu bez proxy – hlavní web pak může za proxy zůstat.

Nechcete tohle řešit sám?

Aktualizace, zálohy, bezpečnost a drobné úpravy pohlídám za vás. Balíčky správy začínají na 300 Kč měsíčně.

Správa webu →

Komentáře

Zatím žádné komentáře. Buďte první.

Přidat komentář

Odesláním komentáře mi předáte jméno, technicky se uloží i vaše IP adresa (po roce ji mažu). E-mail uložím jen tehdy, když zaškrtnete sledování komentářů; nepotvrzený odběr se po týdnu maže i s e-mailem, potvrzený držím do odhlášení. Podrobnosti v zásadách zpracování osobních údajů →

Komentáře schvaluji ručně, zobrazí se po schválení.