Jak działa protokół HTTP: przewodnik dla administratora
Zrozumienie działania protokołu HTTP w kontekście administracji serwerami. Analiza metod, kodów stanu, nagłówków oraz konfiguracji nginx i logów dla wersji HTTP/1.1 do HTTP/3.
Czym jest HTTP?
HTTP (hypertext transfer protocol) to zestaw reguł, których używa klient oraz serwer WWW do przesyłania zapytań i odpowiedzi. Klient wysyła żądanie: metodę taką jak GET, ścieżkę taką jak /pricing, wersję protokołu, listę nagłówków oraz opcjonalnie treść. Serwer odpowiada kodem stanu, takim jak 200, po którym następują jego własne nagłówki oraz zazwyczaj treść. Każde wyświetlenie strony i każde wywołanie API (application programming interface) na serwerze to pojedyncza, powtarzana wymiana danych.
Protokół HTTP jest bezstanowy. Serwer nie przechowuje informacji o poprzednich zapytaniach, dlatego każdy mechanizm wymagający pamięci, na przykład sesja logowania, jest przenoszony w nagłówku każdego pojedynczego żądania. Ta właściwość wyjaśnia większość dalszych zagadnień: buforowanie jest w całości sterowane nagłówkami, a load balancer może przekierować kolejne żądanie do innego backendu bez przerywania działania usługi.
Poniżej przedstawiono, jak ten model wygląda z perspektywy serwera, w logach dostępu oraz w konfiguracji nginx.
Surowe żądanie i odpowiedź z adnotacjami
Poniżej znajduje się kompletne żądanie HTTP/1.1. Pusta linia kończy nagłówki, a wszystko, co znajduje się za nią, stanowi treść (body). Żądanie GET zazwyczaj nie posiada treści.
GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzipGETto metoda, która określa oczekiwaną operację.GETsłuży do odczytu,POSTdo wysyłania danych,PUTdo zastępowania zasobów,DELETEdo usuwania, aHEADprosi o nagłówki żądaniaGETbez przesyłania treści./pricingto ścieżka. Nazwa hosta nie jest częścią linii żądania, dlatego wymagany jest kolejny nagłówek.HTTP/1.1to wersja protokołu używana przez klienta.Host: example.comwskazuje witrynę, z którą klient chce się połączyć. Protokół HTTP/1.1 wymaga tego nagłówka, dlatego nginx odpowiada na żądanie bez niego błędem400 Bad Request.- Pozostałe linie to preferencje.
Accept-Encoding: gzipinformuje, że klient obsługuje dekompresję, więc serwer może skompresować treść odpowiedzi.
Odpowiedź ma taką samą strukturę, z linią statusu na początku.
HTTP/1.1 200 OK
Date: Thu, 06 Aug 2026 09:12:44 GMT
Server: nginx
Content-Type: text/html; charset=utf-8
Content-Length: 5310
Cache-Control: public, max-age=300
<!doctype html>...200 OKto kod statusu wraz z jego opisem. Istotny jest kod; opis ma charakter informacyjny i jest ignorowany przez klientów.Content-Typeinformuje klienta, jak interpretować przesyłane bajty.Content-Lengthokreśla rozmiar treści w bajtach, dzięki czemu klient wie, gdzie kończy się odpowiedź. Gdy rozmiar nie jest znany z góry, serwer wysyłaTransfer-Encoding: chunkedi oznacza koniec za pomocą fragmentu o zerowej długości.Cache-Controlinformuje przeglądarkę oraz pośredniczące pamięci podręczne, jak długo mogą przechowywać tę odpowiedź.- Pusta linia po nagłówkach oddziela je od treści w obu kierunkach.
Nazwy nagłówków nie są wrażliwe na wielkość liter, a każda linia kończy się znakiem powrotu karetki (carriage return) oraz znakiem wysuwu linii (line feed), a nie samym znakiem nowej linii. Nie wpisuje się ich ręcznie, ale można je napotkać podczas analizy przechwyconych pakietów.
Aby monitorować rzeczywistą parę żądanie-odpowiedź, należy wykonać poniższe polecenie dla własnej witryny:
curl -sS -o /dev/null -D - https://example.com/-D - zapisuje nagłówki odpowiedzi w terminalu, a -o /dev/null odrzuca treść. Należy preferować to rozwiązanie zamiast curl -I, ponieważ -I wysyła żądanie HEAD. Serwer aplikacji, który obsługuje HEAD inaczej niż GET (a wiele tak robi), wyświetli wówczas nagłówki, których żadna przeglądarka nigdy nie otrzyma. curl -v wyświetla obie strony komunikacji, przy czym linie żądania są oznaczone jako >, a linie odpowiedzi jako <.
Jak wygląda linia żądania w logu dostępu nginx
Nginx dostarcza format logowania combined, którego definicja wygląda następująco:
log_format combined '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';Pojedyncza linia wygenerowana przez ten format:
203.0.113.45 - - [06/Aug/2026:09:12:44 +0000] "GET /pricing HTTP/1.1" 200 5310 "https://example.com/" "Mozilla/5.0 (X11; Linux x86_64) Chrome/127.0.0.0 Safari/537.36"203.0.113.45to$remote_addr, adres, który nawiązał połączenie TCP (transmission control protocol). Za proxy jest to adres proxy, a nie użytkownika końcowego.- Pierwszy
-to stały symbol zastępczy. Drugi to$remote_user, który jest wypełniany tylko wtedy, gdy używane jest uwierzytelnianie HTTP basic authentication. "GET /pricing HTTP/1.1"to$request, czyli linia żądania skopiowana dokładnie w takiej formie, w jakiej dotarła do serwera.200to status zwrócony przez serwer, a nie status, który otrzymał użytkownik.5310to$body_bytes_sent, czyli sama treść odpowiedzi. Nagłówki odpowiedzi nie są wliczane, dlatego ta liczba jest zawsze mniejsza niż rzeczywista liczba wysłanych bajtów.- Ostatnie dwa pola w cudzysłowie to
RefererorazUser-Agent. Oba pochodzą od klienta, więc mogą zawierać dowolne dane.
Ponieważ $request jest kopiowane dosłownie, wszelkie śmieci również pojawiają się w logu w formie surowej. Klient, który próbuje nawiązać połączenie TLS (transport layer security) z portem 80 obsługującym zwykły tekst, pozostawia linię 400, której pole żądania zaczyna się od bajtów w formacie escape, takich jak "\x16\x03\x01\x02\x00\x01". \x16 to typ rekordu handshake TLS, więc te bajty stanowią początek ClientHello, a nie linię żądania. Serwer zachowuje się poprawnie. Przyczyną jest skierowanie ruchu HTTPS na port HTTP.
Dodaj $server_protocol do swojego formatu logowania. Wyświetla ono HTTP/1.1, HTTP/2.0 lub HTTP/3.0 i jest to najszybszy sposób na potwierdzenie, że zmiana protokołu faktycznie weszła w życie.
Co oznaczają typowe kody stanu, gdy własna witryna je zwraca
Pierwsza cyfra określa klasę błędu i to od niej należy zacząć analizę.
2xx oznacza powodzenie. 200 OK przy standardowym odczycie. 201 Created po żądaniu POST, które utworzyło zasób. 204 No Content przy sukcesie bez treści do odesłania, co jest typową odpowiedzią na DELETE.
3xx oznacza przekierowanie. 301 jest trwałe, a przeglądarki buforują je bardzo agresywnie, czasem aż do wyczyszczenia profilu użytkownika, dlatego 301 wskazujące na błędną nazwę hosta jest trudne do naprawienia. Podczas testowania przekierowań należy używać 302. 304 Not Modified to sukces, a nie błąd: klient wysłał If-None-Match z nagłówkiem ETag (entity tag), który serwer nadal rozpoznaje, więc odpowiedziano samymi nagłówkami bez treści. Logi pełne 304 oznaczają, że buforowanie działa poprawnie.
4xx oznacza błędne żądanie. 400 Bad Request to nieprawidłowe dane wejściowe. 401 Unauthorized oznacza brak uwierzytelnienia i musi zawierać nagłówek WWW-Authenticate wskazujący schemat. 403 Forbidden oznacza, że żądanie zostało zrozumiane, ale odrzucone. 404 Not Found to ścieżka, która nie istnieje. 405 Method Not Allowed to poprawna ścieżka z niewłaściwą metodą, co zwraca POST dla lokalizacji plików statycznych. 413 oznacza treść żądania większą niż client_max_body_size w nginx, domyślnie 1 megabajt; log błędów potwierdza to wpisem client intended to send too large body.
Kod 403 dla pliku statycznego niemal zawsze wynika z uprawnień systemu plików, a nie z reguł HTTP. Przed zmianą konfiguracji należy sprawdzić /var/log/nginx/error.log. open() "/srv/site/index.html" failed (13: Permission denied) oznacza, że użytkownik procesu roboczego nginx nie może odczytać pliku, najczęściej z powodu braku uprawnień wykonywania dla innych użytkowników na katalogu nadrzędnym. directory index of "/srv/site/" is forbidden oznacza, że ścieżka prowadzi do katalogu bez pliku indeksowego, gdy autoindex jest wyłączone.
5xx oznacza awarię po stronie serwera. 500 to nieobsłużony błąd w aplikacji. 502 Bad Gateway oznacza, że nginx nie otrzymał poprawnej odpowiedzi od upstreamu, a log błędów wskazuje przyczynę: connect() failed (111: Connection refused) while connecting to upstream oznacza, że nikt nie nasłuchuje na adresie z proxy_pass. 504 Gateway Timeout oznacza, że upstream zaakceptował połączenie, ale nie odpowiedział w ciągu proxy_read_timeout (domyślnie 60 sekund), co log rejestruje jako upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable to celowa odmowa połączenia. Należy pamiętać, że wbudowany ogranicznik szybkości (rate limiter) nginx zwraca 503, ponieważ limit_req_status domyślnie ustawiono na 503. Jeśli w logach poszukujesz 429 Too Many Requests, a znajdujesz 503, to właśnie dlatego. Ustaw limit_req_status 429;, aby uzyskać dokładny kod.
Nagłówki istotne podczas uruchamiania serwera
Host wybiera witrynę. Jeden adres IP może obsługiwać setki nazw hostów, a nginx dopasowuje Host do server_name, aby zdecydować, który blok server obsłuży żądanie. Jeśli nie ma dopasowania, nginx używa domyślnego serwera, czyli pierwszego bloku nasłuchującego na danym adresie i porcie, chyba że inny został oznaczony jako default_server. Otrzymanie niewłaściwej witryny po dodaniu nowego wirtualnego hosta prawie zawsze wynika z tego, że nazwa nie pasowała, więc żądanie zostało przekierowane do domyślnego bloku. Przetestuj to bez ingerencji w DNS:
curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/User-Agent to opis klienta dostarczany przez niego samego; jest to dowolny tekst. Traktuj go jako wskazówkę podczas analizy logów. Nigdy nie używaj go jako mechanizmu kontrolnego, ponieważ klient, który chce skłamać, po prostu to zrobi. Blokowanie scrapera przez User-Agent wyeliminuje jedynie te uczciwe.
Content-Type decyduje o tym, jak interpretowane są bajty: application/json dla żądania API, text/html; charset=utf-8 dla strony. nginx mapuje rozszerzenia plików na typy za pomocą /etc/nginx/mime.types, a dostarczony nginx.conf ustawia default_type application/octet-stream;. W rezultacie plik z rozszerzeniem, którego nginx nie rozpoznaje, jest oferowany jako plik do pobrania zamiast być wyrenderowanym. Widocznym objawem jest strona ładująca się bez stylów, podczas gdy konsola przeglądarki wyświetla Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME to skrót od Multipurpose Internet Mail Extensions, czyli schematu nazewnictwa, z którego pochodzą te ciągi typów.
Cache-Control pozwala kontrolować każdy cache pomiędzy serwerem a czytelnikiem. public, max-age=31536000, immutable jest odpowiednie dla zasobów, których nazwa pliku zawiera skrót zawartości, ponieważ nazwa zmienia się wraz z treścią. no-store należy stosować do wszystkiego, co jest specyficzne dla użytkownika, ponieważ współdzielony cache, który przechowuje stronę zalogowanego użytkownika, przekaże ją kolejnej osobie żądającej tego samego adresu URL. private to ustawienie pośrednie: przeglądarka może zachować zasób, ale współdzielony cache nie może.
X-Forwarded-For istnieje, ponieważ proxy ukrywa odwiedzającego. Gdy żądanie przechodzi przez reverse proxy, $remote_addr jest adresem proxy, więc logi, geolokalizacja i rate limiting widzą tylko jednego klienta. Proxy musi przekazać oryginalny adres dalej:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;Serwer odbierający musi otrzymać instrukcję, aby ufać tym danym oraz informację, komu dokładnie ufać:
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;Wymieniaj tylko zakresy, które kontrolujesz. X-Forwarded-For to zwykły tekst, który może wysłać każdy klient, więc set_real_ip_from 0.0.0.0/0; pozwala odwiedzającemu wybrać adres, który zapiszesz w logach i który zliczy Twój rate limiter.
X-Forwarded-Proto zapobiega konkretnej i bardzo częstej awarii. Proxy kończy połączenie TLS i przekazuje żądanie do aplikacji przez zwykłe HTTP. Aplikacja widzi zwykłe żądanie, uznaje, że odwiedzający powinien korzystać z HTTPS i odpowiada 301 https://example.com/. Przeglądarka podąża za przekierowaniem, proxy ponownie kończy TLS i znów przekazuje zwykłe HTTP, a pętla powtarza się, aż przeglądarka podda się z błędem ERR_TOO_MANY_REDIRECTS. Przesłanie X-Forwarded-Proto: https informuje aplikację, że odwiedzający już korzysta z HTTPS, więc przestaje ona wymuszać przekierowania.
HTTP/1.1 a HTTP/2 a HTTP/3: co to zmienia dla użytkownika
HTTP/1.1 to protokół tekstowy, który obsługuje jedno żądanie na raz w ramach jednego połączenia. Connection: keep-alive pozwala kolejnym żądaniom na ponowne wykorzystanie tego samego połączenia TCP, co oszczędza czas potrzebny na jego nawiązanie, jednak odpowiedzi nadal wracają w kolejności, w jakiej zostały wysłane. Jedna wolna odpowiedź blokuje wszystko, co znajduje się w kolejce za nią. Jest to zjawisko blokowania głowy kolejki (head-of-line blocking), które przeglądarki obchodzą poprzez otwieranie wielu połączeń do tej samej nazwy hosta jednocześnie.
HTTP/2 zachowuje te same metody i kody statusu, zmieniając jedynie formatowanie na binarne. Wiele żądań współdzieli jedno połączenie jako niezależne strumienie, a powtarzający się tekst nagłówków jest kompresowany, co ma znaczenie, ponieważ współczesne żądania zawierają ich bardzo dużo. Połączenie nadal opiera się na TCP, więc utrata jednego pakietu wstrzymuje wszystkie strumienie w tym połączeniu do czasu dotarcia retransmisji. Blokowanie głowy kolejki nie zniknęło; przeniosło się z warstwy HTTP do warstwy transportowej. Mechanizm server push był częścią HTTP/2, ale w praktyce przestał być używany, ponieważ Chrome usunął jego obsługę w 2022 roku.
HTTP/3 ponownie zachowuje tę samą semantykę, ale zastępuje TCP protokołem QUIC, czyli warstwą transportową zbudowaną na UDP (user datagram protocol). Strumienie QUIC są całkowicie niezależne, więc utracony pakiet wstrzymuje tylko ten strumień, do którego należał. TLS 1.3 jest wbudowany w proces nawiązywania połączenia QUIC, zamiast być nakładany jako kolejna warstwa, dzięki czemu nowe połączenie wymaga mniejszej liczby cykli wymiany danych (round trips). Wynikają z tego dwie praktyczne konsekwencje: port UDP 443 musi być otwarty w każdej zaporze sieciowej na drodze pakietów, a każda sieć, która ogranicza lub blokuje UDP, wymusi na klientach powrót do HTTP/2.
Co to zmienia w praktyce. Przeglądarki nigdy nie zaczynają połączenia od HTTP/3. Łączą się przez HTTP/2 lub HTTP/1.1, odczytują nagłówek Alt-Svc: h3=":443"; ma=86400 w odpowiedzi i używają HTTP/3 do kolejnych połączeń z danym hostem. Nagłówek ten nie jest więc opcjonalną dekoracją, lecz mechanizmem wykrywania. W nginx obsługa HTTP/2 stała się osobną dyrektywą w wersji 1.25.1 (http2 on; wewnątrz bloku server, zastępując stary parametr listen ... http2), a QUIC pojawił się w wersji mainline 1.25.0, gdzie witryna HTTP/3 wymaga dyrektywy listen 443 quic reuseport; obok zwykłego listen 443 ssl;.
Poziom dojrzałości proxy w tym zakresie jest różny, dlatego warto sprawdzić wersję, która jest aktualnie uruchomiona. Według stanu na sierpień 2026, Caddy obsługuje HTTP/3 domyślnie bez dodatkowej konfiguracji. nginx wymaga jawnego słuchacza quic oraz nagłówka Alt-Svc opisanego powyżej. Traefik włącza tę funkcję dla każdego punktu wejścia (entry point) poprzez jawną opcję http3. Jeśli terminacja TLS odbywa się w Traefik in front of several Docker apps, to tam zapada decyzja o wersji protokołu, a połączenie między proxy a kontenerem zazwyczaj odbywa się przez zwykłe HTTP/1.1, niezależnie od tego, co wynegocjowała przeglądarka.
Należy weryfikować, a nie zakładać. Polecenie curl --http3 -sS -o /dev/null -D - https://example.com/ działa tylko wtedy, gdy curl -V wymienia HTTP3 wśród swoich funkcji, a większość pakietów dystrybucyjnych tego nie zawiera. Pewną metodą sprawdzenia jest własny dziennik: należy dodać $server_protocol do formatu logowania i odczytać, co faktycznie negocjują przeglądarki. Przed wykonaniem tych kroków należy potwierdzić, czy port UDP 443 jest faktycznie otwarty, ponieważ zapora zezwalająca tylko na TCP 443 spowoduje cichą awarię HTTP/3, podczas gdy witryna będzie nadal działać przez HTTP/2. Wiedza o tym, which ports are open and listening on your Linux server, jest pierwszą rzeczą, którą należy sprawdzić.
HTTPS: HTTP to protokół, TLS to otoczka
HTTPS nie jest odrębnym protokołem. To te same żądania i te same kody statusu przesyłane wewnątrz sesji TLS. Port 80 przesyła je w postaci jawnej, a port 443 w postaci zaszyfrowanej. Uścisk dłoni TLS następuje jako pierwszy, a następnie żądanie HTTP przesyłane jest wewnątrz zaszyfrowanego kanału. Z tego powodu problem z certyfikatem nigdy nie posiada przypisanego kodu statusu: błąd występuje, zanim zostanie wysłany choćby jeden bajt HTTP, więc nie ma odpowiedzi, którą można by ponumerować.
W przypadku serwera obsługującego kilka witryn istotny jest jeden szczegół dotyczący kolejności. Certyfikat wybierany jest przy użyciu SNI (Server Name Indication), pola w uścisku dłoni TLS, które przesyła nazwę hosta w postaci jawnej, zanim powstanie jakikolwiek nagłówek HTTP. Serwer wybiera więc najpierw certyfikat na podstawie SNI, a następnie w drugiej kolejności wybiera wirtualny host na podstawie nagłówka Host. Są to dwa oddzielne wyszukiwania, które zazwyczaj są ze sobą zgodne. Gdy tak nie jest, przeglądarka wyświetla błąd niezgodności nazwy, taki jak NET::ERR_CERT_COMMON_NAME_INVALID, i w ogóle nie wysyła żądania, ponieważ dla nazwy, której certyfikat nie obejmuje, został zaoferowany certyfikat domyślnego serwera.
Dla publicznej witryny należy uzyskać właściwy certyfikat i pozwolić mu na automatyczne odnawianie. Certbot z Let's Encrypt na nginx zapisuje ścieżki do certyfikatów w bloku serwera i instaluje timer odnawiania. Dla nazwy hosta, której żaden publiczny urząd nie może zweryfikować, takiej jak nazwa wewnętrzna lub adres IP w sieci lokalnej, certyfikat z podpisem własnym na Ubuntu jest uczciwym rozwiązaniem, pod warunkiem zaakceptowania konieczności ręcznego zaufania certyfikatowi przez każdego klienta.
Gdy TLS działa, należy przekierować cały ruch z portu 80 na port 443:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Nagłówek Strict-Transport-Security należy dodać dopiero po uzyskaniu pewności. Nagłówek add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; nakazuje przeglądarkom odrzucanie zwykłego HTTP dla danej nazwy hosta przez dwa lata; przeglądarki przestrzegają tego polecenia z własnej pamięci podręcznej, co oznacza, że późniejsze usunięcie nagłówka nie cofa tego efektu. Należy zacząć od wartości max-age wynoszącej kilka godzin, potwierdzić, że każda subdomena faktycznie działa przez HTTPS, a następnie zwiększyć ten czas.
FAQ
Jaka jest różnica między HTTP a HTTPS?
HTTPS to HTTP przesyłane wewnątrz sesji TLS (transport layer security). Metody i kody statusu są identyczne. Zmienia się to, że bajty są szyfrowane między klientem a punktem terminacji TLS, a domyślny port zmienia się z 80 na 443. Ponieważ uzgadnianie TLS kończy się przed wysłaniem pierwszego bajtu HTTP, błąd certyfikatu nigdy nie generuje kodu statusu. Dlatego ostrzeżenie przeglądarki o certyfikacie wyświetla nazwę błędu, taką jak NET::ERR_CERT_COMMON_NAME_INVALID, zamiast numeru, np. 403.
Dlaczego moja strona zwraca 502 Bad Gateway?
Kod 502 z nginx oznacza, że nginx nie otrzymał poprawnej odpowiedzi od upstreamu, do którego przesyła ruch. Żądanie użytkownika było poprawne, ale problem wystąpił za serwerem nginx. Przeczytaj /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream oznacza, że nikt nie nasłuchuje na adresie i porcie wskazanym w proxy_pass, więc sprawdź, czy aplikacja działa i jest powiązana z oczekiwanym adresem. no live upstreams while connecting to upstream oznacza, że każdy serwer w bloku upstream został oznaczony jako niedostępny po wielokrotnych awariach. Porównaj to z 504 Gateway Timeout, co oznacza, że upstream zaakceptował połączenie, ale nie odpowiedział w czasie proxy_read_timeout.
Dlaczego mój log dostępu pokazuje ten sam adres IP dla każdego odwiedzającego?
Ponieważ $remote_addr rejestruje adres, który otworzył połączenie TCP, a za reverse proxy lub siecią CDN tym adresem jest serwer proxy. Adres odwiedzającego dociera w nagłówku X-Forwarded-For. Ustaw proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; na proxy, a następnie na odbierającym nginx ustaw set_real_ip_from na zakres adresów proxy oraz real_ip_header X-Forwarded-For;. Wymieniaj tylko zakresy, które kontrolujesz, ponieważ ten nagłówek to tekst, który może wysłać każdy klient. Zaufanie mu w przypadku całego Internetu pozwala odwiedzającemu wybrać adres, który logujesz i który podlega limitom (rate limiting).
Czy muszę włączać HTTP/2 lub HTTP/3?
HTTP/2 warto włączyć, ponieważ wymaga to jednej dyrektywy na stronie z TLS i usuwa limit żądań na połączenie, który spowalnia strony z wieloma małymi plikami. HTTP/3 daje mniejszy i mniej pewny zysk, a wymaga otwartego portu UDP 443 oraz kompilacji proxy z obsługą QUIC. Pamiętaj, że przeglądarki przełączają się na HTTP/3 dopiero po otrzymaniu nagłówka Alt-Svc w poprzedniej odpowiedzi, więc bez tego nagłówka nic się nie zmieni, niezależnie od tego, co zawiera linia listen. Dodaj $server_protocol do formatu logów i zmierz, co faktycznie negocjują odwiedzający, zanim poświęcisz na to czas.
Co oznacza 403 Forbidden, gdy plik istnieje?
Na statycznej stronie 403 zazwyczaj wynika z uprawnień systemu plików, a nie z reguł HTTP. open() ... failed (13: Permission denied) w /var/log/nginx/error.log oznacza, że użytkownik procesu roboczego nginx nie może odczytać pliku. Najczęściej wynika to z braku uprawnień wykonywania dla innych użytkowników w katalogu nadrzędnym, a nie z błędnych uprawnień samego pliku. directory index of ... is forbidden oznacza, że żądanie wskazało katalog bez pliku indeksowego, podczas gdy autoindex jest wyłączone. Jawna reguła deny w dopasowanym bloku location również zwraca 403, więc sprawdź ten blok, gdy log błędów milczy.