Inngest में दो बार हल किए बिना कैप्चा कैसे हल करें

Inngest में कैप्चा हल करने का काम एक ही step.run कॉल के अंदर रहना चाहिए, और token का इस्तेमाल भी उसी कॉल के अंदर होना चाहिए। Inngest हर step सीमा पर आपके फ़ंक्शन को ऊपर से दोबारा चलाता है, इसलिए किसी step के बाहर रखी हर चीज़ हर बार फिर से चलती है। और पूरा हो चुका step memoize हो जाता है, इसलिए पहले step में हल किया गया token चौथे step में बिना किसी बदलाव के दोहरा दिया जाता है, उसके expire होने के काफ़ी बाद। दोनों नियम सॉल्वर से नहीं, execution मॉडल से निकलते हैं।
आपको क्या चाहिए
- serve endpoint वाला कोई Inngest ऐप, जो आमतौर पर /api/inngest पर होता है, और लोकल runs के लिए Inngest Dev Server।
- किसी Windows मशीन पर चलता हुआ CapSkip, और उस ऐप में इंस्टॉल किया हुआ Node client जो आपके फ़ंक्शन serve करता है।
- sitekey और page URL, जो फ़ंक्शन में पक्के तौर पर लिखे होने के बजाय event payload पर आएँ।
- Server mode, जब भी ऐप सॉल्वर की अपनी मशीन के अलावा कहीं और डिप्लॉय हो, यानी ज़्यादातर डिप्लॉयमेंट में। यह कनेक्शन सेटिंग्स के अंदर बस एक सेटिंग है।
# npm install capskip npm install inngest capskip
चरण 1: solve किसी step के अंदर क्यों होना चाहिए
Inngest आपके फ़ंक्शन को एक बार चलाकर ऊपर से नीचे नहीं उतरता। वह फ़ंक्शन चलाता है, पहले step पर रुकता है, नतीजा दर्ज करता है, और फिर पिछली execution की state जोड़कर फ़ंक्शन को ऊपर से दोबारा invoke करता है। उसका अपना डॉक्यूमेंटेशन दूसरे पास को साफ़ शब्दों में बताता है: step का कोड चलाया नहीं जाता, बल्कि SDK उस नतीजे को step.run की return value में डाल देता है।
यही पूरा मॉडल है, और इसका एक नतीजा बाक़ी सबसे ज़्यादा मायने रखता है। जो कोड किसी step के बाहर बैठा है वह memoize नहीं होता, इसलिए वह हर invocation पर चलता है। चार step वाला फ़ंक्शन आपके handler को चार बार invoke करता है, यानी steps के ऊपर लिखा गया solve एक ही फ़ंक्शन run के लिए चार बार चलता है।
// npm install capskip
// WRONG. This line runs once per step boundary, so a
// four-step function solves four CAPTCHAs for one run.
const result = await solver.recaptcha(sitekey, pageUrl);
await step.run("fetch-form", async () => { /* ... */ });
await step.run("submit", async () => { /* ... */ });Inngest यह नियम सीधे बताता है: कोई भी non-deterministic तर्क, जैसे database कॉल या API कॉल, किसी step.run कॉल के अंदर ही रखा जाना चाहिए। solve एक API कॉल है, इसलिए उसकी जगह उसी के अंदर है। मीटर वाले सॉल्वर के साथ वह ग़लती एक बिल की शक्ल में दिखती है। लोकल सॉल्वर के साथ वह चार गुना काम और चार tokens की शक्ल में दिखती है, जिनमें से तीन फेंक दिए जाते हैं।
चरण 2: एक ही step में हल कीजिए और सबमिट कीजिए
दूसरा नियम कम स्पष्ट है और बाद में काटता है। जैसे ही कोई step पूरा होता है, उसकी return value स्टोर हो जाती है और हर अगली invocation पर दोहरा दी जाती है। step.run से लौटाया गया सारा डेटा JSON के रूप में serialize होता है, और state जिस चीज़ के आधार पर memoize होती है वह step id है।
तो हल करने वाले step से लौटाया गया token एक स्टोर की हुई string है। वह अगली invocation पर और उसके बाद वाली पर भी हूबहू वैसा ही लौटता है, और तब तक वह कई मिनट पुराना हो सकता है। reCAPTCHA token लगभग दो मिनट तक चलता है। solve और submit के बीच पड़ने वाली हर चीज़ उस खिड़की को खाती है: कोई sleep, कोई धीमा fetch, या कोई ऐसा step जिसने backoff के साथ कुछ बार retry किया। इनमें से कोई भी उस खिड़की को पूरी तरह ख़त्म कर सकता है।
// WRONG. The token is memoized here and replayed later,
// by which time it has almost certainly expired.
const token = await step.run("solve", () =>
solver.recaptcha(sitekey, pageUrl).then((r) => r.code)
);
await step.sleep("settle", "5m");
await step.run("submit", () => postForm(token));उन्हें साथ रखिए। एक ही step हल करता है और सबमिट करता है, और सिर्फ़ वही लौटाता है जिसकी बाक़ी फ़ंक्शन को ज़रूरत है, और वह लगभग कभी token ख़ुद नहीं होता।
// RIGHT. The token is born and spent inside one step,
// so nothing expired is ever replayed.
const outcome = await step.run("solve-and-submit", async () => {
const { code } = await solver.recaptcha(sitekey, pageUrl);
const res = await postForm(pageUrl, code);
return { status: res.status, id: res.id };
});वही समय सीमा उन लोगों को भी पकड़ती है जो queue jobs को एक के बाद एक जोड़ते हैं, और इसे एक बार पढ़ लेना फ़ायदेमंद है: reCAPTCHA token कितनी देर चलता है.
चरण 3: retries, और किन errors को इसका हक़ है
Inngest पहली कोशिश के अलावा किसी फ़ंक्शन या step को चार बार retry करता है, और हर step.run का अपना स्वतंत्र retry काउंटर होता है। Retries jitter वाले exponential backoff का इस्तेमाल करती हैं। आप retries विकल्प को शून्य से बीस तक कहीं भी सेट कर सकते हैं।
जोड़े गए solve और submit वाले step के लिए वे डिफ़ॉल्ट लगभग सही हैं, क्योंकि retry किया गया step अपना कोड दोबारा चलाता है और इसलिए दोबारा हल भी करता है। विरासत में लेने के लिए कोई बासी token बचता ही नहीं। ट्यून करने लायक़ बात यह है कि किन विफलताओं को retry मिले ही।
| कौन-सा exception | इसका क्या मतलब है | retry के लायक़? |
|---|---|---|
| NetworkException | CapSkip तक पहुँच नहीं बनी, या वह रीस्टार्ट हो रहा है | हाँ। retries इसी के लिए हैं |
| TimeoutException | Polling client की अपनी अधिकतम सीमा से आगे निकल गई | एक बार, शायद। चार बार शायद ही कभी |
| ApiException | API ने कोई error कोड लौटाया | एरर कोड पर निर्भर है। आमतौर पर नहीं |
| ValidationException | पैरामीटर ग़लत थे और फिर से ग़लत ही रहेंगे | नहीं। NonRetriableError फेंकें |
import { NonRetriableError } from "inngest";
import { ValidationException } from "capskip";
const outcome = await step.run("solve-and-submit", async () => {
try {
const { code } = await solver.recaptcha(sitekey, pageUrl);
return await postForm(pageUrl, code);
} catch (err) {
// A bad sitekey will be bad on all five attempts.
if (err instanceof ValidationException) {
throw new NonRetriableError(err.message);
}
throw err; // everything else gets the normal backoff
}
});NonRetriableError बची हुई retries को छोड़ देता है और जिस step से उसे फेंका गया था उसे फ़ेल कर देता है, और ऐसी request के लिए आपको यही चाहिए जो बदक़िस्मत नहीं बल्कि ख़राब बनी हुई थी। अगर सॉल्वर ने बताया कि वह टूटा नहीं बल्कि व्यस्त है, तो RetryAfterError आपको डिफ़ॉल्ट वक्र लेने के बजाय ख़ुद देरी तय करने देता है।
चरण 4: पूरा फ़ंक्शन
ऊपर की हर बात, एक ही फ़ाइल में। ध्यान दीजिए कि client कहाँ बनाया गया है: handler के बाहर, ताकि वह हर invocation के बजाय हर प्रोसेस में एक बार बने, और वह किसी run की state अपने पास नहीं रखता।
// npm install capskip
import { Inngest } from "inngest";
import { CapSkip } from "capskip";
export const inngest = new Inngest({ id: "signup-worker" });
// CAPSKIP_HOST is 127.0.0.1 locally and the solver machine
// once this app is deployed anywhere else.
const solver = new CapSkip({
host: process.env.CAPSKIP_HOST ?? "127.0.0.1",
port: 8080,
});
export const submitSignup = inngest.createFunction(
{ id: "submit-signup", retries: 4 },
{ event: "signup/requested" },
async ({ event, step }) => {
const { sitekey, pageUrl, email } = event.data;
// One step. The token never leaves it.
const outcome = await step.run("solve-and-submit", async () => {
const { code } = await solver.recaptcha(sitekey, pageUrl);
return postSignup(pageUrl, email, code);
});
await step.run("record", () => saveResult(email, outcome));
return outcome;
}
);वह कॉल reCAPTCHA v2 है। बाक़ी प्रकार भी उसी आकार के हैं: invisible या enterprise को 1 सेट करके भेजिए, या version को किसी action के साथ v3 सेट कीजिए, या उसकी जगह turnstile या geetest कॉल कीजिए। पूरा surface यहाँ है: Node.js कैप्चा सॉल्वर पेज.
चरण 5: आपका कोड असल में कहाँ चलता है, और उसके लिए कौन-सा मोड चाहिए
Inngest ज़्यादातर hosted ऑटोमेशन प्लेटफ़ॉर्म से एक ऐसे तरीक़े से अलग है जो इस सेक्शन का फ़ैसला करता है। आपके फ़ंक्शन Inngest के इंफ़्रास्ट्रक्चर पर नहीं चलते। Inngest किसी serve endpoint पर, आमतौर पर /api/inngest पर, HTTP के ज़रिए आपके एप्लिकेशन को कॉल करता है, और आपका कोड आपके अपने ऐप के अंदर चलता है। इसलिए यह सवाल कि “क्या यह 127.0.0.1 तक पहुँच सकता है”, Inngest से नहीं, पूरी तरह इस बात से जुड़ा है कि आपने डिप्लॉय कहाँ किया।
| ऐप कहाँ डिप्लॉय है | कौन-सा कनेक्शन मोड |
|---|---|
| लोकल रूप से, Dev Server के साथ, CapSkip वाली मशीन पर | Local mode। 127.0.0.1 सचमुच सही है |
| आपके अपने server पर या आपके नेटवर्क की किसी VM पर | सॉल्वर के LAN पते के साथ Server mode |
| Vercel या Lambda जैसे किसी serverless होस्ट पर | स्टैटिक पब्लिक IP और एक फ़ायरवॉल नियम के साथ Server mode |
| ऐप के बग़ल वाले कंटेनर में, सॉल्वर कहीं और | Server mode। कंटेनर के अंदर loopback यानी वही कंटेनर |
दो कनेक्शन मोड होते हैं। Local 127.0.0.1 पर bind होता है और सिर्फ़ उसी डिवाइस को जवाब देता है। Server आपके नेटवर्क address या पब्लिक IP पर bind होता है, ताकि कोई दूसरा बॉक्स, कोई कंटेनर होस्ट या कोई serverless फ़ंक्शन उसी Windows मशीन तक API के ज़रिए पहुँच सके। दोनों एक ही जगह मिलते हैं: कनेक्शन सेटिंग्स, और Server mode सिर्फ़ यह बदलता है कि सॉल्वर किस address पर सुनता है। hardware अब भी आपका ही है और हल अब भी बिना मीटर वाला है।
मीटर पर न गिना जाना ही दिन भर चलने वाले फ़ंक्शन को चलाने लायक़ बनाता है।
एक बात जिसमें आसानी से घालमेल हो जाता है: Inngest Cloud को भी आपके serve endpoint तक पहुँचना होता है, और वह सॉल्वर से अलग नेटवर्किंग का मामला है। जिस डिप्लॉयमेंट को Inngest पहले से कॉल कर पाता है, ज़रूरी नहीं कि वह आपके LAN को भी कॉल कर सके।
आम errors और उनका मतलब
| आप जो देखते हैं | कारण | फिक्स |
|---|---|---|
| एक ही फ़ंक्शन run के लिए कई solves दर्ज | solve step.run के बाहर है, इसलिए हर सीमा पर दोहराता है | उसे किसी step के अंदर ले जाएँ |
| जो token ठीक से हल हुआ था, उसी पर submit फ़ेल होता है | memoize किया गया token expire होने के बाद दोहराया गया | एक ही step में हल करें और सबमिट करें |
| कोई step चार बार retry करता है और हर बार एक जैसा फ़ेल होता है | पैरामीटर की ग़लती को अस्थायी मानकर चलाया जा रहा है | ValidationException के लिए NonRetriableError फेंकें |
| डिप्लॉय करने के बाद हर run पर NetworkException | ऐप सॉल्वर की मशीन से हट गया | Server mode, और डिप्लॉयमेंट में CAPSKIP_HOST सेट करें |
| दो डिप्लॉय के बीच किसी step के नतीजे का आकार बदल गया | step का आउटपुट JSON के रूप में serialize होता है और id से मिलाया जाता है | जब return value बदले तो step id का नाम बदल दें |
| ख़ुद लिखे poll से CAPCHA_NOT_READY सामने आना | नतीजा पूरा होने से पहले ही पढ़ लिया गया | client को poll करने दें। वह ख़ुद ही धीमा होता जाता है |
| किसी ApiException के अंदर ERROR_WRONG_USER_KEY | डिप्लॉयमेंट environment में CAPSKIP_API_KEY सेट नहीं है | उसे वहाँ सेट करें जहाँ ऐप चलता है, फिर दोबारा डिप्लॉय करें |
छठी पंक्ति वाली दिक़्क़त लोगों को सबसे पहले तब मिलती है जब वे client इस्तेमाल करने के बजाय अपना polling loop ख़ुद लिखते हैं। उस response में जो स्पेलिंग है वह हमारी तरफ़ की टाइपो नहीं है, क्योंकि API सचमुच उसे उसी तरह लौटाता है। इसे पूरी तरह यहाँ समझाया गया है: CAPCHA_NOT_READY गाइड.
FAQ
क्या मैं token को किसी step से लौटाकर बाद में इस्तेमाल कर सकता हूँ?
कर सकते हैं, और वह टेस्टिंग में काम करेगा और प्रोडक्शन में फ़ेल होगा। वह value स्टोर हो जाती है और हर बाद वाली invocation पर दोहरा दी जाती है, इसलिए जिस पल दोनों steps के बीच कोई धीमी चीज़ आ बैठती है, आप ऐसा token सबमिट कर रहे होते हैं जो फ़ंक्शन के इंतज़ार करते करते expire हो चुका था। solve और token को खपाने वाली चीज़ को एक ही step में रखिए, और credential के बजाय नतीजा लौटाइए।
क्या retry नया कैप्चा हल करता है या पुराना ही दोबारा इस्तेमाल करता है?
नया। जो step फ़ेल हुआ है वह memoize नहीं होता, इसलिए retry उसके अंदर का कोड दोबारा चलाता है और उसमें हल करने वाली कॉल भी शामिल है। हर step.run अपना स्वतंत्र retry काउंटर रखता है, इसलिए एक गड़बड़ step दूसरों का बजट ख़र्च नहीं करता। ठीक इसी वजह से जोड़ा हुआ step retry के लिए सुरक्षित है और बँटा हुआ नहीं।
मेरा ऐप Vercel पर है। क्या वह फिर भी मेरी मेज़ पर रखे सॉल्वर तक पहुँच सकता है?
हाँ, Server mode के साथ। फ़ंक्शन Vercel के sandbox में चलता है, इसलिए वहाँ loopback का मतलब वही sandbox है, आपकी मशीन नहीं। कनेक्शन सेटिंग्स में CapSkip को अपने पब्लिक IP पर bind कीजिए, उसके आगे एक फ़ायरवॉल नियम लगाइए जो सिर्फ़ वही आने दे जिसकी आप उम्मीद करते हैं, और प्रोजेक्ट environment में CAPSKIP_HOST सेट कीजिए। स्टैटिक पब्लिक IP की सलाह दी जाती है, ताकि address आपके नीचे से खिसके नहीं।
यह Temporal में करने से कैसे अलग है?
दोनों durable execution हैं और दोनों अलग अलग रास्तों से एक ही नियम पर पहुँचते हैं। Temporal आपके चलाए हुए worker के अंदर किसी workflow को उसके event history से दोबारा चलाता है, इसलिए नियम determinism से आता है। Inngest आपके HTTP endpoint को दोबारा invoke करता है और memoize किए गए step नतीजे उसमें डाल देता है, इसलिए नियम replay से भी आता है और उम्र बढ़ाते token से भी। Temporal वाला संस्करण यहाँ विस्तार से समझाया गया है: Temporal workflow गाइड.
संक्षेप में
solve को step.run के अंदर रखिए, कभी उसके ऊपर नहीं, क्योंकि किसी step के बाहर की हर चीज़ हर step सीमा पर फिर से चलती है। solve और token को खपाने वाली चीज़ को एक ही step में रखिए, क्योंकि पूरा हो चुका step memoize होकर दोहराया जाता है और token सिर्फ़ लगभग दो मिनट जीता है। डिफ़ॉल्ट retries को वैसे ही रहने दीजिए, लेकिन पैरामीटर की ग़लतियों के लिए NonRetriableError फेंकिए। CAPSKIP_HOST को environment से सेट कीजिए और जहाँ भी ऐप सॉल्वर की अपनी मशीन पर न हो, वहाँ CapSkip को Server mode में चलाइए।
- क्लाइंट के पीछे मौजूद कच्चे एंडपॉइंट यहाँ दस्तावेज़ित हैं: CapSkip API डॉक्यूमेंटेशन.
- checkbox चैलेंज को ख़ुद यहाँ समझाया गया है: reCAPTCHA v2 सॉल्वर पेज.
फ़ंक्शन पर concurrency limit तय करने से पहले यह तौलने लायक़ है: CapSkip एक कैप्चा बायपास टूल है जो उस hardware पर चलता है जो पहले से आपका है, इसलिए एक साथ कितने runs हल होंगे इसकी इकलौती सीमा वही मशीन है, कोई बैलेंस नहीं।
