Tutorials

SSE ile Gerçek Zamanlı CAPTCHA Çözüm Bildirimleri

Bir CAPTCHA çözümünü tarayıcıya iletmenin en ucuz yolu, çözümü hiç aramamaktır. res.php'yi beş saniyede bir sorgulayan döngü, ortalamada iki buçuk saniyelik gereksiz gecikme ve görev başına onlarca boşa istek üretir. Bunun yerine CaptchaAI'nin pingback parametresiyle sonucu kendi sunucunuza çağırtır, oradan tarayıcıya açık duran bir Server-Sent Events (SSE) bağlantısıyla iletirsiniz.

Bu rehberde akışın mimarisi, Flask ve Express için kopyala-çalıştır sunucu kodu ve yük dengeleyici arkasına geçtiğinizde kırılan noktalar var.

Mimari: pingback'ten tarayıcıya dört durak

Sunucunuz iki uç nokta barındırır: tarayıcının bağlandığı SSE akışı ve CaptchaAI'nin sonucu bıraktığı callback adresi. İkisini client_id etiketi birbirine bağlar.

[Client] ← SSE stream ← [Your Server] ← Callback ← [CaptchaAI]
   ↓                          ↑
   Submit task → [CaptchaAI] ──┘ (pingback URL points to your server)
  1. İstemci SSE uç noktanıza bağlanır (kalıcı HTTP bağlantısı)
  2. İstemci, pingback sunucunuzu işaret ederek CaptchaAI'ye bir CAPTCHA görevi gönderir
  3. CaptchaAI çözer ve sonucu callback uç noktanıza iletir
  4. Sunucunuz sonucu SSE akışı aracılığıyla istemciye geçirir

Buradaki tek CaptchaAI'ye özgü ayrıntı pingback parametresidir: görev gönderimine eklediğinizde, çözüm hazır olunca CaptchaAI verdiğiniz adrese tek bir HTTP isteği atar. Gönderim isteğinin geri kalanı — key, method, site parametreleri — hiç değişmez, yani var olan bir entegrasyona tek satır ekleyerek geçebilirsiniz. İki uç noktayı ayrı tutun: callback'i CaptchaAI dışarıdan çağırır, SSE uç noktasına ise tarayıcı bağlı kalır; ikisini tek işleyicide birleştirmek her ölçeklendirme adımını zorlaştırır.

Adım 1: Flask ile sunucu tarafı

Sunucu her istemci için ayrı bir olay kuyruğu tutar: callback geldiğinde ilgili kuyruğa yazar, SSE üreticisi o kuyruktan okuyup tarayıcıya aktarır. İki başlık kritik — X-Accel-Buffering: no nginx'in arabelleğe almasını kapatır, 30 saniyelik keepalive yorumu proxy zaman aşımını engeller.

import os
import queue
import threading
import requests
from flask import Flask, Response, request, jsonify

app = Flask(__name__)

API_KEY = os.environ["CAPTCHAAI_API_KEY"]

# Per-client event queues: client_id -> Queue
client_queues = {}
queues_lock = threading.Lock()


@app.route("/events/<client_id>")
def sse_stream(client_id):
    """SSE endpoint — clients connect here for real-time results."""
    q = queue.Queue()

    with queues_lock:
        client_queues[client_id] = q

    def generate():
        try:
            while True:
                # Block until a result arrives (timeout for keepalive)
                try:
                    data = q.get(timeout=30)
                    yield f"event: captcha-solved\ndata: {data}\n\n"
                except queue.Empty:
                    # Send keepalive comment to prevent connection timeout
                    yield ": keepalive\n\n"
        finally:
            with queues_lock:
                client_queues.pop(client_id, None)

    return Response(
        generate(),
        mimetype="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "X-Accel-Buffering": "no"  # Disable nginx buffering
        }
    )


@app.route("/submit", methods=["POST"])
def submit_captcha():
    """Submit a CAPTCHA task with callback to this server."""
    data = request.json
    client_id = data["client_id"]
    sitekey = data["sitekey"]
    pageurl = data["pageurl"]

    callback_url = f"{request.host_url}callback?client_id={client_id}"

    resp = requests.post("https://ocr.captchaai.com/in.php", data={
        "key": API_KEY,
        "method": "userrecaptcha",
        "googlekey": sitekey,
        "pageurl": pageurl,
        "pingback": callback_url,
        "json": 1
    })
    result = resp.json()

    if result.get("status") == 1:
        return jsonify({"task_id": result["request"]})
    return jsonify({"error": result.get("request")}), 400


@app.route("/callback")
def captcha_callback():
    """Receive CaptchaAI callback and push to SSE stream."""
    client_id = request.args.get("client_id")
    task_id = request.args.get("id")
    solution = request.args.get("code")

    import json
    message = json.dumps({
        "task_id": task_id,
        "solution": solution
    })

    with queues_lock:
        q = client_queues.get(client_id)
        if q:
            q.put(message)

    return "OK", 200


if __name__ == "__main__":
    app.run(port=5000, threaded=True)

method: "userrecaptcha" satırı reCAPTCHA içindir. Cloudflare Turnstile veya GeeTest v3 için yalnızca method ve ilgili site parametresini değiştirmeniz yeterlidir — SSE katmanı gönderdiğiniz türü hiç bilmez, çünkü callback her tür için aynı biçimde döner.

İki noktaya dikkat edin. callback_url içindeki client_id, sonucu doğru tarayıcıya yönlendiren tek anahtardır; tahmin edilebilir bir sayaç yerine uuid kullanın. Ve Flask'ı threaded=True olmadan çalıştırırsanız açık SSE bağlantısı tek işçiyi bloke eder, /submit hiç yanıt alamaz — geliştirmede en sık rastlanan takılma noktası budur.

Adım 2: Tarayıcı tarafında EventSource

İstemci tarafı şaşırtıcı derecede kısadır: tek bir EventSource nesnesi akışa bağlanır ve captcha-solved olaylarını dinler. Görev gönderimi ayrı bir POST /submit isteğiyle yapılır; sonuç geldiğinde olay kendiliğinden tetiklenir. Kodun tamamında tek bir setInterval yoktur — sorgulama mantığının kaybolduğu yer tam olarak burasıdır.

EventSource bağlantı koptuğunda kendiliğinden yeniden bağlanır, dolayısıyla onerror içinde manuel yeniden deneme yazmanız gerekmez; oraya yalnızca kullanıcıya durum gösteren bir uyarı koyun. Üretimde ayrıca sayfa kapanırken eventSource.close() çağırmak iyi bir alışkanlıktır, aksi hâlde sunucu tarafında kapanmamış bağlantılar birikir.

<!DOCTYPE html>
<html>
<body>
  <button onclick="submitCaptcha()">Solve CAPTCHA</button>
  <div id="results"></div>

  <script>
    const clientId = crypto.randomUUID();
    const resultsDiv = document.getElementById("results");

    // Connect SSE stream
    const eventSource = new EventSource(`/events/${clientId}`);

    eventSource.addEventListener("captcha-solved", (event) => {
      const data = JSON.parse(event.data);
      resultsDiv.innerHTML += `<p>Task ${data.task_id}: ${data.solution.substring(0, 30)}...</p>`;
    });

    eventSource.onerror = () => {
      console.log("SSE connection lost, reconnecting...");
    };

    async function submitCaptcha() {
      const response = await fetch("/submit", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          client_id: clientId,
          sitekey: "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
          pageurl: "https://example.com"
        })
      });
      const result = await response.json();
      resultsDiv.innerHTML += `<p>Submitted: ${result.task_id}</p>`;
    }
  </script>
</body>
</html>

EventSource bağlantı koptuğunda kendiliğinden yeniden bağlanır; onerror içine manuel yeniden deneme değil, yalnızca kullanıcıya durum gösteren bir uyarı koyun. Sayfa kapanırken eventSource.close() çağırın, aksi hâlde sunucuda kapanmamış bağlantılar birikir.

Adım 3: Aynı akışın Node.js (Express) karşılığı

Node.js tarafında kuyruk tutmanıza gerek kalmaz; istemcinin Response nesnesini bir Map içinde saklayıp doğrudan ona yazarsınız. Uç nokta isimleri ve akış Python sürümüyle birebir aynı olduğundan iki servisi aynı panele birlikte bağlayabilirsiniz.

Olay döngüsü sayesinde Express sürümü eşzamanlılıkta çok daha rahattır: her bağlantı yalnızca açık bir soket ve Map içinde bir referanstır. Buna karşılık req.on("close") içinde clearInterval ve clients.delete çağırmayı atlarsanız, kapanan her sekme arkasında çalışan bir zamanlayıcı bırakır; bellek sızıntısı birkaç saat içinde görünür.

const express = require("express");
const axios = require("axios");

const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY;
const BASE_URL = process.env.BASE_URL || "http://localhost:3000";

// Per-client SSE connections: clientId -> Response object
const clients = new Map();

// SSE endpoint
app.get("/events/:clientId", (req, res) => {
  const clientId = req.params.clientId;

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
    "X-Accel-Buffering": "no",
  });

  clients.set(clientId, res);

  // Keepalive every 30 seconds
  const keepalive = setInterval(() => {
    res.write(": keepalive\n\n");
  }, 30000);

  req.on("close", () => {
    clearInterval(keepalive);
    clients.delete(clientId);
  });
});

// Submit CAPTCHA
app.post("/submit", async (req, res) => {
  const { client_id, sitekey, pageurl } = req.body;
  const callbackUrl = `${BASE_URL}/callback?client_id=${client_id}`;

  try {
    const resp = await axios.post("https://ocr.captchaai.com/in.php", null, {
      params: {
        key: API_KEY,
        method: "userrecaptcha",
        googlekey: sitekey,
        pageurl: pageurl,
        pingback: callbackUrl,
        json: 1,
      },
    });

    if (resp.data.status === 1) {
      return res.json({ task_id: resp.data.request });
    }
    res.status(400).json({ error: resp.data.request });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// CaptchaAI callback → push to SSE
app.get("/callback", (req, res) => {
  const clientId = req.query.client_id;
  const taskId = req.query.id;
  const solution = req.query.code;

  const clientRes = clients.get(clientId);
  if (clientRes) {
    const data = JSON.stringify({ task_id: taskId, solution: solution });
    clientRes.write(`event: captcha-solved\ndata: ${data}\n\n`);
  }

  res.sendStatus(200);
});

app.listen(3000, () => console.log("SSE server running on :3000"));

SSE, WebSocket ve sorgulama: hangisi bu iş için doğru?

Kararı tek soruyla verebilirsiniz: istemcinin sunucuya sürekli veri göndermesi gerekiyor mu? CAPTCHA bildiriminde yanıt hayır — veri yalnızca sunucudan istemciye akar. Çift yönlü WebSocket burada gereğinden fazla altyapı, sorgulama ise gereğinden fazla istek demektir.

Özellik SSE WebSocket Sorgulama
Yön Sunucu – İstemci Çift yönlü İstemci – Sunucu
Protokol HTTP/1.1+ WS/WSS HTTP
Otomatik yeniden bağlanma Yerleşik Manuel Yok
Tarayıcı desteği Tüm modern tarayıcılar Tüm modern tarayıcılar Hepsi
Karmaşıklık Düşük Orta Düşük
Boşa giden istekler Yok Yok Çok
CAPTCHA sonuçları için İdeal Fazla karmaşık Çalışır ama israf
  • Tek yönlü bildirim yetiyorsa: SSE — yeniden bağlanma tarayıcıda yerleşiktir, ek kod yazmazsınız.
  • İstemci de sürekli veri gönderiyorsa: WebSocket.
  • Tek seferlik, arka planda bir sonuç kontrolü yapıyorsanız: res.php sorgulaması hâlâ makul.

Yerel senaryo: çok oturumlu ödeme adımı QA'i

Bu kalıp en çok, birden fazla oturumun durumunu tek ekranda izlediğiniz panellerde işe yarar. Örnek: bir e-ticaret ekibi, Europe/Istanbul saat diliminde gece çalışan regresyon koşusunda 40 eşzamanlı ödeme adımı senaryosu test ediyor; her senaryo bir CAPTCHA doğrulamasına takılıyor ve QA mühendisi hangi oturumun nerede beklediğini canlı görmek istiyor.

Sorgulamayla bu 40 oturum, dakikada yüzlerce boşa res.php isteği üretir. Pingback ve SSE ile aynı panel açık bağlantı başına sıfır boşa istekle çalışır; her çözüm düştüğü anda ekranda belirir. Thread tabanlı planlarda çözüm başına ücret olmadığı için maliyette değişen tek şey eşzamanlılık ihtiyacınızdır: 40 paralel oturum için ADVANCE ($90/ay, 50 thread) rahat eder, daha küçük bir regresyon seti içinse BASIC ($15/ay, 5 thread) yeterlidir. Fiyatlar USD üzerindendir; TL kurundaki oynaklık aylık bütçenizi değiştirmez.

Veri kazıma tarafında çalışıyorsanız, aynı panelde topladığınız içerik kişisel veri barındırdığı anda KVKK kapsamına girer. SSE burada yalnızca teslimat katmanıdır — veriyi toplama yetkiniz ve veriyle ne yaptığınız ayrı bir sorumluluktur.

Üretim ortamı: ölçeklendirme ve bağlantı yönetimi

Yük dengeleyici arkasında ölçeklendirme

SSE bağlantıları durum bilgilidir. Birden fazla sunucu örneği çalıştırıyorsanız CaptchaAI'nin callback'i, istemcinin SSE bağlantısını tutan örnekten farklı bir örneğe düşebilir — sonuç doğru istemciye hiç ulaşmaz. Tek sunucuda kusursuz çalışan kurulumların yayına alındıktan sonra sessizce bozulmasının bir numaralı nedeni budur.

Belirti yanıltıcıdır: hata kaydı düşmez, callback 200 döner, yalnızca panelde bazı sonuçlar hiç görünmez. İki örnekli bir kurulumda sonuçların kabaca yarısı kaybolur ve sorun "ara sıra çalışmıyor" gibi görünür.

Çözüm: örnekler arasına Redis Pub/Sub koyun. Callback'i alan örnek sonucu kanala yayınlar, istemciyi tutan örnek aynı kanala abone olarak akışa geçirir:

# Callback handler publishes to Redis
import redis
r = redis.Redis()
r.publish(f"captcha:{client_id}", json.dumps(message))

# SSE handler subscribes to Redis
pubsub = r.pubsub()
pubsub.subscribe(f"captcha:{client_id}")
for msg in pubsub.listen():
    if msg["type"] == "message":
        yield f"data: {msg['data'].decode()}\n\n"

Tarayıcı bağlantı sınırları

Tarayıcılar HTTP/1.1 üzerinde alan başına yalnızca 6 eşzamanlı SSE bağlantısına izin verir. Daha fazla paralel görev izleyecekseniz iki seçeneğiniz var: HTTP/2'ye geçip sınırı yükseltmek ya da istemci başına tek akış açıp birden çok görevin sonucunu task_id alanıyla ayırmak. İkincisi hem sınırı aşar hem de açık bağlantı sayısını düşürür.

Yayına almadan önceki dört kontrol

  • SSE uç noktasında X-Accel-Buffering: no ve Cache-Control: no-cache başlıkları ayarlı mı?
  • Keepalive aralığı (30 saniye) proxy zaman aşımının altında mı?
  • Birden fazla örnek varsa callback ile SSE işleyicisi arasında Redis Pub/Sub var mı?
  • Tarayıcı farklı bir alan adından bağlanıyorsa Access-Control-Allow-Origin doğru değeri döndürüyor mu?

Sık karşılaşılan sorunlar ve çözümleri

Sorun Neden Çözüm
SSE bağlantısı her 30 saniyede bir düşüyor Proxy/yük dengeleyici zaman aşımı Keepalive yorumları gönderin; proxy zaman aşımını artırın
Sonuçlar gelmiyor Callback farklı sunucu örneğine düşüyor Callback ile SSE işleyicileri arasına Redis Pub/Sub ekleyin
Tarayıcı konsolunda hata görünüyor CORS başlıkları eksik SSE uç noktasına Access-Control-Allow-Origin başlığını ekleyin
Sürekli yeniden bağlanma Sunucu hatalı biçimli SSE gönderiyor Her olayın iki satır sonuyla bittiğinden emin olun; veri biçimini doğrulayın
Callback hiç gelmiyor pingback adresi dışarıdan erişilemiyor Adresin genel internetten ulaşılabilir olduğunu doğrulayın; yerel geliştirmede tünel kullanın

Sık sorulan sorular

SSE, Cloudflare arkasında çalışır mı?

Çalışır, ancak Cloudflare yanıtı arabelleğe alabilir. X-Accel-Buffering: no başlığını gönderin veya akış (streaming) modunu etkinleştirin. Aynı önlem nginx ve çoğu ters proxy için de geçerlidir.

Tek sunucu kaç eşzamanlı SSE bağlantısı taşır?

Her bağlantı hafif ve açık tutulan bir HTTP bağlantısı olduğu için Node.js on binlerce eşzamanlı akışı taşır. Thread tabanlı Flask kurulumu çok daha erken sınıra dayanır; yüksek eşzamanlılıkta asyncio tabanlı bir çatıya (örneğin FastAPI) geçin.

Pingback adresim yerel makinemdeyken nasıl test ederim?

CaptchaAI callback'i genel internetten erişilebilir bir adrese gönderir, dolayısıyla localhost doğrudan çalışmaz. Geliştirme sırasında bir tünel servisiyle yerel portunuzu dışarı açın ya da callback'i staging sunucunuza yönlendirip sonucu oradan izleyin.

Bu akış hangi CAPTCHA türleriyle kullanılabilir?

SSE katmanı türden bağımsızdır; gönderim tarafında reCAPTCHA v2/v3, Cloudflare Turnstile, Cloudflare doğrulama akışı, GeeTest v3 ve görüntü/OCR türlerini kullanabilirsiniz. hCaptcha ve FunCaptcha (Arkose Labs) desteklenmiyor, GeeTest v4 için çok yakında ifadesi geçerli. CaptchaFox (beta), Friendly Captcha (beta) ve Lemin (beta) yalnızca beta aşamasındadır.

Bağlantı koptuğunda çözüm kaybolur mu?

EventSource bağlantıyı kendiliğinden yeniden kurar. Kısa kesintilerde çözüm sunucudaki kuyrukta ya da Redis kanalında beklediği için yeniden bağlanan istemciye iletilebilir. Kaybın kabul edilemediği yerde callback sonucunu kalıcı bir kuyruğa yazın ve istemci döndüğünde yeniden yayınlayın.

Sonraki adımlar

Sorgulama döngüsünü kaldırmak tek bir parametreyle başlar: gönderim isteğine pingback ekleyin, callback'i SSE akışına bağlayın. CaptchaAI API anahtarınızı alın ve ilk çözümü panelde canlı görün.

İlgili rehberler:

Bu makale için yorumlar devre dışı bırakılmıştır.