Googlebot ignoruje sendBeacon() – testy i wnioski

Sprawdziłem to empirycznie po tym jak w Google Search Console zauważyłem nagły skok do 131 tysięcy liczbę adresów URL oznaczonych jako „Strona zablokowana z powodu innego błędu 4xx”. Przygotowałem izolowany test porównujący sendBeacon() z fetch() na tej samej stronie, wymusiłem wizytę Googlebota i przeanalizowałem logi skryptu i serwera.

googlebot sendbeacon

Wynik po 2 dniach testów i 5 wariantach URL oraz 20 zarejestrowanych żądaniach: 0 wywołań sendBeacon(), 20 wywołań fetch(). Wychodzi na to, że Googlebot konsekwentnie pomija sendBeacon(), mimo że obydwa wywołania znajdowały się w tym samym skrypcie JavaScript i tu robi się ciekawiej.

Odkryty problem z błędami 4xx w GSC nie miał nic wspólnego z tym zjawiskiem. Mechanizm crawlowania Google, który wyłapuje ciągi znaków przypominające adresy URL bezpośrednio w kodzie źródłowym strony (bez wykonywania JavaScriptu) i odpytuje je standardowym żądaniem GET. To są 2 pozornie powiązane zjawiska i 2 zupełnie osobne mechanizmy Google działające równolegle na tym samym kodzie.

Punkt wyjścia – problem w Google Search Console

Wszystko zaczęło się od rutynowego przeglądu Google Search Console. W zakładce dotyczącej indeksowania stron zauważyłem nagły wzrost liczby adresów URL sklasyfikowanych pod statusem: „Strona zablokowana z powodu innego błędu 4xx”, jak na obrazie:

beacon api 4xx gsc

Każdy z tych adresów kończy się unikalnym identyfikatorem UUID konkretnego artykułu.

Postanowiłem więc zbadać to zachowanie u źródła. Skopiowałem adres z GSC i wkleiłem go bezpośrednio do paska adresu przeglądarki, a serwer zwrócił: HTTP/1.1 405 Method Not Allowed 🙂

Całkiem logiczne, bo wpisywanie adresu z palca zawsze wysyła żądanie metodą GET podczas gdy endpointy analityczne projektuje się zazwyczaj pod odbiór danych metodą POST.

Gdy wszedłem na sam artykuł i otworzyłem narzędzia deweloperskie (Chrome DevTools), to samo żądanie wykonało się bezbłędnie:

  • Metoda:POST
  • Kod: 202 Accepted
  • Nagłówki:sec-fetch-mode: no-cors, sec-fetch-site: same-origin

Przeszedłem do analizy kodu źródłowego strony, a tam znalazłem:

<script nonce="Qx7mK2vR9NpL4sT8ZyWcEg==">
(function(){
  try {
    navigator.sendBeacon('/api/portal/views/article/0eb9a8-xxxxxxxxxxxx-6dee71d8a784');
  } catch(e) {}
})(); 
</script>

Przeglądarka wykonuje ten skrypt, co skutkuje wysłaniem beacona (żądania typu POST) i poprawną odpowiedzią 202. Dlaczego więc Googlebot uparcie raportował błąd 405 (a więc wysyłał GET)?

Pierwsza myśl jest taka „Googlebot uruchamia JavaScript, poprawnie go renderuje, próbuje wywołać navigator.sendBeacon(), ale z jakiegoś powodu modyfikuje metodę HTTP i zamiast POST, wysyła GET„. To tłumaczyłoby odpowiedź serwera 405 ale… w zwyczaju mam podważać pierwotne założenia dlatego pomyślałem „a co w przypadku gdy Googlebot nie wykonuje tego kodu i wyciąga to w inny sposób?„.

Postanowiłem sprawdzić, czyli klasycznie jak to u mnie – TESTUJEMY! 😀

co Googlebot robi z sendBeacon(), a czego nie robi wcale.

Metodologia testu – co Googlebot robi z sendBeacon() i czego nie robi

Na moim SEO-poligonie1 stworzyłem całkowicie odizolowane środowisko testowe żeby wyeliminować czynniki zakłócające (zewnętrzne skrypty, cache przeglądarki ruch użytkowników).

Postawiłem obok siebie sendBeacon() oraz fetch() żeby sprawdzić, które z nich i jakimi metodami zostanie wywołane przez Googlebota. Poniżej przedstawiam gotowce, które możesz skopiować i przeprowadzić swój własny test.

Krok 1: Endpoint logujący (backend)

Serce testu to prosty PHP, którego.zadaniem było przechwytywanie każdego przychodzącego żądania i zapisywanie szczegółowych metadanych bezpośrednio do pliku tekstowego na serwerze.

<?php
$logFile = __DIR__ . '/beacon-test.log';

$entry = [
    'time'        => date('d-m-Y H:i:s'),
    'method'      => $_SERVER['REQUEST_METHOD'],
    'ip'          => $_SERVER['REMOTE_ADDR'],
    'user_agent'  => $_SERVER['HTTP_USER_AGENT'] ?? '',
    'endpoint'    => $_SERVER['REQUEST_URI'],
    'referer'     => $_SERVER['HTTP_REFERER'] ?? '',
];

file_put_contents($logFile, json_encode($entry) . PHP_EOL, FILE_APPEND);

http_response_code(202);

Skrypt obsługiwał obydwa testowane wywołania i rozróżniał prostym parametrem ?type=:

  • /test-beacon-log.php?type=beacon
  • /test-beacon-log.php?type=fetch

Krok 2: Strona testowa (frontend)

Czyściutki, minimalistyczny HTML niezawierający żadnej treści poza prostym skryptem JavaScript wykonującym oba żądania jedno po drugim:

<!DOCTYPE html>
<html lang="pl">
<head>
    <meta charset="UTF-8">
    <title>Test Googlebot sendBeacon</title>
</head>
<body>
    <h1>Test sendBeacon vs fetch</h1>
    <script>
    (function(){
      try {
        navigator.sendBeacon('/test-beacon-log.php?type=beacon');
      } catch(e) {}

      try {
        fetch('/test-beacon-log.php?type=fetch', { 
            method: 'POST'
        });
      } catch(e) {}
    })();
    </script>
</body>
</html>

Krok 3: Test z Googlebotem

Zgłosiłem strony do indeksowania przez GSC, a żeby wykluczyć wpływ ewentualnego cache’owania wyników renderowania, test powtarzałem w odstępach czasu za każdym razem zmieniając parametr w adresie URL (?v=2, ?v=3, ?v=4, ?v=5).

Wszystkie zaobserwowane adresy IP (m.in 66.249.72.201, 66.249.72.202, 66.249.72.203) mieszczą się w oficjalnie publikowanej przez Google puli adresów wykorzystywanych przez Googlebota2 (zakres 66.249.64.0/19).

Wyniki, czyli surowe dane z logów

Test przeprowadziłem w dwóch niezależnych sesjach w odstępie 12 godzin na 5 różnych wariantach adresu URL.

Łącznie w logach serwera zarejestrowałem 20 żądań wywołanych przez dwa różne mechanizmy: Google-InspectionTool oraz Googlebot/2.1.

Tabela zbiorcza wyników

DataMechanizmWariant URLEndpoint beaconEndpoint fetch
11.08Google-InspectionToolv102
11.08Googlebot/2.1v102
12.08Google-InspectionToolv2, v3, v4, v508
12.08Googlebot/2.1v2, v3, v4, v508
Razem5 wariantów020

Wynik jest jednoznaczny: żadne z 20 zarejestrowanych żądań nie dotyczyło endpointu type=beacon. Wszystkie 20 to poprawnie wykonane wywołania fetch(), każdorazowo metodą POST, zgodnie z kodem strony testowej.

Niezależnie od logów dostarczanych przez skrypt postanowiłem jeszcze sprawdzić logi serwera i moja uwagę zwróciła też kolejność zdarzeń, bo pokazuje wyraźnie dwuetapowy proces obsługi strony przez Google.

Za każdym razem Googlebot najpierw sprawdzał robots.txt, a następnie pobierał HTML metodą GET. Dopiero 2 sekundy później wykonywał żądania POST wygenerowane przez fetch() w JavaScript.

Analiza – 2 niezależne mechanizmy Google

Wyniki testu jednoznacznie potwierdzają, że Web Rendering Service (WRS), czyli silnik odpowiedzialny za renderowanie JavaScriptu w ramach infrastruktury Google – konsekwentnie ignoruje wywołania navigator.sendBeacon(), jednocześnie w pełni poprawnie wykonując równoległe wywołanie fetch().

To jednak nie wyjaśnia jeszcze, skąd biorą się błędy 405 zgłaszane w Google Search Console. Skoro WRS ignoruje sendBeacon(), nie może on być bezpośrednią przyczyną żądań GET trafiających na endpoint /api/portal/views/article/.... Musi zatem istnieć inny, niezależny mechanizm, który generuje te żądania.

2 różne procesy, 2 różne cele

Żeby to zrozumieć, trzeba rozróżnić 2 odrębne podsystemy działające w ramach infrastruktury Google, które działają zupełnie niezależnie od siebie:

  • 1. Crawler (klasyczny Googlebot) – jako headless Chrome, jego zadaniem jest pobranie HTML dokładnie tak, jak przesyła go serwer. Bez wykonywania jakiegokolwiek JavaScriptu. Ten sam crawler odpowiada również za mechanizm polegający na skanowaniu pobranego kodu źródłowego (wliczając w to <script>) w poszukiwaniu ciągów znaków przypominających adresy URL. Nie ma znaczenia, czy dany fragment kodu JavaScript zostanie kiedykolwiek wykonany – wystarczy, że wygląda jak ścieżka lub link, aby trafić do kolejki. Właśnie to Google opisuje w dokumentacji sprawdzonych metod dotyczących linków 3.
  • 2. Web Rendering Service (WRS) – na tym etapie JavaScript zostaje faktycznie wykonany, w headless Chrome. WRS otrzymuje HTML pobrany wcześniej przez crawler, renderuje stronę i wykonuje osadzony JavaScript pomijając sendBeacon(), który potwierdziłem w teście.

Skąd więc błąd 405?

Zestawiając ze sobą 2 mechanizmy. Próba bezpośredniego wejścia na adres API w przeglądarce generuje żądanie metodą GET. Dokładnie to samo zachowanie replikuje mechanizm odkrywania adresów URL w ramach crawlera:

  1. Crawler pobiera surowy HTML
  2. Podczas statycznej analizy tekstu natrafia na ciąg /api/portal/views/article/… zaszyty wewnątrz kodu JS
  3. Trafia do kolejki adresów wartych sprawdzenia, tak jak każdy zwykły link.
  4. Crawler odpytuje ten adres standardowym żądaniem GET
  5. Serwer skonfigurowany do obsługi metody POST na tym endpointcie odpowiada 405 Method Not Allowed
  6. GSC klasyfikuje ten wynik jako „Stronę zablokowaną z powodu innego błędu 4xx”

Nie mam innego wytłumaczenia 🙂

Najważniejszy wniosek

Mamy więc do czynienia z 2 pozornie powiązanymi ale całkowicie niezależnymi przyczynowo zjawiskami:

ZjawiskoMechanizm odpowiedzialnySkutek
Ignorowanie sendBeacon()Web Rendering ServiceBrak wpływu na dane analityczne z wizyt bota – ale też brak wpływu na błędy w GSC
Błędy 405 w GSCCrawlerRosnąca lista „zablokowanych” adresów w raporcie 4xx

Innymi słowy: naprawienie problemu z sendBeacon() (np. zmiana na fetch()) nie rozwiązałaby problemu błędów 4xx w Search Console – bo przyczyna leży zupełnie gdzie indziej, na wcześniejszym etapie przetwarzania strony przez Google, zanim jakikolwiek JavaScript zostanie w ogóle wykonany.

W trakcie pisania przypomniał mi się post na Linkedin Gary’ego Illyesa (Analyst w Google), który poruszył temat nietypowych żądań HTTP generowanych przez crawlery Google.

Wspomniał, że crawlery Google wysyłają czasem metody inne niż GET/POST (HEAD, OPTIONS, PUT, PATCH, DELETE – łącznie mniej niż 1,5% ruchu), a przyczyną jest właśnie JavaScript wykonywany podczas renderowania. To potwierdza ogólny mechanizm, że WRS faktycznie inicjuje żądania sieciowe na bazie kodu JavaScript. Nie odnosi się wprost do sendBeacon() ale można potraktować to jako sygnał, że temat „dziwnych” zachowań sieciowych crawlerów podczas renderowania JavaScript jest cały czas aktualny.

Praktyczne rozwiązanie problemu z GSC

Po ustaleniu prawdziwej przyczyny błędów 405 rozwiązanie problemu staje się proste 🙂

Krok 1: Zablokuj ścieżkę API w robots.txt

To najprostszy i najskuteczniejszy sposób, by zapobiec kolejkowaniu tych adresów do crawlowania. Wystarczy dodać jedną regułę:

User-agent: *
Disallow: /api/portal/views/

Po wdrożeniu tej zmiany crawler Google przestanie w ogóle próbować odwiedzać adresy w tej ścieżce.

Ważne: dla pewności że Google faktycznie zauważył zmianę w pliku. W GSC można poprosić o ponowne sprawdzenie robots.txt

  • Ustawienia → Robots.txt → Otwórz raport
  • 3 kropki → Poproś o ponowne zaindeksowanie

Krok 2: Zmień odpowiedź serwera z 405 na 404

robots.txt działa na zasadzie „grzecznej prośby” – większość legalnych crawlerów (w tym Googlebot) go respektuje ale nie jest to twarde zabezpieczenie. Jesli Googlebot znajdzie taki adres na innej stronie, to może ignorować robots.txt. Dlatego dla żądań GET można zwracać 404 Not Found.

Przykładowa dyrektywa do .htaccess

<IfModule mod_rewrite.c>
    RewriteEngine On

    RewriteCond %{REQUEST_METHOD} !^POST$
    RewriteRule ^api/portal/views/article/ - [R=404,L]
</IfModule>

Dla nginx powinno to zadziałać

location ~ ^/api/portal/views/article/ {
    if ($request_method != POST) {
        return 404;
    }
}

Kontekst techniczny – czym jest sendBeacon()

Dla czytelników, którzy dotarli aż tutaj (gratuluję!) i chcieliby uzupełnić wiedzę o samym mechanizmie – krótkie wyjaśnienie na koniec.

navigator.sendBeacon() to API przeglądarkowe zaprojektowane do wysyłania niewielkich porcji danych telemetrii i analityki bez wpływu na wydajność strony czy doświadczenie użytkownika. Typowe zastosowania to:

  • Rejestrowanie odsłon artykułów lub podstron
  • Śledzenie zdarzeń „przed opuszczeniem strony” (np. porzucenie koszyka, zamknięcie formularza)
  • Wysyłanie danych telemetrycznych tuż przed zamknięciem karty przeglądarki

Przewaga sendBeacon() nad standardowym fetch() czy XMLHttpRequest polega na 2 cechach:

  1. Nieblokujące wykonanie (non-blocking) – wywołanie sendBeacon() nie opóźnia nawigacji do kolejnej strony ani zamknięcia karty
  2. Gwarancja dokończenia żądania – w przeciwieństwie do zwykłego fetch(), które może zostać przerwane w momencie zamknięcia karty lub przejścia na inną stronę, to przeglądarka gwarantuje, że żądanie wysłane przez sendBeacon() zostanie dostarczone do serwera nawet jeśli strona zostanie natychmiast zamknięta

Tabela porównawcza

CechasendBeacon()fetch() (bez keepalive)fetch() z keepalive: true
Blokuje wykonanie skryptuNieNie ale żądanie może zostać przerwaneNie
Przetrwa zamknięcie kartyTakNieW większości przypadków tak
Dostęp do odpowiedzi serweraNieTakTak
Metoda HTTPZawsze POSTDowolna (GET, POST, itd.)Dowolna
Wykonywane przez Googlebot/WRSNie (potwierdzone testem)TakTak (potwierdzone testem)

To właśnie ta ostatnia różnica w tabeli – pozornie drobny szczegół implementacyjny – stała się punktem wyjścia dla tego artykułu.

Wnioski

  • Googlebot i Web Rendering Service konsekwentnie ignorują navigator.sendBeacon(). Potwierdziłem to, 20 zarejestrowanych żądań w testach to wywołania fetch() i okrąglutkie ZERO wywołań sendBeacon(), mimo że obydwa znajdowały się w tym samym wykonywanym JavaScript
  • To zjawisko nie jest jednak przyczyną błędów 4xx w Google Search Console. Błędy generuje niezależny mechanizm – crawler Google w ramach procesu odkrywania adresów URL, który wykrywa ciągi znaków przypominające adresy URL bezpośrednio w kodzie źródłowym strony, bez wykonywania JavaScriptu i odpytuje je standardowym żądaniem GET
  • Rozwiązanie problemu w GSC jest proste. Wystarczy zablokować odpowiednią ścieżkę API w robots.txt, opcjonalnie wzmocnić to zmianą kodu odpowiedzi z 405 na 404

Metodologia ponad wynik!

Niezależnie od konkretnego problemu opisanego w tym artykule, to najważniejszym wnioskiem jest projektować odizolowane i powtarzalne testy z weryfikowalnymi dowodami. Ja swoje oparłem na logach dostarczonych ze skryptu oraz logów serwera.

  1. SEO-poligon – tak określam moje strony internetowe, na których testuję różne rzeczy ↩︎
  2. Weryfikowanie żądań od robotów i modułów pobierania Google – https://developers.google.com/crawling/docs/crawlers-fetchers/verify-google-requests ↩︎
  3. Sprawdzone metody dotyczące linków – https://developers.google.com/search/docs/crawling-indexing/links-crawlable ↩︎

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *