ওয়েবহুক ব্যাখ্যা: সাইনড পেলোড ও নির্ভরযোগ্য রিট্রাই
আপনার ইন্টিগ্রেশন স্টেজিং-এ কাজ করে: পেমেন্ট প্রোভাইডার একটি ইভেন্ট পাঠায়, আপনার এন্ডপয়েন্ট তা গ্রহণ করে, অর্ডারটি paid হয়ে যায়। তারপর প্রোডাকশনে, একজন গ্রাহক দুবার পেমেন্ট করেন, ডিপ্লয়ের সময় একটি ডেলিভারি নিঃশব্দে হারিয়ে যায়, এবং কেউ একটি ক্যাপচার করা রিকোয়েস্ট রিপ্লে করে অর্ডারটি রিফান্ড হিসেবে চিহ্নিত করে। তিনটি ব্যর্থতারই মূল কারণ এক: ওয়েবহুককে একটি সাধারণ HTTP POST হিসেবে বিবেচনা করা হয়েছিল, নিরাপত্তা-গুরুত্বপূর্ণ, অ্যাট-লিস্ট-ওয়ান্স ডেলিভারি চ্যানেল হিসেবে নয়।
এই গাইডে সঠিক ওয়েবহুক ইমপ্লিমেন্টেশনের দুটি দিক কভার করা হয়েছে — পেলোডটি আসল কিনা তা যাচাই করা, এবং রিট্রাই সত্ত্বেও ডেলিভারি টিকে থাকা — এমন কোড সহ যা আপনি যেকোনো স্ট্যাকে মানিয়ে নিতে পারেন।
ওয়েবহুক আসলে কী
ওয়েবহুক হল একটি সার্ভার-টু-সার্ভার HTTP রিকোয়েস্ট যা একটি সিস্টেম ইভেন্ট ঘটলে অন্য সিস্টেমে পাঠায়। এটি সাধারণ পোলিং মডেলকে উল্টে দেয়: আপনার অ্যাপ প্রতি মিনিটে "নতুন কিছু আছে?" জিজ্ঞাসা করার বদলে, প্রোডিউসার ইভেন্টটি আপনার নিবন্ধিত URL-এ পুশ করে।
এই ডিজাইন থেকে দুটি বৈশিষ্ট্য আসে, এবং উভয়ই গুরুত্বপূর্ণ:
- আপনার এন্ডপয়েন্ট পাবলিক। ইন্টারনেটে যে কেউ এতে POST করতে পারে, তাই প্রতিটি রিকোয়েস্ট অবশ্যই প্রমাণিত হতে হবে।
- ডেলিভারি অ্যাট-লিস্ট-ওয়ান্স। নেটওয়ার্ক ব্যর্থ হয়, সার্ভার রিস্টার্ট হয়, এবং টাইমআউট অস্পষ্ট — প্রেরক জানতে পারে না আপনার হ্যান্ডলার চলেছিল কিনা। তাই এটি রিট্রাই করে, এবং আপনি একই ইভেন্ট একাধিকবার দেখতে পারেন।
নিচের সবকিছুই এই দুটি তথ্যের পরিণতি।
HMAC দিয়ে পেলোড সাইন করা
স্ট্যান্ডার্ড পদ্ধতি হল একটি শেয়ারড সিক্রেট এবং কাঁচা রিকোয়েস্ট বডির উপর একটি HMAC। প্রেরক একটি স্বাক্ষর গণনা করে এবং একটি হেডারে রাখে; আপনি এটি পুনরায় গণনা করে তুলনা করেন। যদি মিলে যায়, পেলোডটি এমন কারও কাছ থেকে এসেছে যে সিক্রেট জানে এবং ট্রানজিটে পরিবর্তিত হয়নি।
তিনটি বিবরণ একটি কার্যকর ইমপ্লিমেন্টেশনকে ভাঙা থেকে আলাদা করে:
- কাঁচা বাইটে সাইন করুন। যাচাইয়ের পরেই JSON পার্স করুন। বডি পুনরায় সিরিয়ালাইজ করলে হোয়াইটস্পেস এবং কী-এর ক্রম পরিবর্তিত হয়, এবং স্বাক্ষর কখনো মিলবে না।
- কনস্ট্যান্ট-টাইম তুলনা ব্যবহার করুন। একটি সাধারণ
==টাইমিং তথ্য ফাঁস করে। ভাষার বিল্ট-ইন নিরাপদ তুলনা ব্যবহার করুন। - সাইনড স্ট্রিংয়ে একটি টাইমস্ট্যাম্প অন্তর্ভুক্ত করুন।
timestamp + "." + bodyসাইন করলে আপনি পুরানো রিকোয়েস্ট প্রত্যাখ্যান করতে পারেন, যা রিপ্লে আক্রমণ ব্লক করে।
Node.js-এ স্বাক্ষর যাচাই
const crypto = require('crypto');
function verifyWebhook(rawBody, headers, secret, toleranceSeconds = 300) {
const signature = headers['x-signature']; // e.g. "t=1712345678,v1=abc123..."
const timestamp = headers['x-timestamp'];
if (!signature || !timestamp) return false;
// 1. Reject stale requests (replay protection)
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (!Number.isFinite(age) || age > toleranceSeconds) return false;
// 2. Recompute over the exact bytes received
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload, 'utf8')
.digest('hex');
// 3. Constant-time compare
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(signature, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Express-এ, এর জন্য ওয়েবহুক রুটে express.raw({ type: 'application/json' }) প্রয়োজন যাতে বডি একটি Buffer হিসেবে আসে। যদি একটি গ্লোবাল express.json() মিডলওয়্যার আগে চলে, কাঁচা বডি চলে যায় এবং যাচাই ব্যর্থ হবে — একটি খুব সাধারণ বাগ।
Python-এ যাচাই
import hmac, hashlib, time
def verify(raw_body: bytes, timestamp: str, signature: str, secret: bytes, tolerance: int = 300) -> bool:
if abs(time.time() - int(timestamp)) > tolerance:
return False
signed = timestamp.encode() + b"." + raw_body
expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
ব্যর্থ চেকের উপর সর্বদা 401 ফেরত দিন এবং রিকোয়েস্ট আইডি লগ করুন — কিন্তু কখনো সিক্রেট বা সম্পূর্ণ স্বাক্ষর লগ করবেন না।
এমন রিট্রাই ডিজাইন করুন যা সত্যিই কাজ করে
রিট্রাই তখনই কার্যকর যদি আপনার এন্ডপয়েন্ট পূর্বানুমেয় আচরণ করে। চুক্তিটি সহজ: ইভেন্টটি টেকসইভাবে গ্রহণ করার পর দ্রুত 2xx ফেরত দিন, এবং বাকি সবকিছু রিট্রাই হতে দিন।
| আপনার প্রতিক্রিয়া | প্রেরকের আচরণ | পরিবর্তে কী করবেন |
|---|---|---|
200 / 204 | ডেলিভারড চিহ্নিত করে | সঠিক — এনকিউ করার পর স্বীকার করুন |
400 / 401 | সাধারণত রিট্রাই নেই | স্বাক্ষর বা পেলোড হ্যান্ডলিং ঠিক করুন |
429 / 503 | ব্যাকঅফ সহ রিট্রাই | ওভারলোড হলে ফেরত দিন, সাধারণ 500 নয় |
500 / টাইমআউট | রিট্রাই | ধীর কাজ একটি কিউতে সরান |
দুটি নিয়ম এটি সুস্থ রাখে:
- দ্রুত স্বীকার করুন। যাচাই করুন, ইভেন্টটি একটি কিউ বা টেবিলে সংরক্ষণ করুন,
200ফেরত দিন। আসল কাজটি অ্যাসিঙ্ক্রোনাসভাবে করুন। যে হ্যান্ডলার প্রতিক্রিয়া দেওয়ার আগে তিনটি API কল করে তা টাইমআউট হবে এবং ডুপ্লিকেট ডেলিভারি ট্রিগার করবে। - জিটার সহ এক্সপোনেনশিয়াল ব্যাকঅফ ব্যবহার করুন। যদি প্রেরক অবিলম্বে রিট্রাই করে, একটি সংক্ষিপ্ত বিভ্রাট থান্ডারিং হার্ডে পরিণত হয়। 10s, 30s, 2m, 10m, 1h এর মতো একটি সময়সূচি র্যান্ডম জিটার সহ লোড ছড়িয়ে দেয়।
হ্যান্ডলার আইডেম্পোটেন্ট করুন
যেহেতু ডেলিভারি অ্যাট-লিস্ট-ওয়ান্স, একই ইভেন্ট আইডি দুবার আসতে পারে — একবার টাইমআউটের পরে যেখানে আপনার হ্যান্ডলার আসলে সফল হয়েছিল। প্রসেস করা ইভেন্ট আইডি সংরক্ষণ করুন এবং ডুপ্লিকেট এড়িয়ে যান:
INSERT INTO processed_events (event_id, received_at)
VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING;
-- If 0 rows inserted, this is a duplicate: return 200 and stop.
যেখানে সম্ভব, ব্যবসায়িক অপারেশনটিকে নিজেই আইডেম্পোটেন্ট করুন — কাউন্টার বাড়ানোর বদলে অর্ডার স্ট্যাটাস paid সেট করুন, এবং ডাউনস্ট্রিম সার্ভিস কল করার সময় ইভেন্ট আইডিকে আইডেম্পোটেন্সি কী হিসেবে ব্যবহার করুন।
লাইভে যাওয়ার আগে একটি চেকলিস্ট
- কনস্ট্যান্ট-টাইম তুলনা দিয়ে কাঁচা বাইটের উপর HMAC যাচাই করুন।
- রিপ্লে বন্ধ করতে কয়েক মিনিটের পুরানো টাইমস্ট্যাম্প প্রত্যাখ্যান করুন।
- ইভেন্ট সংরক্ষণ করুন, তারপর এক-দুই সেকেন্ডের মধ্যে
2xxফেরত দিন। - কোনো কাজ করার আগে ইভেন্ট আইডিতে ডুপ্লিকেট সরান।
- ইভেন্ট আইডি, টাইপ এবং ফলাফল লগ করুন — কখনো সিক্রেট নয়।
- আপনার ড্যাশবোর্ড বা অ্যাডমিন টুল থেকে ম্যানুয়ালি ইভেন্ট রিপ্লে করার একটি উপায় প্রকাশ করুন।
- ইচ্ছাকৃতভাবে ভাঙা স্বাক্ষর, ডুপ্লিকেট ডেলিভারি এবং ধীর হ্যান্ডলার দিয়ে পরীক্ষা করুন।
যখন আপনি একটি ব্যর্থ ইন্টিগ্রেশন ডিবাগ করছেন, কাঁচা রিকোয়েস্ট বডিই সেই জিনিস যা আপনি সবচেয়ে বেশি দেখবেন। এটি একটি JSON ফরম্যাটার-এ পেস্ট করলে নেস্টেড ইভেন্ট অবজেক্টগুলো পঠনযোগ্য হয় এবং আপনি ইতিমধ্যে ক্যাপচার করা বাইট পরিবর্তন করেন না।
সাধারণ প্রশ্ন
পেলোড অভিন্ন দেখালেও কেন আমার স্বাক্ষর চেক ব্যর্থ হয়?
প্রায় সবসময় কারণ হ্যাশ করার আগে বডিটি পার্স এবং পুনরায় সিরিয়ালাইজ করা হয়েছিল। JSON কী-এর ক্রম, হোয়াইটস্পেস এবং ইউনিকোড এস্কেপিং পার্স/স্ট্রিংগিফাইয়ের সময় পরিবর্তিত হয়, তাই বাইটগুলো আর প্রেরকের সাইন করা বাইটের সাথে মেলে না। কাঁচা বডি ক্যাপচার করুন এবং সেটি হ্যাশ করুন।
ইভেন্ট প্রসেস করার আগে না পরে 200 ফেরত দেওয়া উচিত?
এটি টেকসইভাবে সংরক্ষণের পরে, ভারী কাজ করার আগে। যদি আপনি আগে প্রসেস করেন, ধীর ডাউনস্ট্রিম কল টাইমআউট ঘটায়, প্রেরক রিট্রাই করে, এবং আপনি ডুপ্লিকেট পান। ইভেন্টটি কিউ করুন, অবিলম্বে প্রতিক্রিয়া দিন, এবং অ্যাসিঙ্ক্রোনাসভাবে প্রসেস করুন।
ওয়েবহুক এন্ডপয়েন্ট সুরক্ষিত করতে HTTPS কি যথেষ্ট?
না। TLS ট্রানজিটে ডেটা সুরক্ষিত করে কিন্তু প্রমাণ করে না কে রিকোয়েস্ট পাঠিয়েছে — যে কেউ একটি পাবলিক URL-এ POST করতে পারে। শেয়ারড সিক্রেট সহ HMAC সাইনিং প্রেরককে প্রমাণ করে, এবং একটি সাইনড টাইমস্ট্যাম্প ক্যাপচার করা রিকোয়েস্টের রিপ্লে প্রতিরোধ করে।
শেষ কথা
ওয়েবহুককে যেকোনো অবিশ্বস্ত ইনপুটের মতো behandle করুন: কাঁচা বাইটের উপর স্বাক্ষর যাচাই করুন, টাইমস্ট্যাম্প পরীক্ষা করুন, ইভেন্ট আইডি দিয়ে ডুপ্লিকেট সরান, এবং ইভেন্ট নিরাপদে সংরক্ষণের পরেই স্বীকার করুন। তা করলে, রিট্রাই ডুপ্লিকেট চার্জের উৎস হওয়া বন্ধ করে এবং যা হওয়ার কথা ছিল — একটি সুরক্ষা জাল — তা হয়ে যায়।