W dzisiejszym artykule chciałem pokazać wam jeden z wielu elementów tego co odróżnia realną aplikację działającą na produkcji od projektów pobocznych, często rozwijanych lokalnie lub posiadających znikomą liczbę użytkowników.
Przemek Smyrdek
2020-09-11
Zapewne wielokrotnie spotykałeś się już z sytuacją, kiedy szukając w sieci pożądanego przez ciebie zasobu, zamiast posta czy filmu w oknie przeglądarki pojawiało się coś takiego:
W zależności od miejsca, które przywitało cię taką oto stroną, inny mógł być interfejs użytkownika, kolory czy też układ poszczególnych elementów na stronie, ale na większości z nich mogłeś zobaczyć część wspólną. Kod błędu HTTP.
400 - Bad Request...
404 - Not Found...
502 - Bad Gateway...
OMG!
Wszystkim, co interesowało mnie na początku mojej przygody z web developmentem było to, czy dana funkcjonalność "działa", lub "nie działa". Jeśli implementowałem rozwiązanie w którego skład wchodził klient (np. aplikacja oparta o Angulara) oraz serwer (np. aplikacja oparta o ASP.NET MVC), to osiągnięciem celu było otrzymanie poprawnej odpowiedzi z serwera i wyświetlenie jej użytkownikowi. Wtedy "działało". A jeśli nie, to robiłem wszystko, aż "zadziała".
Utrzymanie takiego "systemu" było bardzo proste i przyjemne. Uruchamiałem komputer, stawiałem serwer developerski i pracowałem nad projektem. Jeśli dany fragment był skończony, to commitowałem zmiany i miałem gotowe demo, które teraz mogłem pokazać np. wykładowcy. Uptime mojej aplikacji? Równy uptime'owi mojego komputera. Zero użytkowników pozwalało mi "kłaść" aplikację w momencie kiedy szedłem spać.
W środowisku aplikacji produkcyjnych, z których korzystają realni użytkownicy, nie możemy sobie jednak pozwolić na takie uproszczenia.
Operowanie tak prostymi stwierdzeniami jak "działa" / "nie działa" nie daje wystarczającej informacji na temat stanu danego systemu w danym punkcie czasu. Śmiało zgaduję, że 99% developerów wypuszczających kod na produkcję przekonanych jest o tym, że "działa". Rzeczywistość pokazuje (najczęściej w godzinach, kiedy nie ma nas w biurze), że czasami jednak nie do końca...
Jak więc sprawić, aby nieprzewidziane sytuacje były wkalkulowane w proces utrzymania systemu, a problemy w okolicach drugiej w nocy nie spędzały snu z powiek? Jedną z pierwszych rzeczy o którą powinieneś zadbać jest zwracanie poprawnych kodów błędów i statusów odpowiedzi. To absolutne minimum do tego żeby zrozumieć co ukryło się pod stwierdzeniem "nie działa".
Tak jak w komunikacji pomiędzy dwiema osobami, tak w komunikacji pomiędzy klientem i serwerem może zdarzyć się naprawdę wszystko. Aby w trakcie całego procesu wymiany informacji zminimalizować liczbę potencjalnych błędów i wpadek, poszczególne jego etapy poddaje się standaryzacji.
Przyjęło się, że w sieci, w tzw. "warstwie aplikacji", jednym z najpopularniejszych standardów (protokołów) stał się protokół HTTP.
Jak wszystko, co ma na celu standaryzację wybranego procesu (w tym przypadku wymiany informacji pomiędzy klientem i serwerem), HTTP określony jest poprzez techniczną specyfikację. Porusza ona rozmaite elementy tego czym ten HTTP tak naprawdę jest - możemy dzięki niej dowiedzieć się czym jest URI, jakie metody możemy wykonywać w jego obrębie, albo w jaki sposób powinien działać mechanizm cache'owania.
W tej samej specyfikacji znajdziemy też rozdział poświęcony tematyce statusów odpowiedzi, czyli ustandaryzowanego formatu na określanie tego, co stało się z danym żądaniem wysłanym na serwer. Innymi słowy - naszego koła ratunkowego do zrozumienia sytuacji, w trakcie których nie było nas akurat przy biurku.
Po pierwsze, format - wg standardu HTTP statusy odpowiedzi to zawsze trzy cyfry.
The status-code element is a three-digit integer code giving the result of the attempt to understand and satisfy the request.
Po drugie - klient nie jest zmuszany do poprawnej obsługi każdego występującego w przyrodzie kodu i statusu odpowiedzi, ale powinien być w stanie poprawnie zareagować na zaistniałą sytuację na podstawie pierwszej cyfry takiego kodu. Pierwsza cyfra statusu określa bowiem klasę odpowiedzi.
Klas mamy pięć, a prezentują się one następująco:
Pierwsza cyfra to bardzo wysokopoziomowy (ale często wystarczający) obraz tego co stało się w momencie wysyłania żądania od klienta do serwera. Pozostałe dwie cyfry to mapowanie liczbowe na konkretne przypadki czy sytuacje, które mogą wystąpić w danej klasie odpowiedzi.
Nie będę teraz skupiał się na wymienianiu długiej listy kodów oraz opisywaniu ich znaczenia (bo najpopularniejsze znajdziecie tutaj), ale w zamian za to pokażę wam jak z kodów można "korzystać w praktyce oraz jak wykorzystuje się je w realnych sytuacjach awaryjnych.
Standardowe znaczenie 404 to "Not Found" - klient odwołuje się do zasobu którego nie znaleziono. Zazwyczaj kojarzy się nam to np. z hostingiem z którego ktoś usunął stronę internetową, ale statusu tego można używać w wielu innych sytuacjach.
Wyobraźmy sobie, że w warstwie back-endu przeprowadzana jest migracja z jednego endpointu na drugi. Zasób, który był dostępny pod adresem /api/users/{id}
, jest teraz dostępny pod adresem /api/profiles/{id}
. Aplikacja back-endowa zostaje zaktualizowana - stary endpoint znika a zamiast niego pojawia się nowy. W tym czasie podobne zmiany nanoszone są w aplikacji klienckiej. Obie aplikacje lądują na produkcji, a monitoring staje się czerwony od błędów o statusie 404. Jak to - przecież zaktualizowaliśmy klienta?
Okazuje się, że pominięto jedną istotną cechę aplikacji klienckich, które są de facto dokumentami które przeglądarka może cache'ować. Część użytkowników faktycznie wchodzi na nową wersję aplikacji a ich przeglądarka wysyła zapytania pod nowy endpoint, jednak część korzysta z wersji zapisanej lokalnie, gdzie wciąż widnieje poprzedni adres do zasobu (którego na back-endzie już nie ma). Poprawny kod błędu to doskonała podpowiedź związana z błędnie przeprowadzoną migracją.
404 to status którego powinniśmy używać kiedy klient odnosi się do zasobu którego nie ma obecnie, ale też nigdy nie było i prawdopodobnie nie będzie pod żądanym adresem. Co jednak w momencie, kiedy zasób przez pewien czas istniał, a w rezultacie pewnych decyzji lub zmian stał się niedostępny? Tutaj z pomocą przychodzi status 410 (Gone), który lepiej oddaje zaistniałą sytuację.
Wyobraźmy sobie, że użytkownik dodaje do zakładek stronę aukcji produktu, którym jest wstępnie zainteresowany, a następnie wyjeżdża na wakacje. Czas mija, produktu nie udaje się sprzedać, a wystawiający ogłoszenie decyduje się na wycofanie aukcji. Po kilku dniach potencjalny klient wraca z wakacji, wchodzi na stronę wg adresu z zakładek i widzi, że aukcja została wycofana.
Zauważmy, że jest to całkowicie inna sytuacja od takiej, kiedy użytkownik dodaje do zakładek adres który nigdy nie istniał w danym serwisie. Nie ma więc powodu sugerować użytkownikowi, że popełnił błąd i zapisał błędny adres strony. Adres był dobry, ale z powodu decyzji osoby trzeciej powiązany z nim zasób jest już niedostępny.
Sytuacja opisana powyżej to znakomita okazja do tego, żeby serwer odpowiadający na dane żądanie odpowiadał kodem 410 zamiast 404. W niektórych przypadkach będzie je można całkowicie zignorować, ponieważ to poprawna odpowiedź związana z logiką danego produktu (coś zupełnie innego niż nagła fala błędów 404, który może wskazywać np. na błędnie zaindeksowane w wyszukiwarce zasoby).
429 to status odpowiedzi którego najczęściej używają wszelkiej maści narzędzia i mechanizmy zabezpieczające twoją aplikację przed użyciem jej ponad przewidywane normy.
Przykładowo, jeśli na podstawie analizy stwierdzono, że rozwijany system wytrzyma w obecnym stanie 10 zapytań na sekundę, a ktoś na skutek błędu w integracji z nim zacznie generować większy ruch niż ten określony jako górna granica, to w pewnym momencie zamiast poprawnych odpowiedzi u klienta mogą się pojawić statusy 429. Jest to znak, że główną przyczyną problemów w otrzymaniu poprawnej odpowiedzi jest używanie danego serwisu zbyt intensywnie. Zespół odpowiedzialny za daną integrację nie musi teraz w pocie czoła szukać błędu w kodzie po swojej stronie, bo zamiast tego wystarczy... zwolnić i wysyłać nieco mniej zapytań.
Kod ten wykorzystuje chociażby Cloudflare - popularny dostawca rozwiązań typu CDN - który poświęca mechanizmowi rate-limitingu osobną sekcję na stronach wsparcia i m.in. opisuje tam ten właśnie kod.
Produkcyjne błędy z kategorii "5xx" to koszmar każdego, kto utrzymuje rozwiązania serwerowe.
O ile komunikaty z klasy "4xx" wskazują raczej na błąd po stronie klienta, to te o wartości (wait for it) 500+ wskazują jednoznacznie na błąd w rozwijanej przez ciebie aplikacji.
Błędem tym może być cokolwiek. Nieobsłużony null. Odwoływanie się do nieistniejącego indeksu w tablicy. Niepoprawne rzutowanie jednego typu na drugi...
Użytkownik chciałby, ale przez błąd w kodzie nie może.
W idealnym świecie warto byłoby postarać się o to, żeby aplikacja była wolna od błędów z tej kategorii. Znaczyłoby to, że wszystkie ścieżki związane z logiką biznesową działają poprawnie, a ew. błędy wskazują na użycie danego modułu przez konsumentów. Praktyka pokazuje jednak, że niejedno miejsce uważane za "state of the art" nie jest wolne od tego typu przypadłości, a nam pozostaje skupić się na monitoringu i systematycznej pracy poświęconej na obserwację zachowania naszego systemu (szczególnie, jeśli już dawno nie przypomina on dwóch stron które planowałeś na początku).
W ramach ciekawostki dodam, że wielu producentów oprogramowania decyduje się wprowadzać nieoficjalne statusy których nie znajdziecie w specyfikacji HTTP, ale o tym wszystkim opowie wam już Wikipedia.
W dzisiejszym artykule chciałem pokazać wam jeden z wielu elementów tego co odróżnia realną aplikację działającą na produkcji od projektów pobocznych, często rozwijanych lokalnie lub posiadających znikomą liczbę użytkowników.
Statusy odpowiedzi HTTP to w początkowych etapach przygody z Web Developmentem bardziej ciekawostka (sprowadzająca się do różnicy pomiędzy kodem 400 a 500) niż realne narzędzie, ale wraz z tym jak rośnie stawka, a my inwestujemy coraz więcej w infrastrukturę oraz reagowanie na sytuacje awaryjne, to fundament do skutecznego reagowania.
Oczywiście nie zmuszam was od teraz do tego, aby za każdym rzucanym wyjątkiem otwierać specyfikację HTTP i szukać najlepiej oddającego daną sytuację kodu (chociaż czemu nie?), ale proponuję przynajmniej wyłączenie autopilota pt. "400 - Bad Request".
Skorzystamy na tym wszyscy - my, programiści, i my, użytkownicy.