နားလည်ထားရမယ့် အချက်
HTTP/1.1 ဟာ TCP connection တစ်ခုပေါ်မှာ ပို့လိုက်တဲ့ စာသားတစ်ခုမျှသာ ဖြစ်တယ် ဆိုတာကို မြင်လိုက်တာနဲ့ အများကြီး ရှင်းသွားတယ်။ request တစ်ခုမှာ method, path နဲ့ version ပါတဲ့ request line တစ်ကြောင်း၊ 'Name: value' ပုံစံ header အကြောင်းရေ ဘယ်လောက်မဆို၊ ကွက်လပ်တစ်ကြောင်း၊ ပြီးမှ body လာတယ်။ ကြောင်းတိုင်းကို CR နဲ့ LF နှစ်လုံးနဲ့ ပိတ်ရပြီး၊ ကွက်လပ်တစ်ကြောင်း (CRLF CRLF) ကသာ 'header ပြီးပြီ' လို့ ပြောပြတဲ့ တစ်ခုတည်းသော အမှတ်အသား ဖြစ်တယ်။ body ဘယ်လောက် ရှည်လဲ ဆိုတာကို Content-Length (သို့) Transfer-Encoding: chunked ကပဲ ပြောပြရတယ် — ဒါမှမဟုတ်ရင် receiver ဟာ connection ပိတ်တဲ့အထိ ဘယ်တော့ပြီးမှန်း မသိဘူး။
status code ကို အလွတ်ကျက်စရာ မလိုဘဲ မိသားစုအလိုက် မှတ်ပါ — 2xx က အောင်မြင်တယ်၊ 3xx က တခြားနေရာမှာ ကြည့်ပါ၊ 4xx က ပို့လိုက်တဲ့ request မှားနေတယ် ဆိုတော့ အတူတူ ထပ်ပို့ရင်လည်း အတူတူပဲ ဖြစ်မယ်၊ 5xx က server ဘက်မှာ ပျက်နေတဲ့အတွက် ခဏနေမှ ထပ်ကြိုးစားတာ ဆီလျော်တယ်။ ဒီကွာခြားချက်က retry logic ကို ဆုံးဖြတ်ပေးတယ်။
အရေးအကြီးဆုံး ဒီဇိုင်း ဆုံးဖြတ်ချက်ကတော့ HTTP ဟာ stateless ဖြစ်တာ ဖြစ်တယ် — server ဟာ request တစ်ခုကို အဲဒီ request ထဲမှာ ပါလာတဲ့ အချက်အလက်တွေနဲ့ပဲ နားလည်ရမယ် လို့ သတ်မှတ်ထားတာ ဖြစ်တယ်။ ဒါက server တွေကို လွယ်လွယ်ကူကူ အများအပြား ချဲ့နိုင်စေတယ် — request ဘယ် server ဆီ ရောက်ရောက် အလုပ်လုပ်တယ်။ ဒါပေမယ့် login လိုမျိုး အရာတွေအတွက် တစ်ခုခု လိုအပ်လာလို့ cookie ကို ထည့်ခဲ့တာ ဖြစ်တယ် — server က Set-Cookie နဲ့ တန်ဖိုးတစ်ခု ပေးလိုက်ပြီး client က နောက် request တိုင်းမှာ ပြန်ပါလာစေတယ်။ state ဟာ connection ထဲမှာ မရှိဘဲ request တိုင်းမှာ ပါလာနေတာ ဖြစ်လို့ stateless သဘောသဘာဝ မပျက်ဘူး ဆိုတာ သတိထားပါ။ keep-alive က တခြားပြဿနာကို ဖြေရှင်းတယ် — request တစ်ခုစီအတွက် TCP connection အသစ် ဖွင့်နေရတာကို ရပ်ပြီး connection တစ်ခုကို ပြန်သုံးစေတယ်။ HTTP/1.1 မှာ connection တစ်ခုပေါ်မှာ request တွေက တစ်ခုပြီးမှ တစ်ခု စီတန်းရလို့ head-of-line blocking ဖြစ်နေဆဲ ဖြစ်တယ်။ HTTP/2 က message တွေကို binary frame တွေအဖြစ် ခွဲပြီး connection တစ်ခုတည်းပေါ်မှာ stream များစွာကို ရောပြီး ပို့နိုင်စေတာကြောင့် response တစ်ခု နှေးနေတာက ကျန်တာတွေကို မပိတ်ဆို့တော့ဘူး။
ANATOMY OF A RAW HTTP REQUEST AND RESPONSE
------------------------------------------
REQUEST RESPONSE
+---------------------------+ +---------------------------+
start line | POST /api/signup HTTP/1.1 | | HTTP/1.1 201 Created |
+---------------------------+ +---------------------------+
headers | Host: shop.example.test | | Content-Type: app/json |
| Content-Type: app/json | | Content-Length: 28 |
| Content-Length: 27 | | Set-Cookie: session=xyz |
| Cookie: session=abc123 | | Connection: keep-alive |
+---------------------------+ +---------------------------+
blank line | CRLF CRLF | | CRLF CRLF |
+---------------------------+ +---------------------------+
body | {"email":"ko@ex.test"} | | {"id":42,"status":"ok"} |
+---------------------------+ +---------------------------+လက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
checkout API ကို ခေါ်တဲ့အခါ user တစ်ချို့ဟာ order နှစ်ခါ တင်မိနေတယ် ဆိုတဲ့ bug တစ်ခုကို စစ်ကြည့်ရအောင်။ log ထဲမှာ မြင်ရတာက mobile client က POST /orders ကို ပို့ပြီး အဖြေ မရလို့ retry လုပ်၊ server ဘက်မှာတော့ ပထမ request က အောင်မြင်ခဲ့ပြီးသား ဖြစ်နေတာ ဖြစ်တယ်။ ဒီနေရာမှာ status code မိသားစုတွေက အဖြေပေးတယ်။ client ရဲ့ retry logic က 'အဖြေ မရရင် ပြန်ပို့' လို့ ရေးထားပြီး၊ ဒါက timeout ဖြစ်တဲ့အခါ request ရောက်မရောက် မသိဘဲ ပြန်ပို့နေတာ ဖြစ်တယ်။ 4xx ကို retry မလုပ်ဘူး၊ 5xx ကို retry လုပ်တယ် ဆိုတဲ့ စည်းမျဉ်းက မှန်ပေမယ့် timeout က status code မဟုတ်လို့ ဒီစည်းမျဉ်းအောက် မကျဘူး။
ဖြေရှင်းနည်းက request ကိုယ်တိုင်ကို idempotent ဖြစ်အောင် လုပ်ဖို့ ဖြစ်တယ် — client က order တစ်ခုစီအတွက် တစ်မူထူးခြားတဲ့ Idempotency-Key header တစ်ခု ထုတ်ပြီး ပါလာစေတယ်၊ server က ဒီ key ကို မြင်ဖူးပြီးသားဆိုရင် အလုပ်ကို ထပ်မလုပ်ဘဲ မူလ response ကို ပြန်ပေးလိုက်တယ်။ ဒါက stateless သဘောသဘာဝနဲ့ ကိုက်ညီတယ် — retry လုပ်တဲ့ request ဟာ သူ့ကိုယ်သူ ရှင်းပြနိုင်တဲ့ အချက်အလက်ကို ကိုယ်တိုင် သယ်လာတယ်။ ဒီလို bug မျိုးကို စစ်တဲ့အခါ browser ရဲ့ network tab က header တွေကို သန့်ရှင်းအောင် ပြင်ပြထားလို့ raw bytes ကို မမြင်ရဘူး ဆိုတာ သတိထားပါ — Content-Length မှားနေတာ (သို့) header နှစ်ခါ ပါနေတာမျိုးက အဲဒီမှာ ပျောက်နေတတ်ပြီး wire ပေါ်မှာမှ ပေါ်တတ်တယ်။
အတူတူ စမ်းရေးကြည့်မယ်
CRLF = "\r\n"
# ---- 1. Build a raw HTTP/1.1 request exactly as it goes on the wire -----
body = '{"email":"ko@example.test"}'
lines = [
"POST /api/signup HTTP/1.1",
"Host: shop.example.test",
"User-Agent: hand-rolled/1.0",
"Content-Type: application/json",
"Content-Length: %d" % len(body.encode()),
"Connection: keep-alive",
"Cookie: session=abc123",
]
request = CRLF.join(lines) + CRLF + CRLF + body
print("--- request on the wire (CRLF shown as \\r\\n) ---")
for line in request.split(CRLF):
print(repr(line)[1:-1] + ("\\r\\n" if line != body else ""))
print("request bytes :", len(request.encode()))
# ---- 2. Parse a raw HTTP/1.1 response ----------------------------------
raw = (
"HTTP/1.1 201 Created" + CRLF +
"Content-Type: application/json" + CRLF +
"Content-Length: 28" + CRLF +
"Set-Cookie: session=xyz789; HttpOnly" + CRLF +
"Connection: keep-alive" + CRLF +
CRLF +
'{"id":42,"status":"created"}'
)
FAMILIES = {
1: "informational", 2: "success", 3: "redirection",
4: "client error", 5: "server error",
}
head, _, resp_body = raw.partition(CRLF + CRLF)
status_line, *header_lines = head.split(CRLF)
version, code, reason = status_line.split(" ", 2)
headers = {}
for line in header_lines:
name, _, value = line.partition(": ")
headers[name.lower()] = value
print()
print("--- parsed response ---")
print("version :", version)
print("status : %s %s (%s)"
% (code, reason, FAMILIES[int(code) // 100]))
print("headers parsed :", len(headers))
for name in sorted(headers):
print(" %-14s %s" % (name + ":", headers[name]))
print("body :", resp_body)
declared = int(headers["content-length"])
print("content-length : declared %d, actual %d, match=%s"
% (declared, len(resp_body.encode()), declared == len(resp_body.encode())))
print("connection :", headers["connection"],
"-> socket stays open for the next request")--- request on the wire (CRLF shown as \r\n) ---
POST /api/signup HTTP/1.1\r\n
Host: shop.example.test\r\n
User-Agent: hand-rolled/1.0\r\n
Content-Type: application/json\r\n
Content-Length: 27\r\n
Connection: keep-alive\r\n
Cookie: session=abc123\r\n
\r\n
{"email":"ko@example.test"}
request bytes : 210
--- parsed response ---
version : HTTP/1.1
status : 201 Created (success)
headers parsed : 4
connection: keep-alive
content-length: 28
content-type: application/json
set-cookie: session=xyz789; HttpOnly
body : {"id":42,"status":"created"}
content-length : declared 28, actual 28, match=True
connection : keep-alive -> socket stays open for the next request၅ မိနစ် စမ်းကြည့်
response ထဲက Content-Length ကို 28 ကနေ 12 အဖြစ် ပြောင်းပြီး run ကြည့်ပါ — code က ဘယ်လို ပြသလဲ၊ တကယ့် HTTP client တစ်ခုဆိုရင် ဘာဖြစ်မလဲ။ ပြီးရင် request ထဲမှာ Host header ကို ဖျက်လိုက်ပြီး HTTP/1.1 မှာ Host က ဘာကြောင့် မဖြစ်မနေ လိုအပ်သလဲ စဉ်းစားပါ။
သတိလေးတစ်ချက်
line ending အဖြစ် \n တစ်လုံးတည်း သုံးပြီး raw request ရေးတာ။ HTTP က CRLF ကို သတ်မှတ်ထားပြီး server တစ်ချို့က လက်ခံပေမယ့် proxy တစ်ချို့က ငြင်းပယ်တယ်။
keep-alive ကို multiplexing လို့ မှတ်တာ။ HTTP/1.1 connection တစ်ခုပေါ်မှာ request တွေက စီတန်းနေဆဲ ဖြစ်ပြီး၊ အပြိုင် ပို့နိုင်တာက HTTP/2 ကမှ စတယ်။
RFC 9110 - HTTP Semantics — Computer Networking