Netlify Functions’ta CAPTCHA Nasıl Çözülür (Node.js SDK)

Netlify Functions’ta bir CAPTCHA çözümü, isteğe yanıt veren senkron function’ın içinde çalışamaz. Netlify bu function’ı 60 saniyede durdurur ve sınırı yükseltmenize izin vermez; bir reCAPTCHA çözümü ise dakikalar sürebilir. Bunun yerine çözümü 15 dakikalık süresi olan bir arka plan function’ında yapın; formu da bu function göndersin ve sonucu, ikinci bir function’ın raporlayabilmesi için Netlify Blobs’a kaydetsin. CapSkip Node.js SDK’sını Server modundaki çözücünüze yöneltin, çünkü bir Netlify function’ının içindeki 127.0.0.1 sizin makineniz değil, Netlify’ın makinesidir. Ardından yeniden denemeleri ele alın: hata fırlatan bir arka plan function’ı yeniden çalışır ve özensiz yazılmış bir function formu iki kez gönderir.
Neye ihtiyacınız var
- Kontrol ettiğiniz bir Windows makinesinde, Server modunda, statik bir genel IP ile ve çözücünün portu internetten erişilebilir durumda çalışan CapSkip. Server modu bağlantı ayarları bölümünde yer alır; geri kalanını 1. adım anlatıyor.
- Function’ları netlify/functions içinde bulunan ve @netlify/blobs paketinin gerektirdiği Node.js 22.12 veya üzeriyle derlenen bir Netlify sitesi. Netlify function’ları, derlemenizin kullandığı Node.js sürümünde çalıştırır. Arka plan function’ları, Free dahil kredi tabanlı tüm planlarda bulunur; eski (legacy) planlarda durum farklıdır, bu yüzden kendi planınızı kontrol edin.
- capskip paketinin 1.3.0 veya üzeri bir sürümü; ayrıca iş kayıtları için @netlify/blobs ve sayfayı okumak için cheerio. Bir function’ın içinde Blobs hiçbir kurulum gerektirmez: site ve token bilgisini Netlify sizin yerinize doldurur.
- CAPTCHA’nın bulunduğu sayfanın URL’si. Örnekler reCAPTCHA v2 çözer; aynı yapı CapSkip’in desteklediği her tür için çalışır.
# npm install capskip @netlify/blobs cheerio npm install capskip @netlify/blobs cheerio
Senkron bir function bunu neden yapamaz
Netlify her function türüne sabit bir yürütme süresi sınırı verir ve bunları function yapılandırma belgelerinde listeler. Üçünden hiçbiri değiştirilemez:
| Function türü | Yürütme süresi sınırı | Bu kurulumdaki görevi |
|---|---|---|
| Senkron | 60 saniye | Bir işin sonucunu raporlamak |
| Zamanlanmış | 30 saniye | Bir işi zamanlayıcıyla başlatmak |
| Arka plan | 15 dakika | Çözmek, ardından formu göndermek |
Şimdi bunu Netlify Functions’taki bir CAPTCHA çözümüyle karşılaştırın. SDK bir reCAPTCHA cevabı için recaptchaTimeout süresi boyunca, yani varsayılan olarak 300 saniyeye kadar bekler; CapSkip’in kendisi de bir görevin boş bir thread için 250 saniye beklemesine ve çözüm için 250 saniye daha harcamasına izin verir. Sakin bir öğleden sonra 20 saniye süren bir çözüm, tüm thread’ler meşgulken ya da bir proxy yavaşken 90 saniye sürer. Senkron bir function 60 saniyeye ulaştığında Netlify onu sonlandırır. CapSkip bunu bilmez; bu yüzden işi sonuna kadar yürütür ve cevabı hiç kimse almaz.
context.waitUntil bir çıkış yolu gibi görünür, çünkü yanıt gittikten sonra da function’ı çalışır durumda tutar. Ama değildir. Netlify’ın belgelerine göre function, asenkron işler dahil, yine yalnızca yürütme süresi sınırına kadar çalışabilir; dolayısıyla waitUntil’e devredilen bir çözüm de aynı şekilde 60 saniyede ölür. Streaming yanıtlar da bu 60 saniyelik sınırı paylaşır.
Bir arka plan function’ı çağırana hemen 202 ile yanıt verir ve 15 dakikaya kadar çalışmaya devam eder. Bedeli de bu 202’de gizlidir: function’ın dönüş değerini hiç kimse almaz. Üstelik bir reCAPTCHA token’ı verildikten yaklaşık iki dakika sonra geçerliliğini yitirir, bu yüzden bir çağıranın gelip onu almasını bekleyerek duramaz. Arka plan function’ı token’ı formu göndererek kendisi kullanmalı ve olup biteni bir kayda geçirmelidir.
1. Adım: SDK’yı Server modundaki çözücünüze yöneltin
Bir Netlify function’ının içinde 127.0.0.1, function’ın kendi sandbox’ıdır. Varsayılan ayarlarla kurulan bir istemci orada hiçbir şeye bağlanamaz ve ECONNREFUSED ile NetworkException fırlatır. CapSkip’i Server moduna alın ki genel IP’nizi dinlesin; Windows makinesi bir router’ın arkasındaysa portu ona yönlendirin ve portun Windows Firewall’dan geçmesine izin verin.
Ardından kimin bağlanabileceğine karar verin. Varsayılan olarak Netlify function’larının bağlandığı adresler Netlify ölçeklendikçe değişir, bu yüzden bir güvenlik duvarı kuralı bunları listeleyemez. Netlify’ın Private Connectivity özelliği function’lara izin verebileceğiniz sabit bir IP kümesi sağlar, ancak bu özellik Enterprise planlar için sunulan bir eklentidir. Bu özellik olmadan kilit, CapSkip’in API Key Validation ayarıdır: ayarı açın, bu site için bir anahtar ekleyin ve onu apiKey olarak verin. Her SDK isteği anahtarı, çağrının geri kalanı gibi düz HTTP üzerinden taşır; bu yüzden function’a, başka hiçbir şeyi bozmadan silebileceğiniz kendine ait bir anahtar verin.
Adresi ve anahtarı Netlify’da Project configuration, ardından Environment variables altında, Functions’ı içeren bir kapsamla ortam değişkenleri olarak saklayın. Burada iki Netlify kuralı insanları tuzağa düşürür. netlify.toml içinde tanımlanan değişkenler function’lara hiçbir zaman ulaşmaz. Ayrıca her dağıtım, derlendiği sırada ayarlı olan değerleri korur; bu yüzden yeni bir CAPSKIP_HOST, siz yeniden dağıtım yapana kadar hiçbir şeyi değiştirmez.
import { CapSkip } from "capskip";
// The SDK does not read these by itself, so pass them in.
if (!process.env.CAPSKIP_HOST) throw new Error("CAPSKIP_HOST is not set");
const solver = new CapSkip({
apiKey: process.env.CAPSKIP_API_KEY, // a key from API Key Validation
host: process.env.CAPSKIP_HOST, // your public IP, Server mode
port: Number(process.env.CAPSKIP_PORT ?? 8080),
});Bu kontrolü koruyun. Değişken yoksa host undefined olur ve SDK tek kelime etmeden varsayılan değeri olan 127.0.0.1’e döner; bu da sizi doğrudan ECONNREFUSED hatasına geri götürür. Tam örnekte bu kontrol çözümün try bloğunun içinde durur; böylece eksik bir değişken, function’ı daha bir kayıt yazamadan durdurmak yerine iş kaydına düşer.
2. Adım: bir arka plan function’ında çözün ve gönderin
Bir arka plan function’ı oluşturmak için function’ın config nesnesinde background değerini true yapmanız yeterlidir. Bu function paylaşılan bir gizli değeri kontrol eder, çünkü URL’si herkese açıktır ve ona gelen her istek çözücünüzü çalıştırır. Ardından sayfayı çeker, sitekey’i okur ve çözer:
// netlify/functions/solve-signup.mjs
import { getStore } from "@netlify/blobs";
import * as cheerio from "cheerio";
// solver from Step 1; PAGE_URL is the page with the CAPTCHA.
export default async (req) => {
if (req.headers.get("x-job-secret") !== process.env.JOB_SECRET) return;
const { jobId, email } = await req.json();
const jobs = getStore({ name: "captcha-jobs", consistency: "strong" });
const page = await fetch(PAGE_URL);
const $ = cheerio.load(await page.text());
const result = await solver.recaptcha($(".g-recaptcha").attr("data-sitekey"), PAGE_URL);
// Then claim the job and post the form, below.
};
export const config = { path: "/api/solve-signup", method: "POST", background: true };reCAPTCHA için cevap result.code içindedir. Onu hemen kullanın: iki dakikalık ömrü yüzünden çözümü yapan çalıştırma, formu gönderen çalıştırmanın ta kendisidir.
Şimdi yeniden denemelere gelelim. Bir arka plan function’ı hatayla sona erdiğinde Netlify onu bir dakika sonra yeniden çalıştırır; bu da başarısız olursa iki dakika sonra bir kez daha. Form gönderilmeden önce yeniden deneme tam olarak istediğiniz şeydir: taze bir sayfa ve taze bir çözüm. Form gönderildikten sonra ise yeniden deneme ikinci bir üyelik kaydı demektir. Bu yüzden function, formu göndermeden önce işi Blobs’ta sahiplenir:
// Only one run can create this key, so only one run posts.
const claim = await jobs.setJSON(`${jobId}-posted`, { at: Date.now() }, { onlyIfNew: true });
if (!claim.modified) return;
const form = $("form").has(".g-recaptcha");
const body = new URLSearchParams(form.serializeArray().map((f) => [f.name, f.value]));
body.set("email", email);
body.set("g-recaptcha-response", result.code);
const cookie = page.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
const res = await fetch(new URL(form.attr("action") || PAGE_URL, PAGE_URL), {
method: "POST",
headers: { cookie, referer: PAGE_URL },
body,
});
await jobs.setJSON(jobId, { state: "done", status: res.status });onlyIfNew ile yazma işlemi yalnızca bu Blobs anahtarı henüz yoksa başarılı olur; modified ise anahtarı bu çalıştırmanın oluşturup oluşturmadığını söyler. Aynı iş için yarışan iki çalıştırmanın ikisi birden true alamaz, bu yüzden formu yalnızca biri gönderir. Function başlamadan önce de bu anahtarı kontrol eder; böylece bitmiş bir işin yeniden çalıştırılması bir çözüm harcamadan geri döner.
Gönderimin kendisi sayfanın çerezlerini ve gizli bir CSRF token’ı da dahil formun kendi alanlarını gönderir; kayıt formlarının çoğu CAPTCHA’nın yanında bunları kontrol eder. Sayfayı Referer olarak da gönderir, çünkü fetch hiç Referer göndermez ve bazı framework’ler Referer olmayan bir HTTPS form gönderimini reddeder. İş bir kez sahiplenildikten sonra hata fırlatmak yerine her hatayı Blobs’a kaydedin. Yeniden deneme sahiplenme kaydında durur, dolayısıyla hata fırlatmak hiçbir şey kazandırmaz; durum endpoint’inizin hatayı gösterebileceği tek yer de bu kayıttır. Aşağıdaki tam örnek iki aşamayı da bu şekilde sarar.
3. Adım: işleri başlatın ve sonuçlarını okuyun
Herhangi bir sunucu tarafı koddan arka plan function’ına POST isteği göndererek bir iş başlatın. İş kimliğini çağıran kendisi üretir, çünkü 202 yanıtında kimliği geri taşıyacak bir gövde yoktur:
const jobId = crypto.randomUUID();
const start = await fetch("https://YOUR_SITE.netlify.app/api/solve-signup", {
method: "POST",
headers: { "x-job-secret": process.env.JOB_SECRET },
body: JSON.stringify({ jobId, email: "YOUR_EMAIL" }),
});
console.log(start.status); // 202: accepted, not solved yetKaydı küçük bir senkron function raporlar. Milisaniyeler içinde çalışır ve 60 saniyelik sınırın çok altında kalır:
// netlify/functions/job-status.mjs
import { getStore } from "@netlify/blobs";
export default async (req, context) => {
const jobs = getStore({ name: "captcha-jobs", consistency: "strong" });
const job = await jobs.get(context.params.id, { type: "json" });
if (!job) return new Response("unknown or not started", { status: 404 });
return Response.json(job);
};
export const config = { path: "/api/jobs/:id", method: "GET" };Onu birkaç saniyede bir yoklayın. Bir iş bitene ya da denemelerinden biri başarısız olana kadar kaydı olmaz; dolayısıyla 404, işin ilk denemesinin hâlâ çalıştığı ya da gizli değer eşleşmediği için işin hiç başlamadığı anlamına gelir. Burada olduğu gibi güçlü tutarlılıkla (strong consistency) okuyun. Blobs varsayılan olarak nihai tutarlılık (eventual consistency) sunar: yeni bir kayıt hemen görünür, ama bir güncellemenin her edge konumuna ulaşması 60 saniyeyi bulabilir; bu süre, o arada bitmiş bir işi hâlâ yeniden deneniyor gibi göstermeye yeter.
İşi bir zamanlayıcıyla çalıştırmak için zamanlanmış bir function kullanın, ama sınırına dikkat edin: 30 saniye, yani senkron function sınırının yarısı. İşi arka plan function’ına devretsin ve bir saniyeden çok daha kısa sürede bitsin:
// netlify/functions/nightly-signup.mjs
export default async () => {
const res = await fetch(`${process.env.URL}/api/solve-signup`, {
method: "POST",
headers: { "x-job-secret": process.env.JOB_SECRET },
body: JSON.stringify({ jobId: crypto.randomUUID(), email: "YOUR_EMAIL" }),
});
console.log("queued:", res.status); // 202 means accepted, not solved
};
export const config = { schedule: "@daily" };URL, Netlify’ın çalışma zamanında function’lara verdiği salt okunur değişkenlerden biridir: sitenizin ana adresi. Zamanlanmış function’lar yalnızca yayınlanmış dağıtımlarda tetiklenir; Deploy Previews veya branch dağıtımlarında tetiklenmez.
Tam çalışan örnek
// npm install capskip @netlify/blobs cheerio
// netlify/functions/solve-signup.mjs
import { getStore } from "@netlify/blobs";
import * as cheerio from "cheerio";
import { CapSkip } from "capskip";
const PAGE_URL = "https://example.com/signup";
export default async (req) => {
// Anyone can POST to this URL, so check a shared secret first.
if (req.headers.get("x-job-secret") !== process.env.JOB_SECRET) return;
const { jobId, email } = await req.json();
const jobs = getStore({ name: "captcha-jobs", consistency: "strong" });
if (await jobs.get(`${jobId}-posted`)) return; // already posted once
// Phase 1: fetch and solve. Throwing here is safe: Netlify runs
// the function again after one minute, then two minutes later.
let page, $, result;
try {
if (!process.env.CAPSKIP_HOST) throw new Error("CAPSKIP_HOST is not set");
const solver = new CapSkip({
apiKey: process.env.CAPSKIP_API_KEY,
host: process.env.CAPSKIP_HOST,
port: Number(process.env.CAPSKIP_PORT ?? 8080),
});
page = await fetch(PAGE_URL);
$ = cheerio.load(await page.text());
result = await solver.recaptcha($(".g-recaptcha").attr("data-sitekey"), PAGE_URL);
} catch (err) {
const prev = await jobs.get(jobId, { type: "json" });
const attempt = (prev?.attempt ?? 0) + 1;
// The first run plus two retries: after the third, nothing reruns.
const state = attempt < 3 ? "retrying" : "failed";
await jobs.setJSON(jobId, { state, attempt, error: String(err) });
throw err;
}
// Phase 2: post the form once. Claim the job first, so a rerun
// that reaches this line finds the claim taken and stops.
const claim = await jobs.setJSON(`${jobId}-posted`, { at: Date.now() }, { onlyIfNew: true });
if (!claim.modified) return;
try {
const form = $("form").has(".g-recaptcha");
const body = new URLSearchParams(form.serializeArray().map((f) => [f.name, f.value]));
body.set("email", email);
body.set("g-recaptcha-response", result.code);
const cookie = page.headers.getSetCookie().map((c) => c.split(";")[0]).join("; ");
const res = await fetch(new URL(form.attr("action") || PAGE_URL, PAGE_URL), {
method: "POST",
headers: { cookie, referer: PAGE_URL },
body,
});
await jobs.setJSON(jobId, { state: "done", status: res.status });
} catch (err) {
// No rethrow: a retry could not post again, so record it here.
await jobs.setJSON(jobId, { state: "failed", error: String(err) });
}
};
export const config = { path: "/api/solve-signup", method: "POST", background: true };Netlify UI’da JOB_SECRET, CAPSKIP_HOST, CAPSKIP_API_KEY ve değiştirdiyseniz CAPSKIP_PORT değişkenlerini ayarlayın, dağıtımı yapın ve 3. adımdaki gibi bir iş başlatın. reCAPTCHA çağrısının aldığı her seçenek, invisible ve Enterprise dahil, burada değişmeden çalışır; bu türün neye ihtiyaç duyduğunu reCAPTCHA v2 çözücü sayfası anlatıyor.
Sık görülen hatalar ve anlamları
| Gördüğünüz | Neden | Düzeltme |
|---|---|---|
| İstek yaklaşık bir dakika sonra başarısız oluyor ve hiçbir form gönderilmiyor | Çözüm senkron bir function’da ya da onun 60 saniyelik sınırını paylaşan waitUntil içinde çalışıyor | Çözümü bir arka plan function’ına taşıyın |
| CAPSKIP_HOST is not set diyen bir iş kaydı ya da o kontrolü kaldırdıysanız ECONNREFUSED 127.0.0.1:8080 ile NetworkException | Dağıtılan function’da CAPSKIP_HOST eksik, bu yüzden SDK loopback’e geri dönerdi | Değişkeni Functions’ı içeren bir kapsamla ayarlayın, ardından yeniden dağıtın |
| netlify dev ile çalışıyor, dağıtıldıktan sonra başarısız oluyor | Yerelde function kendi makinenizde çalışır; orada loopback ve yerel IP’niz çözücüye ulaşır | Dağıtılan site için Server modunu ve genel IP’nizi kullanın |
| netlify.toml içindeki bir değişken function’da undefined oluyor | netlify.toml içinde tanımlanan değişkenler function’lara hiçbir zaman ulaşmaz | Bunun yerine değişkeni Netlify UI, CLI veya API üzerinden ayarlayın |
| Function hâlâ eski bir CAPSKIP_HOST kullanıyor | Bir dağıtım, derlendiği sırada ayarlı olan değerleri korur | Bir değişkeni değiştirdikten sonra yeniden dağıtın |
| Takılı kalan bir bağlantı, ardından ETIMEDOUT ile NetworkException | Port yönlendirilmemiş ya da Windows Firewall bağlantıyı düşürüyor | Portu yönlendirin, Windows Firewall’dan geçmesine izin verin ve ağınızın dışından test edin |
| ERROR_KEY_DOES_NOT_EXIST diyen bir ApiException | API Key Validation açık ve anahtar CapSkip’in listesinde yok ya da CAPSKIP_API_KEY ayarlanmamış ve SDK varsayılan anahtarını gönderdi | Anahtarı CapSkip’e ekleyin ve değişkeni ayarlayın |
| Form iki kez gönderildi | Gönderimden sonraki bir hata yeniden denemeyi tetikledi ve ikinci çalıştırmayı hiçbir şey durdurmadı | 2. adımdaki gibi, göndermeden önce işi onlyIfNew ile sahiplenin |
| Durum endpoint’i sürekli 404 döndürüyor | Gizli değer eşleşmedi, bu yüzden function hiçbir şey yazmadan geri döndü | Çağıranda ve sitede aynı JOB_SECRET değerini ayarlayın |
| Site token’ı reddediyor | Token yaklaşık iki dakikadan eskiydi ya da zaten kullanılmıştı | Çözdükten hemen sonra gönderin, her gönderim için bir token |
FAQ
Netlify Functions’ta bir CAPTCHA çözümü için 60 saniyelik sınırı yükseltebilir miyim?
Hayır. Netlify senkron, zamanlanmış ve arka plan sınırlarını sabit olarak listeler; waitUntil ve streaming yanıtlar da 60 saniyenin içinde kalır. Sunulan uzun sınır, arka plan function’larının 15 dakikalık sınırıdır ve SDK’nın 300 saniyelik beklemesini fazlasıyla karşılar.
Bir Netlify function’ı, evimdeki ya da ofisimdeki bilgisayarda çalışan CapSkip’e ulaşabilir mi?
Evet, Server modu sayesinde. CapSkip genel IP’nizi dinler, router’ınız portu o bilgisayara yönlendirir ve function, yerelde kullanacağı aynı HTTP API üzerinden bağlanır. Statik bir genel IP, CAPSKIP_HOST değerini dağıtımlar arasında geçerli tutar. Private Connectivity olmadan Netlify’a adres bazında izin veremezsiniz; bu yüzden kapı bekçiliğini API Key Validation yapar.
Netlify çağırdığında CapSkip çözüm başına ücret alır mı?
Hayır. Server modu, çözücüyü kimin çalıştırdığını değil, ona nereden erişilebildiğini değiştirir: çözücü yine sizin kendi Windows makinenizdir ve çözümleri saymaz. Netlify ise function çalışma süresini ölçer; dolayısıyla bir çözüm için iki dakika bekleyen bir arka plan function’ı bu sürenin iki dakikasını kullanır.
Bunun AWS Lambda’da çalıştırmaktan farkı nedir?
Kısıtlar farklıdır. API Gateway arkasındaki Lambda’da duvar, 29 saniyelik entegrasyon zaman aşımıdır ve Elastic IP’li bir NAT gateway, her çözüme güvenlik duvarınız için tek bir sabit kaynak adres verir. Netlify’da duvar 60 saniyedir, arka plan function’ı bu duvarı aşmanın yerleşik yoludur ve bir Enterprise eklentisi olmadan sabit bir adres yoktur. Lambda kurulumu şurada: AWS Lambda CAPTCHA rehberi.
Kısa özet
Netlify Functions’taki bir CAPTCHA işi için asla senkron bir function’da çözüm yapmayın: 60 saniyelik sınırı sabittir ve waitUntil bu sınırı aşamaz. Çözümü bir arka plan function’ında yapın, token tazeyken formu aynı çalıştırmada gönderin ve Netlify’ın iki yeniden denemesi formu asla iki kez gönderemesin diye işi önce onlyIfNew ile yapılan bir yazmayla sahiplenin. Sonuçları Blobs’tan küçük bir senkron function aracılığıyla raporlayın. CapSkip’e Server modunda bağlanın; adres ve anahtar Functions kapsamlı değişkenlerde dursun, API Key Validation da açık olsun.
- Node paketinin çözdüğü diğer tüm CAPTCHA türleri: Node.js CAPTCHA çözücü sayfası.
Function’ları Netlify sağlar, çözme işi ise sizin sahip olduğunuz bir Windows makinesinde kalır. Kendinize ait bir captcha çözücü çalıştırmanın amacı da budur: platformun sayacı dakikaları sayar, çözümleri ise hiçbir şey saymaz.
