العودة إلى المدونة

استكشاف الأخطاءدليل

فشل البث بعد HTTP 200: التقط أخطاء منتصف البث في Python

لماذا يمكن أن ينكسر البث بعد حالة 200، وكيف تكتشف event: error، وإعادة محاولة بلغة Python تحتفظ بالنص الجزئي دون تكرار الإجابة.

بواسطة CheapTokens Teamنُشر في 6 دقيقة قراءة

بث بحالة HTTP 200 OK ينتهي بـ event: error وoverloaded_error بدلًا من message_stop
في هذه الصفحة
  1. ما الذي يضمنه 200 حقًا
  2. التقطه في Python
  3. أعد المحاولة دون طباعة الإجابة مرتين
  4. ليس كل توقف مبكر خطأً
  5. قبل أن تشحن

واجهة الدردشة لديك بثّت نصف إجابة، ثم تجمّدت، والسجل يقول 200 OK. هذا ليس تناقضًا: الرمز 200 يعني فقط أن البث بدأ، وليس أنه انتهى. إذا حدث خلل لاحقًا، فلا يمكن للخادم إلا إرسال خطأ داخل البث، لذا لا يلتقطه أي فحص لرمز الحالة. الحل هو اعتبار البث ناجحًا فقط عندما ينتهي بإشارة الإنهاء الخاصة به، وإعادة المحاولة من مخزن مؤقت نظيف عندما لا يفعل ذلك.

يعرض باقي هذه الصفحة كيف يبدو البث المعطوب، ومعالجَين بلغة Python (Anthropic Messages وOpenAI SDK)، وإعادة محاولة لا تطبع بداية الإجابة مرتين. يستخدم كلا المعالجَين CheapTokens كنقطة نهاية API: تحتوي الوثائق على عناوين URL الأساسية، والنموذج التجريبي هو DeepSeek-V4-Flash (المعرّف deepseek-ai/DeepSeek-V4-Flash-0731، سياق 380K، حتى 32,768 رمز إخراج لكل إجابة، سعر واحد في أي ساعة — انظر صفحة النموذج).

ما الذي يضمنه 200 حقًا#

تخيّل ساعي بريد يوقّع على طرد عند الباب: التوقيع يثبت أنه وصل، لا أن الطرد سليم. يعمل HTTP بالطريقة نفسها. يرسل الخادم سطر الحالة والترويسات أولًا، ثم يصل الجسم قطعة قطعة. إذا أُثقل الخادم الخلفي للنموذج في منتصف الطريق، يكون سطر الحالة قد ذهب بالفعل.

تقول صفحة أخطاء Anthropic API (تم التحقق في 1 نوفمبر 2026) ذلك مباشرة: عندما تتلقى استجابة بث عبر SSE، يمكن أن يحدث خطأ بعد أن يعيد API رمز 200، وعندها لا ينطبق معالجة الأخطاء القياسية. وتعطي وثائق البث شكل ذلك الحدث:

text
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

overloaded_error يقابل عادةً HTTP 529 في استدعاء غير بثّي (حمل زائد مؤقت من حركة مرور عالية). تسرد الصفحة نفسها 500 api_error (أعد المحاولة بتراجع أُسّي) و504 timeout_error (استخدم البث للطلبات الطويلة). على الشبكة يبدو البث المعطوب هكذا:

بث يبدأ بـ 200 OK وينتهي بـ event: error بدلًا من message_stop
تشريح خطأ منتصف البث: تتوقف أحداث المحتوى ويصل event: error حيث يجب أن يكون message_stop (الشكل من وثائق البث لدى Anthropic)

تحذّر الوثائق أيضًا من أن أنواع أحداث جديدة قد تُضاف، لذا يجب أن يتعامل كودك مع الأحداث غير المعروفة بلطف. لهذا لا تحاول المعالجات أدناه التعرف على كل بايت خطأ. بل تطرح سؤالًا واحدًا: هل انتهى البث بإشارة الإنهاء الخاصة به؟

  • Anthropic Messages: الحدث الأخير هو message_stop، وstop_reason في message_delta يوضح سبب انتهائه (end_turn، max_tokens، stop_sequence).
  • OpenAI Chat Completions: تحمل القطعة الأخيرة finish_reason غير فارغ (stop، length، …).

لا إشارة إنهاء، أو استثناء في الطريق — الإجابة غير مكتملة، مهما قال رمز الحالة.

التقطه في Python#

خذ روبوت دعم يبثّ الردود إلى المتصفح. تجمع الحلقة النص في مخزن مؤقت وتُبلّغ بثلاثة أشياء: النص حتى الآن، وهل انتهى البث، والخطأ إن وُجد. اختر التبويب الخاص بـ SDK الذي تستخدمه.

معالج واحد، SDKان

Anthropic SDK

python
import os
import anthropic

client = anthropic.Anthropic(
    base_url='https://api.cheaptokens.dev',  # no /v1 for the Anthropic SDK
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

def ask_once(prompt, on_text=lambda t: None):
    parts, finished = [], False
    try:
        with client.messages.stream(
            model=MODEL,
            max_tokens=1024,
            messages=[{'role': 'user', 'content': prompt}],
        ) as stream:
            for event in stream:
                if event.type == 'content_block_delta' and event.delta.type == 'text_delta':
                    parts.append(event.delta.text)
                    on_text(event.delta.text)
                elif event.type == 'message_stop':
                    finished = True
    except anthropic.APIError as err:
        return ''.join(parts), False, err
    return ''.join(parts), finished, None

OpenAI SDK

python
import os
import openai

client = openai.OpenAI(
    base_url='https://api.cheaptokens.dev/v1',
    api_key=os.environ['CHEAPTOKENS_API_KEY'],
)
MODEL = 'deepseek-ai/DeepSeek-V4-Flash-0731'

def ask_once(prompt, on_text=lambda t: None):
    parts, finish = [], None
    try:
        stream = client.chat.completions.create(
            model=MODEL,
            max_tokens=1024,
            stream=True,
            messages=[{'role': 'user', 'content': prompt}],
        )
        for chunk in stream:
            if not chunk.choices:
                continue
            choice = chunk.choices[0]
            if choice.delta.content:
                parts.append(choice.delta.content)
                on_text(choice.delta.content)
            if choice.finish_reason:
                finish = choice.finish_reason
    except openai.APIError as err:
        return ''.join(parts), False, err
    return ''.join(parts), finish is not None, None

هناك تفصيلان يستحقان نظرة ثانية. أولًا، يلتقط كلا المعالجَين فئة الخطأ الأساسية في SDK (anthropic.APIError، openai.APIError)، لذا لا يهم أي فئة فرعية يرفعها SDK لفشل معين. ثانيًا، علم finished منفصل عن الاستثناء: يمكن للبث أيضًا أن ينتهي مبكرًا فقط، دون أي استثناء، ويجب أن تُحتسب تلك الحالة فشلًا أيضًا.

يضيف README الخاص بـ openai-python ما يجب توقعه على جانب OpenAI (تم التحقق في 1 نوفمبر 2026): مهلة قراءة في بث ترفع APITimeoutError، وفشل HTTPX أخرى ترفع APIConnectionError مع الاستثناء الأصلي في __cause__. إذا توقف البث بدلًا من أن يفشل، انظر المهلات وإعادة المحاولات في OpenAI Python SDK.

أعد المحاولة دون طباعة الإجابة مرتين#

إليك خطأً يسهل شحنه. محاولة أولى تطبع "سياسة الاسترداد هي…"، ثم تموت، وتبدأ إعادة المحاولة من جديد وتطبع "سياسة الاسترداد هي…" مرة أخرى — الآن يرى المستخدم البداية مرتين، أو جملة ملتصقة بإعادة تشغيلها. السبب المعتاد هو مخزن مؤقت مشترك واحد: answer += text يعيش خارج المحاولة، لذا تُلحق المحاولة الثانية على الأولى.

العلاج ممل. محاولة واحدة تملك مخزنًا مؤقتًا واحدًا، ويُرمى نص المحاولة الفاشلة (أو يُحتفظ به للسجل فقط)، وتبدأ إعادة المحاولة من الصفر:

python
import time

def ask_with_retry(prompt, attempts=3, on_text=lambda t: None, on_restart=lambda: None):
    for attempt in range(attempts):
        text, finished, err = ask_once(prompt, on_text)
        if finished:
            return text
        status = getattr(err, 'status_code', None)
        if status is not None and 400 <= status < 500 and status not in (408, 409, 429):
            raise err  # a 400/401/404 will not fix itself
        print(f'attempt {attempt + 1} failed after {len(text)} chars: {err!r}')
        if attempt == attempts - 1:
            break
        on_restart()  # clear the message box in your UI
        time.sleep(2 ** attempt)  # 1 s, 2 s
    raise RuntimeError('stream failed after all attempts')

on_restart هو الجزء الذي يوقف التكرار. في واجهة ويب يفرّغ فقاعة الرسالة قبل أن يتدفق البث التالي. في طرفية عادية لا يمكنك إلغاء الطباعة، لذا إما اطبع فقط بعد أن يصبح finished صحيحًا، أو اطبع سطر --- retrying --- مرئيًا. إعادة المحاولة من الصفر تعيد إرسال المطالبة كاملة، لذا يُفوتر الإدخال مرة أخرى للمحاولة الجديدة؛ لمعظم ردود الدردشة هذا سعر عادل مقابل إجابة نظيفة.

رتّب بالترتيب

رتّب خطوات إعادة المحاولة بالترتيب الذي يجب أن ينفذها كودك

  1. أوقف قراءة البث المعطوب واحتفظ بنصه الجزئي للسجل
  2. افحص الخطأ: خطأ 4xx غير 408 و409 و429 يعني التوقف، لا إعادة المحاولة
  3. امسح مربع الرسالة وابدأ مخزنًا مؤقتًا جديدًا
  4. انتظر بتراجع أُسّي (1 ثانية، ثم 2 ثانية)
  5. أرسل الطلب كاملًا مرة أخرى واعرض الإجابة الجديدة فقط

يعيد SDK الخاص بـ Anthropic بالفعل محاولة الفشل العابر — أخطاء الاتصال، وحدود المعدل، و5xx — بتراجع أُسّي، مرتين افتراضيًا، مع احترام retry-after، وmax_retries يغيّر ذلك. هذا يغطي الطلب. أما البث الذي يموت بعد 200 فقصة مختلفة: تقول الوثائق إن معالجة الأخطاء القياسية لا تنطبق هناك، لذا تبقى الحلقة أعلاه مسؤوليتك. بالنسبة لـ 429 وحده، هناك شرح منفصل: 429 Too Many Requests: إعادة المحاولات بتراجع.

ليس كل توقف مبكر خطأً#

قبل أن تعيد المحاولة، افحص سبب انتهاء الإجابة. البث الذي ينتهي بـ stop_reason: max_tokens (Anthropic) أو finish_reason: length (OpenAI) قد انتهى فعلًا — لكنه وصل إلى الحد الذي وضعته. إعادة المحاولة تعيد النص المقطوع نفسه والفاتورة نفسها. ارفع الحد بدلًا من ذلك؛ دليل الإجابة المقطوعة يشرح ذلك.

هناك مجموعة ثالثة: الشبكة. قد تُسقط الاتصالات الخاملة في الطريق، ما يظهر كخطأ اتصال أو بث يتوقف دون أي حدث. تعامل معه كما مع خطأ منتصف البث، وانظر أخطاء الاتصال الشائعة إذا استمر حدوثه. للتوليدات الطويلة، توصي Anthropic بالبث بدلًا من max_tokens غير بثّي ضخم، لذا أنت بالفعل في الجانب الصحيح من ذلك.

قبل أن تشحن#

احفظ معرّف الطلب لكل محاولة فاشلة. تحمل أجسام أخطاء Anthropic حقل request_id وترويسة استجابة request-id، ويكشف Python SDK عن message._request_id؛ بذلك المعرّف يصبح الفشل قابلًا للتتبع بدلًا من أن يكون حكاية.

قائمة التحقق

معالجة أخطاء البث، كما يجب

إذا أردت رؤية المعالج يعمل، وجّهه إلى مفتاحك الخاص: أنشئ مفتاح API في الوثائق، واضبط CHEAPTOKENS_API_KEY، واستدعِ ask_with_retry('Explain SSE in two sentences'). لأساسيات البث في Python، ابدأ بـ استجابات البث في Python؛ ويُغطى جانب Anthropic في Anthropic Python SDK مع CheapTokens.

مسار إعادة المحاولة: يبدأ البث، يصل خطأ، يُرمى النص الجزئي، يُرسل الطلب مرة أخرى
حلقة إعادة المحاولة في خمس خطوات: ابدأ، حدث خطأ، أرمِ المخزن المؤقت الجزئي، تراجع، اسأل مرة أخرى