SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-07

HTTP là gì? Hướng dẫn cho sysadmin

Tìm hiểu HTTP từ góc nhìn sysadmin: method, status code, header, access log nginx, cache, TLS và điểm khác nhau khi dùng HTTP/3.

HTTP là gì?

HTTP (giao thức truyền siêu văn bản) là tập hợp các quy tắc mà client và web server dùng để yêu cầu và gửi dữ liệu. Client gửi một request gồm method như GET, path như /pricing, phiên bản giao thức, danh sách header và đôi khi có body. Server trả lời bằng status code như 200, sau đó là các header của server và thường là một body. Mỗi lần mở trang và mỗi lần gọi API (application programming interface) trên server của bạn đều là một lần trao đổi như vậy, được lặp lại.

HTTP tự nó không lưu trạng thái. Server không nhớ bạn đã yêu cầu gì một giây trước, vì vậy mọi thứ cần hoạt động như bộ nhớ, chẳng hạn login session, đều được truyền trong header của từng request. Đặc điểm này giải thích phần lớn nội dung bên dưới: cache hoàn toàn do header điều khiển, và load balancer có thể gửi request tiếp theo của bạn đến một backend khác mà không làm gián đoạn hoạt động.

Toàn bộ nội dung bên dưới mô tả mô hình đó từ phía server, trong access log và trong cấu hình nginx của bạn.

Một request và response thô, có chú thích

Đây là một request HTTP/1.1 hoàn chỉnh. Một dòng trống kết thúc phần header. Mọi thứ sau dòng đó là body. Một GET thường không có body.

GET /pricing HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
Accept-Encoding: gzip
  • GET là method, cho biết bạn muốn thực hiện thao tác nào. GET đọc dữ liệu, POST gửi dữ liệu, PUT thay thế dữ liệu, DELETE xóa dữ liệu, còn HEAD yêu cầu header của một GET nhưng không lấy body.
  • /pricing là path. Hostname không nằm trong request line, nên mới cần header tiếp theo.
  • HTTP/1.1 là phiên bản protocol mà client đang sử dụng.
  • Host: example.com cho biết site mà client muốn truy cập. HTTP/1.1 bắt buộc phải có header này, nên nginx trả về 400 Bad Request nếu request không có nó.
  • Các header còn lại thể hiện các tùy chọn. Accept-Encoding: gzip cho biết client có thể giải nén, nên server được phép nén body.

Response có cấu trúc tương tự, nhưng có status line ở trên cùng.

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 OK là status code kèm reason phrase. Code mới là phần quan trọng. Phrase chỉ mang tính mô tả và client sẽ bỏ qua.
  • Content-Type cho client biết cách xử lý các byte tiếp theo.
  • Content-Length là kích thước body tính bằng byte, để client biết body kết thúc ở đâu. Khi chưa biết trước kích thước, server sẽ gửi Transfer-Encoding: chunked và đánh dấu phần kết thúc bằng một chunk có độ dài bằng 0.
  • Cache-Control cho browser và các cache ở giữa biết chúng được phép lưu response này trong bao lâu.
  • Dòng trống sau phần header tách header khỏi body trong cả hai chiều.

Tên header không phân biệt chữ hoa chữ thường. Mỗi dòng kết thúc bằng carriage return rồi đến line feed, không phải chỉ bằng newline. Bạn sẽ không tự gõ các ký tự này, nhưng sẽ gặp chúng trong packet capture.

Để quan sát một cặp request và response thực tế, hãy chạy lệnh này với một site do bạn sở hữu:

curl -sS -o /dev/null -D - https://example.com/

-D - ghi các response header vào terminal và -o /dev/null loại bỏ body. Nên dùng cách này thay cho curl -I, vì -I gửi một request HEAD. Một application server xử lý HEAD khác với GET, và nhiều server thực sự làm như vậy, sẽ hiển thị cho bạn các header mà browser không bao giờ nhận được. curl -v in cả hai phía, trong đó request line được đánh dấu bằng > và response line được đánh dấu bằng <.

Dòng request trong access log của nginx trông như thế nào

nginx cung cấp một định dạng log combined, được định nghĩa như sau:

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

Một dòng được tạo bởi định dạng này:

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.45$remote_addr, địa chỉ đã mở kết nối TCP (transmission control protocol). Nếu đi qua proxy, đây là địa chỉ của proxy, không phải của người truy cập.
  • - đầu tiên là placeholder cố định. - thứ hai là $remote_user, chỉ được điền khi sử dụng xác thực HTTP basic.
  • "GET /pricing HTTP/1.1"$request, dòng request được sao chép chính xác như khi nhận được.
  • 200 là mã trạng thái server trả về, không phải mã trạng thái mà người truy cập nhìn thấy.
  • 5310$body_bytes_sent, chỉ tính phần body. Response header không được tính, nên con số này luôn nhỏ hơn số byte thực tế đã gửi.
  • Hai trường được đặt trong dấu ngoặc kép ở cuối là RefererUser-Agent. Cả hai đều đến từ client, nên đều có thể chứa bất kỳ dữ liệu nào.

$request được sao chép nguyên văn, dữ liệu rác cũng xuất hiện nguyên văn. Một client nói TLS (transport layer security) với cổng plaintext 80 sẽ tạo ra một dòng 400 có trường request bắt đầu bằng các byte đã escape như "\x16\x03\x01\x02\x00\x01". \x16 là kiểu bản ghi handshake TLS, nên các byte đó là phần đầu của ClientHello chứ hoàn toàn không phải dòng request. Server của bạn đang hoạt động đúng. Có thành phần đang gửi HTTPS đến một cổng HTTP.

Thêm $server_protocol vào định dạng log. Nó in ra HTTP/1.1, HTTP/2.0 hoặc HTTP/3.0, và đây là cách nhanh nhất để xác nhận thay đổi protocol đã thực sự có hiệu lực.

Ý nghĩa của các mã trạng thái thường gặp khi chính website của bạn trả về chúng

Chữ số đầu tiên cho biết nhóm mã. Đây là phần bạn nên đọc trước.

2xx nghĩa là yêu cầu đã thành công. 200 OK dùng cho thao tác đọc thông thường. 201 Created dùng sau một POST đã tạo ra một đối tượng. 204 No Content dùng khi thao tác thành công nhưng không có dữ liệu nào cần gửi lại. Đây là phản hồi thường dùng cho DELETE.

3xx nghĩa là hãy tìm ở nơi khác. 301 là chuyển hướng vĩnh viễn. Trình duyệt cache mã này rất lâu, đôi khi đến khi người dùng xóa profile, nên một 301 trỏ đến sai hostname sẽ rất khó hoàn tác. Dùng 302 trong khi vẫn đang kiểm thử chuyển hướng. 304 Not Modified là thành công, không phải lỗi: client gửi If-None-Match chứa một ETag (entity tag) mà bạn vẫn nhận ra, nên bạn trả về các header nhưng không có body. Log có nhiều 304 nghĩa là cơ chế cache đang hoạt động.

4xx nghĩa là request bị sai. 400 Bad Request là dữ liệu đầu vào không đúng định dạng. 401 Unauthorized thực sự có nghĩa là chưa xác thực, và phải kèm theo header WWW-Authenticate chỉ rõ scheme. 403 Forbidden nghĩa là request đã được hiểu nhưng vẫn bị từ chối. 404 Not Found là đường dẫn không tồn tại. 405 Method Not Allowed là đường dẫn đúng nhưng method không phù hợp. Đây là phản hồi khi gửi một POST đến vị trí file tĩnh. 413 nghĩa là body lớn hơn client_max_body_size của nginx. Giá trị mặc định là 1 megabyte. Error log xác nhận điều này bằng client intended to send too large body.

403 trên một file tĩnh gần như luôn là vấn đề của filesystem, không phải quy tắc HTTP. Đọc /var/log/nginx/error.log trước khi thay đổi config. open() "/srv/site/index.html" failed (13: Permission denied) nghĩa là user chạy nginx worker không thể đọc file. Nguyên nhân thường gặp nhất là một thư mục cha không có quyền execute cho các user khác. directory index of "/srv/site/" is forbidden nghĩa là đường dẫn được phân giải thành một thư mục không có file index trong khi autoindex đang tắt.

5xx nghĩa là phía bạn gặp lỗi. 500 là lỗi chưa được xử lý trong application của bạn. 502 Bad Gateway nghĩa là nginx không lấy được phản hồi hợp lệ từ upstream. Error log sẽ nêu nguyên nhân: connect() failed (111: Connection refused) while connecting to upstream nghĩa là không có tiến trình nào đang listen trên địa chỉ trong proxy_pass. 504 Gateway Timeout nghĩa là upstream đã chấp nhận connection nhưng không gửi phản hồi trong thời gian proxy_read_timeout, mặc định là 60 giây. Log ghi nhận tình huống này bằng upstream timed out (110: Connection timed out) while reading response header from upstream. 503 Service Unavailable là từ chối có chủ ý. Lưu ý rằng rate limiter của nginx trả về 503, vì limit_req_status mặc định là 503. Nếu bạn tìm 429 Too Many Requests trong log nhưng lại thấy 503, đó là lý do. Đặt limit_req_status 429; để nhận đúng mã.

Các header quan trọng khi chạy server

Host chọn site. Một địa chỉ IP có thể phục vụ hàng trăm hostname, và nginx đối chiếu Host với server_name để quyết định block server nào sẽ xử lý. Nếu không có hostname nào khớp, nginx dùng server mặc định. Đây là block đầu tiên lắng nghe trên địa chỉ và cổng đó, trừ khi block khác được đánh dấu default_server. Nếu virtual host mới trả về nhầm site, gần như luôn là do tên không khớp, nên request rơi xuống server mặc định. Bạn có thể kiểm tra mà không cần thay đổi DNS:

curl -sS -o /dev/null -D - -H 'Host: app.example.com' http://127.0.0.1/

User-Agent là phần tự mô tả do client gửi và có thể chứa văn bản tùy ý. Chỉ dùng nó như một dấu hiệu khi đọc log. Không dùng nó làm cơ chế kiểm soát, vì client muốn giả mạo thì có thể làm vậy. Do đó, chặn scraper theo User-Agent chỉ lọc được những client không cố tình che giấu.

Content-Type quyết định cách diễn giải các byte: application/json cho API request, text/html; charset=utf-8 cho page. nginx ánh xạ phần mở rộng file sang các type bằng /etc/nginx/mime.types, còn nginx.conf được đóng gói thiết lập default_type application/octet-stream;. Vì vậy, file có phần mở rộng mà nginx không nhận biết sẽ được cung cấp dưới dạng file tải xuống thay vì được render. Biểu hiện thường thấy là page tải xong nhưng không có style, trong khi console của browser in ra Refused to apply style from ... because its MIME type ('text/plain') is not a supported stylesheet MIME type. MIME là viết tắt của multipurpose internet mail extensions, tên của scheme tạo ra các chuỗi type này.

Cache-Control dùng để kiểm soát mọi cache nằm giữa server và người đọc. public, max-age=31536000, immutable phù hợp với asset có content hash trong filename, vì tên file sẽ thay đổi khi content thay đổi. Dùng no-store cho mọi thứ chứa dữ liệu riêng của user, vì shared cache giữ lại page sau khi đăng nhập sẽ gửi page đó cho người tiếp theo yêu cầu cùng URL. private là thiết lập ở giữa: browser có thể giữ lại, nhưng shared cache thì không.

X-Forwarded-For cần thiết vì proxy che giấu visitor. Sau khi request đi qua reverse proxy, $remote_addr là địa chỉ của proxy, nên log, geolocation và rate limiting của bạn đều chỉ thấy một client. Proxy phải chuyển tiếp địa chỉ ban đầu:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

Sau đó, server nhận request phải được cấu hình để tin giá trị này, đồng thời phải chỉ rõ chính xác proxy nào được phép cung cấp nó:

set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;

Chỉ liệt kê các range do bạn kiểm soát. X-Forwarded-For là plain text mà bất kỳ client nào cũng có thể gửi, nên set_real_ip_from 0.0.0.0/0; cho phép visitor tự chọn địa chỉ được ghi vào log và địa chỉ được tính bởi rate limiter.

X-Forwarded-Proto ngăn một lỗi cụ thể và rất phổ biến. Proxy termination TLS rồi chuyển tiếp request đến application qua HTTP thường. Application thấy request thường, quyết định visitor phải dùng HTTPS và trả về 301 https://example.com/. Browser làm theo, proxy lại termination TLS rồi tiếp tục chuyển tiếp HTTP thường, và vòng lặp lặp lại đến khi browser từ bỏ với ERR_TOO_MANY_REDIRECTS. Gửi X-Forwarded-Proto: https cho application biết visitor đã dùng HTTPS, nên application dừng redirect.

HTTP/1.1 so với HTTP/2 và HTTP/3: bạn cần biết những thay đổi nào

HTTP/1.1 dùng văn bản và xử lý từng request một trên mỗi connection. Connection: keep-alive cho phép request tiếp theo dùng lại cùng TCP connection, giúp giảm chi phí thiết lập, nhưng response vẫn trả về theo đúng thứ tự request. Một response chậm sẽ chặn mọi response đang xếp hàng phía sau. Đây là hiện tượng head-of-line blocking, nên trình duyệt thường mở đồng thời nhiều connection đến cùng một hostname để tránh vấn đề này.

HTTP/2 giữ nguyên các method và status code, nhưng chuyển sang framing dạng binary. Nhiều request có thể dùng chung một connection dưới dạng các stream độc lập, đồng thời phần header lặp lại được nén. Điều này quan trọng vì request hiện đại thường chứa rất nhiều header. Connection vẫn dùng TCP, nên một packet bị mất sẽ làm mọi stream trên connection đó bị đình trệ cho đến khi packet được retransmit. Head-of-line blocking không biến mất. Nó chuyển từ HTTP xuống tầng transport. Server push từng là một phần của HTTP/2 nhưng thực tế không còn được dùng, vì Chrome đã gỡ hỗ trợ này vào năm 2022.

HTTP/3 tiếp tục giữ nguyên semantics và thay TCP bằng QUIC, một transport được xây dựng trên UDP (user datagram protocol). Các QUIC stream độc lập xuyên suốt xuống các tầng bên dưới, nên packet bị mất chỉ làm stream chứa packet đó bị đình trệ. TLS 1.3 được tích hợp vào QUIC handshake thay vì chạy bên trên, nên connection mới cần ít round trip hơn. Có 2 hệ quả thực tế: phải mở UDP port 443 trên mọi firewall nằm trên đường đi, và mọi network giới hạn hoặc chặn UDP sẽ khiến client quay lại HTTP/2.

Cụ thể, điều này thay đổi gì với bạn? Trình duyệt không bao giờ bắt đầu bằng HTTP/3. Chúng kết nối qua HTTP/2 hoặc HTTP/1.1, thấy header Alt-Svc: h3=":443"; ma=86400 trong response rồi dùng HTTP/3 cho các connection sau đến host đó. Vì vậy, header này không phải phần trang trí tùy chọn. Nó là cơ chế discovery. Trong nginx, HTTP/2 trở thành một directive riêng từ version 1.25.1 (http2 on; bên trong block server, thay cho parameter listen ... http2 cũ), còn QUIC xuất hiện trong mainline 1.25.0. Khi đó, một site HTTP/3 cần listen 443 quic reuseport; cùng với listen 443 ssl; thông thường.

Các proxy có mức độ hỗ trợ khác nhau, nên bạn cần kiểm tra theo version thực tế đang chạy. Tính đến tháng 8 năm 2026, Caddy phục vụ HTTP/3 mặc định mà không cần cấu hình. nginx cần listener quic rõ ràng cùng với header Alt-Svc được mô tả ở trên. Traefik bật HTTP/3 theo từng entry point thông qua option http3 rõ ràng. Nếu bạn terminate TLS tại Traefik phía trước nhiều ứng dụng Docker, version protocol mà visitor nhận được sẽ do Traefik quyết định. Hop từ proxy đến container của bạn thường vẫn là HTTP/1.1 thuần, bất kể browser đã negotiate protocol nào.

Hãy kiểm tra thay vì giả định. curl --http3 -sS -o /dev/null -D - https://example.com/ chỉ hoạt động nếu curl -V liệt kê HTTP3 trong các feature của nó, nhưng phần lớn bản build của các distribution không tích hợp feature này. Cách kiểm tra đáng tin cậy là xem log của chính bạn: thêm $server_protocol vào format rồi đọc protocol mà các browser thực tế negotiate. Trước đó, hãy xác nhận UDP 443 thực sự được mở, vì firewall chỉ cho phép TCP 443 sẽ khiến HTTP/3 fail âm thầm trong khi site vẫn hoạt động qua HTTP/2. Biết những port nào đang mở và listening trên Linux server của bạn là việc đầu tiên cần kiểm tra.

HTTPS: HTTP là giao thức, TLS là lớp bao

HTTPS không phải là một giao thức riêng. Đây là các request và status code giống HTTP, được truyền bên trong một phiên TLS. Cổng 80 truyền chúng ở dạng không mã hóa, còn cổng 443 truyền chúng đã được mã hóa. TLS handshake hoàn tất trước, sau đó HTTP request mới đi bên trong kênh đã mã hóa. Vì vậy, lỗi certificate không bao giờ đi kèm status code: lỗi xảy ra trước khi một byte HTTP được gửi, nên không có response để đánh số.

Một chi tiết về thứ tự xử lý rất quan trọng trên server host nhiều site. Certificate được chọn bằng SNI (server name indication), một trường trong TLS handshake chứa hostname ở dạng không mã hóa trước khi có bất kỳ HTTP header nào. Vì vậy, server chọn certificate từ SNI trước, rồi chọn virtual host từ header Host sau. Đây là hai lần tra cứu riêng biệt, nhưng thông thường sẽ cho cùng một kết quả. Khi chúng không khớp, browser hiển thị lỗi không khớp tên như NET::ERR_CERT_COMMON_NAME_INVALID và không gửi request nào, vì certificate của default server đã được cung cấp cho một tên mà nó không bao phủ.

Với site public, hãy lấy certificate hợp lệ và để hệ thống tự gia hạn. Certbot với Let's Encrypt trên nginx ghi các đường dẫn certificate vào server block và cài renewal timer giúp bạn. Với hostname mà không có public authority nào có thể xác thực, chẳng hạn tên nội bộ hoặc địa chỉ IP trống trên mạng của bạn, certificate tự ký trên Ubuntu là lựa chọn phù hợp, miễn là bạn chấp nhận việc phải cấu hình mọi client để trust certificate đó.

Khi TLS đã hoạt động, chuyển toàn bộ lưu lượng trên cổng 80 sang cổng 443:

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Chỉ thêm Strict-Transport-Security khi bạn chắc chắn. Header add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always; yêu cầu browser từ chối HTTP không mã hóa cho hostname đó trong hai năm. Browser thực hiện việc này từ cache của chính nó, nên xóa header sau đó cũng không hoàn tác được. Hãy bắt đầu với max-age trong vài giờ, xác nhận mọi subdomain thực sự dùng HTTPS, rồi mới tăng thời hạn.

FAQ

HTTP và HTTPS khác nhau như thế nào?

HTTPS là HTTP được truyền bên trong một phiên TLS (transport layer security). Các method và status code giống hệt nhau. Điểm khác là các byte được mã hóa giữa client và thành phần thực hiện TLS, đồng thời cổng mặc định chuyển từ 80 sang 443. Vì quá trình bắt tay TLS hoàn tất trước khi byte HTTP đầu tiên được gửi, lỗi certificate không bao giờ tạo ra status code. Vì vậy, cảnh báo certificate trên trình duyệt hiển thị tên lỗi như NET::ERR_CERT_COMMON_NAME_INVALID thay vì một con số như 403.

Vì sao site của tôi trả về 502 Bad Gateway?

502 từ nginx có nghĩa là nginx không nhận được phản hồi có thể sử dụng từ upstream mà nó proxy đến. Request của visitor không có vấn đề; lỗi nằm ở thành phần phía sau nginx. Đọc /var/log/nginx/error.log. connect() failed (111: Connection refused) while connecting to upstream có nghĩa là không có tiến trình nào listening trên địa chỉ và cổng trong proxy_pass. Hãy kiểm tra application đang chạy và bind đúng địa chỉ bạn mong đợi. no live upstreams while connecting to upstream có nghĩa là mọi server trong upstream block đã bị đánh dấu là down sau nhiều lần thất bại. So sánh với 504 Gateway Timeout. Lỗi này có nghĩa là upstream đã nhận connection nhưng không trả lời trong thời gian proxy_read_timeout.

Vì sao access log hiển thị cùng một địa chỉ IP cho mọi visitor?

$remote_addr ghi lại địa chỉ đã mở TCP connection. Khi đứng sau reverse proxy hoặc content delivery network, địa chỉ đó là địa chỉ của proxy. Địa chỉ của visitor được gửi trong header X-Forwarded-For. Hãy cấu hình proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; trên proxy. Sau đó, trên nginx nhận request, đặt set_real_ip_from thành dải địa chỉ của proxy và cấu hình real_ip_header X-Forwarded-For;. Chỉ liệt kê các dải địa chỉ bạn kiểm soát. Header đó là chuỗi văn bản mà bất kỳ client nào cũng có thể gửi. Nếu tin header này từ toàn bộ Internet, visitor có thể chọn địa chỉ được ghi log và địa chỉ được dùng để rate limit.

Tôi có cần bật HTTP/2 hoặc HTTP/3 không?

Nên bật HTTP/2. Đây chỉ là một directive trên site đã có TLS và nó loại bỏ giới hạn request theo từng connection, vốn khiến trang có nhiều file nhỏ tải chậm. HTTP/3 mang lại lợi ích nhỏ hơn và kém chắc chắn hơn. Nó yêu cầu mở UDP port 443 và proxy build có hỗ trợ QUIC. Lưu ý rằng browser chỉ chuyển sang HTTP/3 sau khi nhận thấy một header Alt-Svc trong response trước đó. Vì vậy, nếu không có header này thì không có gì thay đổi, dù dòng listen của bạn đặt thế nào. Thêm $server_protocol vào log format và đo protocol mà visitor thực sự negotiate trước khi dành thời gian cho HTTP/3.

403 Forbidden có nghĩa gì khi file vẫn tồn tại?

Trên static site, 403 thường là lỗi permission của filesystem chứ không phải rule HTTP. open() ... failed (13: Permission denied) trong /var/log/nginx/error.log có nghĩa là nginx worker user không thể đọc file. Nguyên nhân thường gặp nhất là một parent directory thiếu execute permission cho user khác, không phải do file mode bị sai. directory index of ... is forbidden có nghĩa là request trỏ đến một directory không có index file trong khi autoindex đang tắt. Một rule deny rõ ràng trong block location phù hợp cũng trả về 403. Vì vậy, hãy đọc block đó khi error log không hiển thị nguyên nhân.

#http#https#web-server#headers#http3