ডায়নামিক কন্টেন্ট
কোন রেসপন্স সবার জন্য একই আর কোনটা একজনের নিজস্ব — API, লগইন করা ইউজার, কুকি, মাইক্রোক্যাশিং, এজে পেজ জোড়া লাগানো, আর সেই ভয়ংকর বাগ যেখানে একজনের অ্যাকাউন্ট পেজ সবাই দেখে।
গল্পে বুঝি
গাজীপুরের একটা গার্মেন্টস কারখানার ক্যান্টিনে রাজি বাবুর্চি। দুপুরে দুই হাজার শ্রমিক চল্লিশ মিনিটের বিরতিতে খেতে আসে। রাজি জানে সবকিছু অর্ডার মতো আলাদা করে রাঁধলে অর্ধেক মানুষ না খেয়ে লাইনে ফিরে যাবে। তাই সে খাবারকে তিন ভাগে ভাগ করে। ভাত, ডাল আর আলু ভর্তা সবার জন্য একই, সকালে বড় ডেকচিতে রেঁধে রাখে, পুরো দুপুর ধরে সেখান থেকেই দেয়। মাছের তরকারি বারোটায় রাঁধে, তবে প্রতি দশ মিনিটে দেখে নেয় ঝোল ঠিক আছে কিনা; বিরতির ভিড়ে দশ মিনিট পুরনো তরকারি দিলে কেউ টের পায় না, কিন্তু প্রতিটা প্লেটের জন্য নতুন করে রাঁধলে চুলা আর লোক কোনোটাই কুলোবে না।
তৃতীয় ভাগটা আলাদা। হাইসামের ডায়াবেটিস, তার জন্য চিনি ছাড়া আলাদা খাবার; মরিয়মের চিংড়িতে অ্যালার্জি; প্রতিদিন কয়েকজনের নাম লেখা টিফিন বাক্স আসে বাড়ি থেকে, যেগুলো গরম করে নির্দিষ্ট মানুষকে দিতে হয়। এগুলো রাজি কখনো বড় ডেকচিতে রাখে না। প্রতিটা বাক্সে নাম লেখা, কাউন্টারে কর্মচারী নাম মিলিয়ে দেয়।
একদিন নতুন সহকারী বিরুনি একটা ভুল করল। হাইসামের চিনি-ছাড়া বিশেষ খাবারটা ভালো দেখে ভাবল এটাই আজকের “স্পেশাল”, বড় ডেকচিতে ঢেলে দিল। পরের এক ঘণ্টা যে-ই এলো, সে হাইসামের খাবার পেল, আর হাইসাম নিজে এসে দেখল তার জন্য কিছু নেই। আরও খারাপ হতো যদি ডেকচিতে চলে যেত মরিয়মের নাম লেখা টিফিন, যার ভেতরে তার বাড়ির ঠিকানা লেখা একটা চিরকুট ছিল।
মিলিয়ে নিই: ভাত-ডালের ডেকচি হলো লম্বা TTL-এর পাবলিক ক্যাশ (স্ট্যাটিক ফাইল, পাবলিক পেজ)। দশ মিনিট পর পর দেখে নেওয়া মাছের তরকারি হলো মাইক্রোক্যাশিং: খুব অল্প সময়ের জন্য ডায়নামিক পেজ ক্যাশ করা, যাতে ভিড়ের সময় origin একটা রিকোয়েস্টের জায়গায় হাজার রিকোয়েস্ট সামলাতে না হয়। নাম লেখা টিফিন হলো ব্যক্তিগত রেসপন্স (Cache-Control: private বা no-store), যা শেয়ার্ড ক্যাশে কখনো যাবে না, আর নাম মেলানো কর্মচারী হলো বাইপাস রুল (কুকি বা Authorization থাকলে ক্যাশ এড়ানো)। বিরুনির ভুল হলো CDN-এর সবচেয়ে ভয়ংকর বাগ: একজনের ব্যক্তিগত পেজ শেয়ার্ড ক্যাশে বসে যাওয়া, তারপর সবার কাছে সার্ভ হওয়া। বাস্তবে এই বাগ বড় বড় কোম্পানিতে হয়েছে, আর একবার হলে সেটা একটা ডেটা লিক, শুধু পারফরম্যান্সের বাগ না।
“ডায়নামিক” মানে আসলে কী
“এটা ডায়নামিক পেজ, ক্যাশ করা যাবে না” কথাটা প্রায়ই ভুল। দুইটা আলাদা প্রশ্ন আছে, যা গুলিয়ে ফেলা হয়:
- কন্টেন্ট কত ঘন ঘন বদলায়? (সময়ের প্রশ্ন)
- কন্টেন্ট কি সবার জন্য একই? (ব্যক্তির প্রশ্ন)
| সবার জন্য একই | ইউজার অনুযায়ী আলাদা | |
|---|---|---|
| কদাচিৎ বদলায় | স্ট্যাটিক ফাইল, ব্লগ পোস্ট: লম্বা TTL | ইউজারের প্রোফাইল সেটিং: ব্রাউজারে private |
| ঘন ঘন বদলায় | হোমপেজ, সংবাদ তালিকা, প্রোডাক্ট স্টক, খেলার স্কোর: মাইক্রোক্যাশ | কার্ট, ড্যাশবোর্ড, অ্যাকাউন্ট পেজ: শেয়ার্ড ক্যাশে না |
একটা সংবাদ সাইটের হোমপেজ প্রতি মিনিটে বদলায়, কিন্তু একই মুহূর্তে সবার জন্য একই। এটা “ডায়নামিক”, কিন্তু CDN-এ ক্যাশ করার জন্য আদর্শ। আর ইউজারের সেটিংস পেজ হয়তো মাসে একবার বদলায়, কিন্তু কখনো শেয়ার্ড ক্যাশে যাবে না। ক্যাশ করা যাবে কিনা ঠিক করে দ্বিতীয় প্রশ্ন, প্রথমটা শুধু TTL কত হবে ঠিক করে।
কী একটা রেসপন্সকে ব্যক্তিগত বানায়
একটা রেসপন্স ব্যক্তিগত যদি তার কন্টেন্ট রিকোয়েস্টের এমন কিছুর ওপর নির্ভর করে যা ক্যাশ কী-তে নেই। সাধারণ উৎস:
- সেশন কুকি — লগইন করা ইউজারের নাম, “আপনার কার্টে ৩টা জিনিস”
Authorizationহেডার — API টোকেন, বেসিক অথ- ইউজারের অবস্থান বা ভাষা — যদি সার্ভার IP বা
Accept-Languageদেখে কন্টেন্ট বদলায় - A/B টেস্টের গ্রুপ — কুকিতে রাখা
- CSRF টোকেন — ফর্মের ভেতরে লুকানো, প্রতিটা সেশনে আলাদা
শেষেরটা সূক্ষ্ম: একটা “পাবলিক” প্রোডাক্ট পেজে যদি একটা কমেন্ট ফর্ম থাকে আর তাতে সেশন-নির্দিষ্ট CSRF টোকেন বসানো থাকে, পেজটা আর পাবলিক না। ক্যাশ করলে সবাই একই টোকেন পাবে, আর ফর্ম জমা দেওয়া ফেল করবে বা আরও খারাপ কিছু হবে।
নিয়মগুলো: শেয়ার্ড ক্যাশ কী সংরক্ষণ করতে পারে
HTTP ক্যাশিং স্ট্যান্ডার্ড (RFC 9111) “শেয়ার্ড ক্যাশ” (CDN, প্রক্সি) আর “প্রাইভেট ক্যাশ” (ব্রাউজার) আলাদা করে। ডায়নামিক কন্টেন্টের জন্য প্রাসঙ্গিক নিয়ম:
| হেডার | শেয়ার্ড ক্যাশ কী করবে |
|---|---|
Cache-Control: private | সংরক্ষণ করবে না; শুধু ব্রাউজার রাখতে পারে |
Cache-Control: no-store | কেউ সংরক্ষণ করবে না, ব্রাউজারও না |
Cache-Control: no-cache | সংরক্ষণ করতে পারে, কিন্তু প্রতিবার origin-এর সাথে যাচাই করে তবেই দেবে |
রিকোয়েস্টে Authorization | রেসপন্স ক্যাশ থেকে দেবে না, যদি না রেসপন্সে public, s-maxage বা must-revalidate থাকে |
Set-Cookie রেসপন্সে | স্ট্যান্ডার্ড নিষেধ করে না, কিন্তু বেশিরভাগ CDN আর nginx ডিফল্টে এমন রেসপন্স ক্যাশ করে না |
Authorization-এর নিয়মটা মনে রাখার মতো: স্ট্যান্ডার্ড বলে টোকেন সহ রিকোয়েস্ট ডিফল্টে শেয়ার্ড ক্যাশে যাবে না। কিন্তু আপনি যদি “সব কিছু ১০ মিনিট ক্যাশ করো” জাতীয় একটা জোরালো রুল দেন (CDN-এ “cache everything”, বা origin-এ সব রেসপন্সে s-maxage), সেই সুরক্ষা উঠে যায়। কারণ s-maxage নিজেই সেই ব্যতিক্রমগুলোর একটা।
CDN বা প্রক্সি স্ট্যান্ডার্ড কতটা মানে, সেটা ভেন্ডরভেদে আলাদা। কিছু CDN Set-Cookie সহ রেসপন্স ক্যাশ করে সেই হেডার বাদ দিয়ে, কিছু ক্যাশই করে না, আর “cache everything” রুল অনেক সুরক্ষা বাইপাস করে। নিজের এজে হলেও নির্ভর করবেন না: যা ক্যাশ হওয়া উচিত না, তার জন্য স্পষ্ট বাইপাস রুল লিখুন। আপনার CDN ঠিক কী করে, তাদের ডকে দেখে নিন আর curl দিয়ে পরীক্ষা করুন।
Origin-এর দায়িত্ব: প্রতিটা রেসপন্সে স্পষ্ট হেডার
CDN-এর রুলের চেয়েও বেশি নির্ভরযোগ্য সুরক্ষা হলো origin নিজে প্রতিটা রেসপন্সে ঠিক বলে দেওয়া সেটা কী। একটা ভালো ডিফল্ট: অ্যাপ ডিফল্টে private, no-store পাঠায়, আর পাবলিক রুটগুলো স্পষ্টভাবে পাবলিক ঘোষণা করে। উল্টোটা না। কেউ নতুন একটা রুট যোগ করে হেডার দিতে ভুলে গেলে সেটা ক্যাশ না হওয়াই ভালো, ক্যাশ হওয়ার চেয়ে।
// Default every response to non-cacheable; public handlers opt in.
func noStoreByDefault(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "private, no-store")
next.ServeHTTP(w, r)
})
}
func articleHandler(w http.ResponseWriter, r *http.Request) {
// Same for every visitor, changes every few minutes: let the CDN hold it briefly.
w.Header().Set("Cache-Control", "public, max-age=0, s-maxage=60, stale-while-revalidate=30")
renderArticle(w, r)
}
func accountHandler(w http.ResponseWriter, r *http.Request) {
// Inherits private, no-store from the middleware.
renderAccount(w, r, currentUser(r))
} max-age=0, s-maxage=60 মানে: ব্রাউজার রাখবে না (প্রতিবার CDN-কে জিজ্ঞেস করবে), CDN ৬০ সেকেন্ড রাখবে। এতে পার্জ করলে বা কন্টেন্ট বদলালে ব্রাউজারে আটকে থাকা পুরনো কপির সমস্যা থাকে না, অথচ origin ভিড় থেকে বাঁচে।
মাইক্রোক্যাশিং
মাইক্রোক্যাশিং মানে একটা পাবলিক ডায়নামিক রেসপন্স অল্প কয়েক সেকেন্ডের জন্য ক্যাশ করা। শুনতে অর্থহীন লাগে (“মাত্র ১ সেকেন্ড?”), কিন্তু হিসাবটা দেখুন:
illustrative
homepage traffic at peak : 2,000 requests/second
origin render time : 150 ms, ~40 renders/second per app server
without cache : 2,000 renders/s -> ~50 app servers
with a 1-second microcache : ~1 render/s per edge cache node
(plus collapsing: exactly one in flight per key) ভিড় যত বেশি, মাইক্রোক্যাশ তত শক্তিশালী, কারণ origin-এর লোড আর ট্রাফিকের ওপর নির্ভর করে না, শুধু TTL আর ক্যাশ নোডের সংখ্যার ওপর নির্ভর করে। ইউজার সর্বোচ্চ এক সেকেন্ড পুরনো পেজ দেখে, যা প্রায় সব কন্টেন্টের জন্য অদৃশ্য।
nginx-এ একটা মাইক্রোক্যাশ, সব প্রয়োজনীয় বাইপাস সহ:
proxy_cache_path /var/cache/nginx/micro keys_zone=micro:50m max_size=2g inactive=10m;
# Anything that makes the response personal skips the cache entirely.
map $http_cookie $has_session {
default 0;
"~*(^|;\s*)(sessionid|auth_token)=" 1;
}
map $request_method $not_cacheable_method {
default 1;
GET 0;
HEAD 0;
}
server {
listen 443 ssl;
server_name baytalhikma.org;
location / {
proxy_pass http://app;
proxy_cache micro;
proxy_cache_valid 200 301 1s;
proxy_cache_valid 404 10s;
proxy_cache_lock on;
proxy_cache_use_stale updating error timeout http_500 http_502 http_503;
proxy_cache_background_update on;
proxy_cache_bypass $has_session $http_authorization $not_cacheable_method;
proxy_no_cache $has_session $http_authorization $not_cacheable_method;
add_header X-Cache-Status $upstream_cache_status always;
}
} দুইটা জোড়া ডিরেক্টিভ কেন:
proxy_cache_bypass— এই রিকোয়েস্টের জন্য ক্যাশ থেকে পড়বে নাproxy_no_cache— এই রিকোয়েস্টের রেসপন্স ক্যাশে লিখবে না
শুধু প্রথমটা দিলে লগইন করা ইউজার ক্যাশ থেকে পড়বে না ঠিকই, কিন্তু তার ব্যক্তিগত রেসপন্স ক্যাশে লেখা হয়ে যেতে পারে, আর পরের লগআউট করা ভিজিটর সেটা পাবে। দুটো সবসময় একসাথে।
$http_authorization এখানে স্পষ্ট করে দেওয়া হয়েছে। nginx-এর proxy_cache রিকোয়েস্টের Authorization দেখে নিজে থেকে ক্যাশ এড়াবে, এর ওপর নির্ভর করবেন না; লিখে দিন। nginx ডিফল্টে Set-Cookie সহ রেসপন্স ক্যাশ করে না, এটা একটা ভালো দ্বিতীয় জাল, কিন্তু প্রথম জাল আপনার নিজের রুল।
কুকি: ক্যাশের সবচেয়ে বড় শত্রু
বেশিরভাগ সাইটে প্রায় সব রিকোয়েস্টে কোনো না কোনো কুকি থাকে: অ্যানালিটিক্স, কনসেন্ট ব্যানার, A/B টেস্ট। “কুকি থাকলেই বাইপাস” রুল দিলে hit ratio প্রায় শূন্য হয়ে যায়। তাই রুলটা হতে হবে নির্দিষ্ট কুকির নাম ধরে, যেমন উপরের sessionid|auth_token, সব কুকি ধরে না।
আরও কয়েকটা অভ্যাস:
- পাবলিক পেজে সেশন শুরু করবেন না। অনেক ফ্রেমওয়ার্ক প্রথম ভিজিটেই একটা সেশন কুকি বসায়, লগইন না করলেও। ফলে প্রতিটা ভিজিটর দ্বিতীয় পেজ থেকেই “লগইন করা” দেখায় আর ক্যাশ বাইপাস হয়। সেশন শুধু লগইনে বা কার্টে কিছু যোগ করলে শুরু করুন।
- স্ট্যাটিক ফাইল আলাদা হোস্টনেমে (
static.baytalhikma.org) রাখলে সেখানে মূল ডোমেইনের কুকি যায়ই না। - ক্যাশ কী-তে কুকি রাখবেন না (কখনো কখনো এটা “সমাধান” হিসেবে প্রস্তাব করা হয়)। প্রতিটা ইউজারের আলাদা কপি মানে hit ratio শূন্য, আর ক্যাশ নষ্ট। ব্যতিক্রম: খুব কম সম্ভাব্য মানের একটা কুকি, যেমন
lang=bnবাlang=en; সেটা কী-তে রাখা ঠিক আছে (চ্যাপ্টার ৪)।
API ক্যাশিং
পাবলিক API-এর GET এন্ডপয়েন্টও ক্যাশ করা যায়: প্রোডাক্ট তালিকা, এক্সচেঞ্জ রেট, নামাজের সময়সূচি, পাবলিক প্রোফাইল। নিয়ম পেজের মতোই, সাথে কয়েকটা বাড়তি বিষয়:
- Query string ক্যাশ কী-র অংশ।
?page=2&sort=priceআর?sort=price&page=2আলাদা কী হয়ে যায় যদি CDN প্যারামিটার সাজিয়ে না নেয়। ক্লায়েন্টে একটা নির্দিষ্ট ক্রমে প্যারামিটার বানান, বা CDN-এ query string normalization চালু করুন। - ইউজার-নির্দিষ্ট ফিল্ড আলাদা এন্ডপয়েন্টে।
/products/42-এর রেসপন্সে “আপনি কি এটা উইশলিস্টে রেখেছেন” ফিল্ড থাকলে পুরো রেসপন্স ব্যক্তিগত হয়ে যায়। পাবলিক ডেটা/products/42(ক্যাশযোগ্য), ব্যক্তিগত অংশ/me/wishlist(ক্যাশ না) — এভাবে ভাগ করলে বড় অংশটা ক্যাশে যায়। - API key query string-এ থাকলে প্রতিটা ক্লায়েন্টের আলাদা কী, আর key ক্যাশ লগে চলে যায়। হেডারে রাখুন।
Vary: Authorizationঅনেক সময় প্রস্তাব করা হয়। এতে প্রতিটা টোকেনের জন্য আলাদা কপি হয় (ক্যাশ প্রায় অকেজো), আর টোকেন কী-র অংশ হয়ে যায়। ব্যক্তিগত API রেসপন্স শেয়ার্ড ক্যাশে না রাখাই সহজ আর নিরাপদ।
GraphQL-এর সমস্যা
GraphQL সাধারণত সব কুয়েরি POST /graphql দিয়ে পাঠায়। CDN POST ক্যাশ করে না, আর সব কুয়েরির URL একই, তাই ক্যাশ কী-তে পার্থক্য থাকে না। সমাধান persisted queries: ক্লায়েন্ট পুরো কুয়েরির বদলে তার একটা হ্যাশ পাঠায়, GET দিয়ে:
GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}&variables={"id":"42"} এখন URL প্রতিটা কুয়েরি আর ভ্যারিয়েবল অনুযায়ী আলাদা, মেথড GET, আর CDN সেটা সাধারণ GET-এর মতো ক্যাশ করতে পারে। বাড়তি লাভ: সার্ভার শুধু আগে থেকে নিবন্ধিত কুয়েরি গ্রহণ করলে কেউ ইচ্ছামতো ভারী কুয়েরি পাঠাতে পারে না। GraphQL রেসপন্সের Cache-Control সেট করা কঠিন (একটা কুয়েরিতে পাবলিক আর ব্যক্তিগত দুই ফিল্ড থাকতে পারে), তাই অনেক সার্ভার লাইব্রেরি প্রতিটা ফিল্ডের ক্যাশ নীতি থেকে সবচেয়ে কড়াটা বেছে নেয়। আপনার লাইব্রেরি কী করে, তার ডকে দেখে নিন।
এজে পেজ জোড়া লাগানো: ESI আর এজ কম্পোজিশন
একটা পেজের ৯৫% পাবলিক, শুধু হেডারের “সালাম, ফাতিমা” অংশটা ব্যক্তিগত। পুরো পেজ ক্যাশ বাদ দেওয়া অপচয়। দুই ধরনের সমাধান:
১. ক্লায়েন্ট-সাইডে ব্যক্তিগত অংশ। পুরো পেজ পাবলিক আর ক্যাশড, হেডারে একটা খালি জায়গা। পেজ লোডের পর JavaScript /me এন্ডপয়েন্ট (ক্যাশ-বিহীন) থেকে নাম এনে বসায়। সহজ, যেকোনো CDN-এ কাজ করে। খরচ: ব্যক্তিগত অংশ একটু পরে দেখা যায়।
২. এজে জোড়া লাগানো। পেজের টেমপ্লেটে একটা ট্যাগ থাকে, এজ সেটা দেখে আলাদা একটা ফ্র্যাগমেন্ট আনে আর জায়গামতো বসায়। পুরনো স্ট্যান্ডার্ড হলো ESI (Edge Side Includes), যা Varnish আর কিছু CDN সাপোর্ট করে:
<header>
<esi:include src="/fragments/user-greeting" />
</header>
<main>
... cached article body ...
</main> এজ মূল পেজ ১০ মিনিট ক্যাশ করে, আর প্রতিটা রিকোয়েস্টে শুধু /fragments/user-greeting origin থেকে আনে (বা সেটাকে আলাদা নীতিতে ক্যাশ করে)। আধুনিক বিকল্প হলো এজ ফাংশন দিয়ে HTML স্ট্রিম করার সময় অংশ বদলানো (চ্যাপ্টার ১১)। দুটোতেই একটা বিষয় সাবধানে দেখতে হয়: ব্যক্তিগত ফ্র্যাগমেন্টের রেসপন্সটা নিজেই private থাকতে হবে, নইলে সমস্যাটা পেজ থেকে ফ্র্যাগমেন্টে সরে যায় মাত্র।
ডায়নামিক কন্টেন্টে stale-while-revalidate
stale-while-revalidate (চ্যাপ্টার ৬-এ বিস্তারিত) ডায়নামিক কন্টেন্টে বিশেষ কাজের:
Cache-Control: public, s-maxage=30, stale-while-revalidate=60, stale-if-error=86400 - প্রথম ৩০ সেকেন্ড: ক্যাশ থেকে টাটকা
- ৩০ থেকে ৯০ সেকেন্ড: ক্যাশের পুরনো কপি দেওয়া হয় সাথে সাথে, আর ব্যাকগ্রাউন্ডে একটা রিফ্রেশ যায়
- origin ডাউন থাকলে: এক দিন পর্যন্ত পুরনো কপি দিয়ে চালিয়ে যাওয়া
ফলে কোনো ইউজার কখনো origin-এর রেন্ডারের জন্য অপেক্ষা করে না, যতক্ষণ পেজটা নিয়মিত চাওয়া হয়। CDN এই ডিরেক্টিভ কতটা সমর্থন করে, ভেন্ডরের ডকে দেখে নিন; না করলে CDN-এর নিজস্ব “serve stale” সেটিং থাকে।
ভয়ংকর বাগ: একজনের পেজ সবার কাছে
এবার সেই ঘটনা যা প্রতিটা CDN ব্যবহারকারীর জানা উচিত। মেকানিজমটা প্রায় সবসময় একই:
- কেউ একটা “cache everything” রুল দেয়, বা origin-এ একটা মিডলওয়্যার সব রেসপন্সে
public, s-maxage=300বসায়, পারফরম্যান্সের জন্য। - একজন লগইন করা ইউজার, ধরুন সিনা,
/accountখোলে। এই URL-এর জন্য ক্যাশে কিছু নেই, তাই রিকোয়েস্ট origin-এ যায়, origin সিনার নাম, ইমেইল, অর্ডারের ইতিহাস সহ পেজ বানায়। - রেসপন্সে
publicথাকায় CDN সেটা/accountকী-তে ক্যাশ করে। কুকি ক্যাশ কী-তে নেই। - পরের ৫ মিনিট যে-ই
/accountখোলে, লগইন করা হোক বা না, সে সিনার পেজ দেখে।
এর আরও কয়েকটা রূপ:
- Web cache deception (চ্যাপ্টার ১২): আক্রমণকারী সিনাকে
/account/profile.cssলিংকে ক্লিক করায়। অ্যাপ অজানা সাফিক্স উপেক্ষা করে/accountপেজ দেখায়, আর CDN.cssদেখে ভাবে স্ট্যাটিক ফাইল, ক্যাশ করে। আক্রমণকারী তারপর একই URL খুলে সিনার পেজ পায়। - রিডাইরেক্ট ক্যাশ হওয়া: লগইনের পর
302 Location: /dashboard?token=...ক্যাশ হয়ে গেলে টোকেন সবার কাছে যায়। Set-Cookieক্যাশ হওয়া: CDN হেডার সহ রেসপন্স ক্যাশ করলে সবাই একই সেশন কুকি পায় আর সবাই একই ইউজার হিসেবে লগইন হয়ে যায়।- এরর পেজ: origin-এর একটা ডিবাগ এরর পেজে সেশনের তথ্য ছিল, আর CDN 500 পেজ ক্যাশ করল।
প্রতিরোধের চেকলিস্ট
- অ্যাপের ডিফল্ট
private, no-store; পাবলিক রুট স্পষ্টভাবে ঘোষণা করে। - CDN-এ “cache everything” ধরনের রুল শুধু নির্দিষ্ট পাথে (
/assets/*,/blog/*), পুরো সাইটে কখনো না। - সেশন কুকি বা
Authorizationথাকলে বাইপাস, পড়া আর লেখা দুটোতেই। - ব্যক্তিগত পাথ (
/account,/checkout,/api/me) CDN-এ স্পষ্টভাবে বাইপাস, অ্যাপের হেডারের ওপর নির্ভর না করে, দ্বিতীয় স্তর হিসেবে। - ক্যাশ শুধু এক্সটেনশন দেখে নয়, রেসপন্সের
Content-TypeআরCache-Controlদেখে। অ্যাপ অজানা পাথে 404 দিক, ভিন্ন পেজ না। Set-Cookieসহ রেসপন্স কখনো শেয়ার্ড ক্যাশে না।- একটা স্বয়ংক্রিয় টেস্ট: লগইন করে ব্যক্তিগত পাথ খুলুন, তারপর কুকি ছাড়া একই পাথ খুলুন; দ্বিতীয় রেসপন্সে প্রথমটার ইউজারের কোনো তথ্য থাকলে CI ফেল।
#!/usr/bin/env bash
# Fails if a logged-in page is ever served from cache to an anonymous request.
set -euo pipefail
url="https://baytalhikma.org/account"
marker="sina@baytalhikma.org" # something only the test user's page contains
curl -s -b "sessionid=$TEST_SESSION" "$url" -o /dev/null
body=$(curl -s "$url")
if grep -q "$marker" <<<"$body"; then
echo "LEAK: anonymous request received a logged-in page" >&2
exit 1
fi
echo "ok" এই বাগ ধরা পড়লে প্রথম কাজ পুরো ক্যাশ পার্জ করা (বা অন্তত সংশ্লিষ্ট পাথ), তারপর রুল ঠিক করা। ক্রমটা উল্টো করলে রুল ঠিক হওয়ার পরও ক্যাশে জমা থাকা ব্যক্তিগত পেজগুলো TTL শেষ হওয়া পর্যন্ত সার্ভ হতে থাকবে। আর এটা একটা ডেটা লিক: কোন সময় থেকে কোন সময় পর্যন্ত কাদের পেজ ফাঁস হতে পারে, লগ থেকে বের করে নিয়ম অনুযায়ী জানাতে হতে পারে।
ডিবাগিং: এই চ্যাপ্টারের সমস্যাগুলো
লক্ষণ: এক ইউজার আরেকজনের নাম, কার্ট বা অ্যাকাউন্ট দেখছে। নিশ্চিত করুন: কুকি ছাড়া ব্যক্তিগত পাথটা চেয়ে দেখুন রেসপন্স ক্যাশ থেকে আসছে কিনা।
curl -sI https://baytalhikma.org/account | grep -iE 'cache-control|age|cache-status|x-cache|set-cookie' illustrative: this is the leak
cache-control: public, s-maxage=300
age: 142
x-cache: HIT HIT বা শূন্যের বেশি age = ব্যক্তিগত পেজ ক্যাশে। সমাধান: আগে পার্জ, তারপর origin-এ private, no-store, CDN-এ পাথ বাইপাস, “cache everything” রুলের পরিধি কমানো।
লক্ষণ: ইউজাররা বলছে লগইন করার পরও “লগইন করুন” বাটন দেখছে, বা লগআউটের পরও নাম দেখছে। নিশ্চিত করুন: লগইন করা কুকি সহ রিকোয়েস্টে ক্যাশ স্ট্যাটাস দেখুন।
curl -sI -b 'sessionid=abc123' https://baytalhikma.org/ | grep -iE 'cache-status|x-cache|age' কুকি সহও HIT = বাইপাস রুল কাজ করছে না (কুকির নাম ভুল, বা রুল শুধু পড়ায়, লেখায় না)। সমাধান: কুকির আসল নাম মিলিয়ে বাইপাস রুল; nginx-এ proxy_cache_bypass আর proxy_no_cache দুটোই। বিকল্প হিসেবে ব্যক্তিগত অংশ ক্লায়েন্ট-সাইডে আনুন।
লক্ষণ: পাবলিক পেজের hit ratio প্রায় শূন্য। নিশ্চিত করুন: একজন সাধারণ ভিজিটর কোন কুকি পাঠায় আর রেসপন্সে কী আসে।
curl -sI https://baytalhikma.org/blog/ | grep -iE 'set-cookie|cache-control|vary' প্রথম ভিজিটেই set-cookie: sessionid=... = ফ্রেমওয়ার্ক সবার জন্য সেশন শুরু করছে, তাই সবাই বাইপাসে পড়ছে। cache-control: private = অ্যাপের ডিফল্ট পাবলিক রুটেও প্রযোজ্য। সমাধান: সেশন শুধু দরকার হলে; পাবলিক রুটে স্পষ্ট public, s-maxage; বাইপাস রুল নির্দিষ্ট কুকির নামে।
লক্ষণ: ফর্ম জমা দিলে “CSRF token mismatch”, শুধু কিছু ইউজারের, CDN চালুর পর। নিশ্চিত করুন: দুইবার পেজ আনুন আর টোকেন তুলনা করুন।
for i in 1 2; do curl -s https://baytalhikma.org/contact | grep -o 'name="csrf_token" value="[^"]*"'; done দুইবারই একই টোকেন = ক্যাশড পেজে সেশন-নির্দিষ্ট টোকেন। সমাধান: ফর্মওয়ালা পেজ ক্যাশ বাদ দিন, অথবা টোকেন JavaScript দিয়ে ক্যাশ-বিহীন এন্ডপয়েন্ট থেকে আনুন।
লক্ষণ: API রেসপন্স পুরনো বা ভুল ইউজারের ডেটা। নিশ্চিত করুন: দুই আলাদা টোকেন দিয়ে একই এন্ডপয়েন্ট চেয়ে age আর বডি তুলনা করুন।
curl -s -H "Authorization: Bearer $TOKEN_A" -D - https://api.baytalhikma.org/v1/me -o a.json | grep -i age
curl -s -H "Authorization: Bearer $TOKEN_B" -D - https://api.baytalhikma.org/v1/me -o b.json | grep -i age
diff a.json b.json && echo "SAME BODY: leak" সমাধান: ব্যক্তিগত এন্ডপয়েন্টে private, no-store; API রেসপন্সে s-maxage বা public সাবধানে, শুধু পাবলিক এন্ডপয়েন্টে।
লক্ষণ: GraphQL-এ CDN কোনো লাভ দিচ্ছে না, origin-এ সব কুয়েরি পৌঁছায়। নিশ্চিত করুন: নেটওয়ার্ক ট্যাবে দেখুন কুয়েরিগুলো POST কিনা। সমাধান: persisted queries দিয়ে GET, আর রেসপন্সে ফিল্ড-ভিত্তিক Cache-Control।
লক্ষণ: মাইক্রোক্যাশ চালুর পরও ভিড়ে origin ভেঙে পড়ে। নিশ্চিত করুন: ভিড়ের সময় এজের X-Cache-Status গুনুন।
awk '{print $NF}' /var/log/nginx/edge.log | sort | uniq -c EXPIRED আর MISS বেশি, UPDATING বা STALE নেই = proxy_cache_lock আর use_stale updating বসানো হয়নি, তাই প্রতি সেকেন্ডের এক্সপায়ারিতে সবাই একসাথে origin-এ যাচ্ছে। BYPASS বেশি = বাইপাস রুল খুব বিস্তৃত।
মূল শিক্ষা
- “ঘন ঘন বদলায়” আর “সবার জন্য আলাদা” দুইটা ভিন্ন প্রশ্ন। ক্যাশ করা যাবে কিনা ঠিক করে দ্বিতীয়টা; প্রথমটা শুধু TTL।
- অ্যাপের ডিফল্ট
private, no-store, পাবলিক রুট নিজে ঘোষণা করে। ভুলে গেলে ক্যাশ না হওয়াই নিরাপদ। - মাইক্রোক্যাশিং ভিড়ে origin-এর লোড ট্রাফিক থেকে আলাদা করে দেয়। এক সেকেন্ডের TTL-ও বিশাল পার্থক্য আনে, সাথে collapsing আর stale থাকলে।
- বাইপাস রুল পড়া আর লেখা দুটোতেই। nginx-এ
proxy_cache_bypassআরproxy_no_cacheসবসময় একসাথে। - নির্দিষ্ট সেশন কুকির নামে বাইপাস, সব কুকিতে না। আর পাবলিক পেজে সেশন শুরুই করবেন না।
- ব্যক্তিগত ডেটা আলাদা এন্ডপয়েন্টে বা ফ্র্যাগমেন্টে রাখলে পেজের বড় অংশ ক্যাশে যায়।
- “cache everything” শুধু নির্দিষ্ট পাথে। একজনের পেজ সবার কাছে যাওয়া পারফরম্যান্স বাগ না, ডেটা লিক।
- লিক ধরা পড়লে আগে পার্জ, তারপর রুল ঠিক। আর CI-তে একটা টেস্ট রাখুন যা লগইন করা পেজ বেনামি রিকোয়েস্টে আসলে ফেল করে।