لا تُحِط HttpClient بـ using ── الممارسة العمليّة للاتّصال عبر HTTP في تطبيقات C# للأعمال (أنماط الإنشاء، وضبط المهلة، وإعادة المحاولة)

· · CSharp, .NET, HttpClient, IHttpClientFactory, الشبكات, async/await, تطوير Windows, الاستشارات التقنية

«بعد الظهر تبدأ الاتّصالات بواجهات API الخارجيّة بالفشل بخطأ SocketException» أو «بعد تبديل الخادم الوجهة، يستمرّ التطبيق بالاتّصال بالخادم القديم» ── استدعاء HttpClient عبر GetAsync بسيط في ظاهره، لكنّ الخطأ في كيفيّة إنشاء النسخة (instance) والاحتفاظ بها يزرع هذا النوع من الأعطال التي «تعمل في البداية، ثمّ تنكسر بعد الدخول في التشغيل الفعليّ».

في هذا المقال، وبافتراض أنّ تطبيق Windows للأعمال يستدعي واجهة API خارجيّة أو خدمة داخليّة، سنرتّب النمط الصحيح لإنشاء HttpClient، وتصميم المهلات الزمنيّة، وإعادة المحاولة، ومعالجة الأخطاء، وحتّى المطبّات الخاصّة ببيئة Windows، بالترتيب الذي يكثر التردّد فيه في العمل الفعليّ.

1. الخلاصة أوّلاً (جدول القرار)

تختلف طريقة الاحتفاظ بـ HttpClient باختلاف بنية التطبيق. لنبدأ بجدول القرار.

شكل التطبيق النمط الموصى به السبب
أداة سطر أوامر (.NET) تنتهي خلال ثوانٍ إلى دقائق نسخة واحدة static أو singleton من HttpClient في عمليّة قصيرة العمر، يمكن تجاهل مشكلة تغيّر DNS عمليّاً
تطبيق يعمل باستمرار لفترة طويلة أو خدمة Windows (.NET، بدون DI) HttpClient من نوع static + ضبط SocketsHttpHandler.PooledConnectionLifetime يحلّ استنزاف المقابس ومشكلة تغيّر DNS معاً
تطبيق يستخدم Generic Host / DI (.NET) IHttpClientFactory (عبر AddHttpClient) يمكن ترك تجميع الـ handlers واستبدالها لِـ factory، مع فصل الإعدادات لكلّ وجهة عبر عملاء مُسمَّين أو مُصنَّفين بنوع
تطبيقات .NET Framework إدخال IHttpClientFactory عبر حزمة Microsoft.Extensions.Http استنزاف المنافذ (ports) أكثر شيوعاً عند الإنشاء الذاتيّ في .NET Framework، واستخدام الـ factory موصى به رسميّاً1
عندما يختلف إعداد البروكسي أو الكوكيز أو الشهادات باختلاف الوجهة فصل HttpClient لكلّ إعداد (بدون إعادة استخدامه) لا يمكن تغيير إعدادات اتّصال الـ handler بعد إرسال أوّل طلب2

وبعد ذلك، نكتب الخلاصة أوّلاً:

  • لا يجوز إنشاء new HttpClient() والتخلّص منه بـ using في كلّ طلب. فـ HttpClient مصمَّم على افتراض إعادة الاستخدام، ويحتفظ داخليّاً بمجمّع اتّصالات. عند الإنشاء والتخلّص المتكرّر، تُستنزف المقابس المتاحة تحت الحمل المرتفع ويظهر SocketException2.
  • كما أنّ الاكتفاء بجعله static لا يحلّ المشكلة كاملةً. لأنّ HttpClient لا يحلّ DNS إلا عند إنشاء الاتّصال، فحتّى إن تغيّر عنوان IP الخاصّ بالوجهة، يستمرّ في استخدام الاتّصال القديم. الحلّ الموصى به رسميّاً هو تحديد عمر الاتّصال عبر SocketsHttpHandler.PooledConnectionLifetime1.
  • إن كنتَ تستخدم Generic Host أو DI، فاترك الأمر لِـ IHttpClientFactory. يجمِّع الـ factory الـ handlers ويستبدلها كلّ دقيقتين افتراضيّاً، ما يعالج استنزاف المقابس وتغيّر DNS معاً3.
  • القيمة الافتراضيّة للمهلة الزمنيّة هي 100 ثانية. وهذا طول شبه معادل لـ«التجمّد اللانهائيّ» بالنسبة لتطبيق أعمال، لذا حدِّد المتطلّبات بوضوح لكلّ وجهة.
  • لا تُنشئ إعادة المحاولة بنفسك، بل ابدأ من المعالج القياسيّ في Microsoft.Extensions.Http.Resilience. فهو يوفِّر إعادة المحاولة وقاطع الدارة (circuit breaker) والمهلة الزمنيّة بمجموعة قيم افتراضيّة مُجرَّبة، ما يتجنّب منذ مرحلة التصميم أخطاء شائعة في حلقات إعادة المحاولة المُنشأة يدويّاً، مثل إعادة إرسال POST فاشل دون شرط فيسبِّب تسجيلاً مزدوجاً4.

2. لماذا لا يصحّ «الإنشاء في كلّ مرّة داخل using» ── استنزاف المقابس

بما أنّ HttpClient ينفِّذ IDisposable، تبدو الشيفرة التالية صحيحة للوهلة الأولى.

// نمط خاطئ: الإنشاء والتخلّص في كلّ طلب
public async Task<string> GetDataAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);
}

المشكلة أنّ استدعاء Dispose لا يُحرِّر المقبس (socket) فوراً على مستوى نظام التشغيل. فبحسب مواصفات TCP، يبقى المقبس المُغلَق من الطرف الذي أنهى الاتّصال في حالة TIME_WAIT لبعض الوقت. طالما تواتر الاستدعاء منخفضاً، لا يحدث شيء، لكن عند ارتفاع الحمل، تتراكم المقابس غير المُحرَّرة، وفجأة يظهر SocketException ويتعذّر الاتّصال2.

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

# تجميع عدد المقابس في حالة TIME_WAIT لكلّ وجهة
netstat -ano | Select-String "TIME_WAIT" | Measure-Object

مع ذلك، لا ينطبق هذا الأمر على HttpClient الذي يُستلَم عبر IHttpClientFactory. فالعميل المُنشَأ عبر الـ factory لا يتخلَّص من الـ handler (الجهة الفعليّة لمجمّع الاتّصالات) عند استدعاء Dispose، لذا إحاطته بـ using آمنة3.

3. جعله static لا يُنهي القصّة ── مشكلة تغيّر DNS

جعل HttpClient من نوع static كإجراء وقائيّ ضدّ استنزاف المقابس اتّجاه صحيح، لكن يبقى وحده مشكلة أخرى. يحلّ HttpClient عنوان DNS فقط عند إنشاء الاتّصال، ولا يراعي حتّى TTL الخاصّ بسجلّ DNS2. طالما يبقى الاتّصال حيّاً داخل المجمّع، يستمرّ في الاتّصال بعنوان IP القديم حتّى لو تغيّر عنوان الوجهة.

العطل من نوع «بدّلتُ DNS بسبب فشل التحويل (failover)، لكنّ التطبيق ظلّ يرى الخادم القديم حتّى إعادة التشغيل» سببه هذه الآليّة بالذات. الحلّ الذي توصي به الإرشادات الرسميّة هو تحديد عمر الاتّصال عبر SocketsHttpHandler.PooledConnectionLifetime1.

// النمط الموصى به في .NET (Core) / .NET 5 وما بعده:
// إعادة إنشاء الاتّصال دوريّاً حتّى يتابع تغيّرات DNS
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});

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

كما أنّ SocketsHttpHandler تطبيق يعود إلى .NET Core 2.1 فما بعده، وغير متاح في .NET Framework. في حال .NET Framework، استخدم IHttpClientFactory الموضَّح في الفصل التالي1.

4. عند استخدام DI، اعتمد على IHttpClientFactory

في التطبيقات التي تستخدم Generic Host أو حاوية DI، يكون IHttpClientFactory (عبر AddHttpClient) هو الخيار الأوّل. للاطّلاع على شرح Generic Host نفسه، راجع «ما هو .NET Generic Host»، ولإدخاله في تطبيق سطح مكتب، راجع «استخدام Generic Host + BackgroundService في تطبيق سطح مكتب».

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// عميل مُسمَّى: فصل الإعدادات لكلّ وجهة
builder.Services.AddHttpClient("OrderApi", client =>
{
    client.BaseAddress = new Uri("https://order.example.co.jp/");
    client.Timeout = TimeSpan.FromSeconds(10);
});

توجد ثلاث نقاط أساسيّة في سلوك الـ factory ينبغي معرفتها:

  • الـ handlers مُجمَّعة، وتُستبدَل كلّ دقيقتين افتراضيّاً. يعيد CreateClient نسخة جديدة من HttpClient في كلّ مرّة، لكن الـ handler (مجمّع الاتّصالات) خلف الكواليس مُشترَك، فلا يحدث استنزاف للمقابس، ويتابع تغيّرات DNS عبر الاستبدال الدوريّ3.
  • يُفترَض استخدام HttpClient المُنشَأ عبر الـ factory لمدّة قصيرة. فالاحتفاظ بالنسخة المُستلَمة في حقل singleton يمنعها من المشاركة في استبدال الـ handler، فتتوقّف عن متابعة تغيّرات DNS. تجنَّب أيضاً حقن عميل مُصنَّف بنوع (typed client) في خدمة singleton للسبب نفسه3.
  • يلزم الحذر في التطبيقات المعتمِدة على الكوكيز. بسبب تجميع الـ handlers، تُشارَك CookieContainer دون قصد. عند استخدام الكوكيز، تُنصَح الوثائق الرسميّة بتجنّب الـ factory، أو تعطيل معالجة الكوكيز وإضافة الترويسات (headers) يدويّاً بنفسك1.

بخصوص الحصول على الرمز المميّز (token) عند استدعاء واجهة API محميّة بمصادقة (كالمحميّة بـ Microsoft Entra ID)، راجع «دمج مصادقة Entra ID في تطبيقات WinForms/WPF».

5. تصميم المهلة الزمنيّة ── القيمة الافتراضيّة 100 ثانية طويلة جدّاً لتطبيق أعمال

القيمة الافتراضيّة لـ HttpClient.Timeout هي 100 ثانية5. في تطبيق أعمال يستدعي واجهة API امتداداً لعمليّة على الشاشة، فإنّ انتظار 100 ثانية يعادل عمليّاً «التجمّد»، لذا حدِّدها صراحةً لكلّ وجهة.

// المهلة الافتراضيّة للعميل ككلّ
client.Timeout = TimeSpan.FromSeconds(10);

// إن أردتَ تقصير أو تطويل المهلة لطلب معيّن فقط، استخدم CancellationTokenSource
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);

نقاط التصميم التي يجب الانتباه إليها:

  • الاستثناء عند حدوث المهلة الزمنيّة هو TaskCanceledException. ابتداءً من .NET 5، يحتوي الاستثناء الداخليّ عند مهلة ناتجة عن HttpClient.Timeout على TimeoutException5. لكن في حالة المهلة عبر CancellationTokenSource الخاصّ بك كما في المثال أعلاه، لا يُرفَق استثناء داخليّ. التمييز الموثوق بين مهلة زمنيّة وإلغاء ناتج عن المستخدم لا يعتمد على الاستثناء الداخليّ، بل على «هل الرمز (token) الممرَّر من جهة الاستدعاء أصبح ملغى؟» (مثال الشيفرة في الفصل 7). احذر أن الاكتفاء بمعالجة HttpRequestException فقط في catch يفوِّت المهلة الزمنيّة.
  • Timeout هو حدّ لـ«الطلب بأكمله». إن أردتَ تقييد وقت إنشاء الاتّصال فقط، استخدم SocketsHttpHandler.ConnectTimeout معه. يمكن التعبير عن متطلّب مثل «أريد التخلّي بعد 3 ثوانٍ إذا كان الخادم متوقّفاً، لكن أريد الانتظار حتّى 60 ثانية للاستجابات الكبيرة في الحالة الطبيعيّة» بدمج هذين الإعدادين2.
  • تجنَّب التخزين المؤقّت (buffering) الافتراضيّ عند تنزيل ملفّات كبيرة. يقرأ HttpClient افتراضيّاً الاستجابة كاملةً إلى الذاكرة، لذا في التنزيلات التي تتجاوز عشرات الميغابايتات، حدِّد HttpCompletionOption.ResponseHeadersRead وعالِجها كتدفّق (stream)2.

بخصوص فصل قيمة المهلة الزمنيّة إلى appsettings.json وتغييرها حسب البيئة، يمكن استخدام جدول القرار في «الممارسة العمليّة لإدارة الإعدادات في تطبيقات Windows للأعمال» كما هو.

كما أنّ استدعاء الاتّصال عبر HTTP من WinForms/WPF مع الحظر (blocking) عبر .Result أو .Wait() يسبِّب توقّفاً (deadlock) في خيط واجهة المستخدم. هذا المطبّ الشائع مشروح بالتفصيل في «جدول القرار العمليّ لـ C# async/await»، وننصح بقراءته قبل كتابة معالجة الاتّصال.

6. إعادة المحاولة ── استخدم معالج resilience القياسيّ بدل حلقة مُنشأة يدويّاً

الشبكة عرضة للفشل المؤقّت، لذا تحتاج العمليّة التي تستدعي واجهة API خارجيّة إلى إعادة محاولة. لكنّ إعادة المحاولة اليدويّة عبر حلقة for وTask.Delay تتطلّب تنفيذ كلّ النقاط التالية بنفسك بشكل صحيح، وهو أمر غير مجدٍ.

  • التمييز بين الفشل الذي يجوز إعادة محاولته (مهلة زمنيّة، HTTP 408/429/5xx) والفشل الذي لا فائدة من إعادة محاولته (HTTP 400/401/404)
  • استبعاد طرق HTTP التي تسبِّب حادثاً عند إعادة تنفيذها (كتنفيذ POS مزدوج للتسجيل)
  • التراجع الأُسّيّ (exponential backoff) مع jitter (تذبذب يمنع إعادة إرسال جميع العملاء في وقت واحد وإسقاط الخادم مجدّداً)
  • قاطع الدارة (circuit breaker) الذي يوقف الاستدعاء نفسه عند استمرار العطل

توفِّر المعالجات القياسيّة في حزمة Microsoft.Extensions.Http.Resilience هذه المجموعة كاملةً بقيم افتراضيّة مُجرَّبة4.

builder.Services.AddHttpClient("OrderApi", client =>
    {
        client.BaseAddress = new Uri("https://order.example.co.jp/");
    })
    .AddStandardResilienceHandler(); // مجموعة قياسيّة: إعادة محاولة + قاطع دارة + مهلة زمنيّة

القيم الافتراضيّة للمعالج القياسيّ هي: مهلة زمنيّة 30 ثانية للطلب بأكمله، حدّ أقصى 3 محاولات إعادة بتراجع أُسّيّ (تأخير أوّليّ ثانيتان مع jitter)، مهلة زمنيّة 10 ثوانٍ لكلّ محاولة، وتُعامَل رموز الحالة HTTP 408/429/5xx وHttpRequestException كأخطاء مؤقّتة4.

نقطة واحدة تحتاج الانتباه في القيم الافتراضيّة، وهي أنّ المعالج القياسيّ يعيد محاولة جميع طرق HTTP افتراضيّاً. في واجهات API التي تتضرّر من تنفيذ POST التسجيليّ مرّتين، عطِّل إعادة المحاولة للطرق غير الآمنة4.

httpClientBuilder.AddStandardResilienceHandler(options =>
{
    // تعطيل إعادة تنفيذ POST/PUT/DELETE وما شابه
    options.Retry.DisableForUnsafeHttpMethods();
});

مع ذلك، ما تحلّه إعادة المحاولة هو الفشل المؤقّت فقط. الأعراض مثل نجاح إنشاء الاتّصال دون تدفّق بيانات، أو استجابة بطيئة جدّاً، غالباً ما تكون مشكلة في طبقة TCP، وتفيد في تحليلها الأساليب المشروحة في «إعادة إرسال TCP وRFC1323 ── تحقيق في توقّف اتّصال كاميرا صناعيّة».

7. معالجة الأخطاء ── كيف تُعامَل رموز الحالة (status codes)

لا يرمي HttpClient استثناءً في حالة «فشل مع وصول استجابة HTTP فعليّة» مثل HTTP 404 أو 500. يحدث الاستثناء فقط عند عدم الحصول على استجابة أصلاً، كفشل الاتّصال أو المهلة الزمنيّة أو الإلغاء. اكتب الشيفرة مع مراعاة هذين النوعين بوضوح.

try
{
    using HttpResponseMessage response = await client.GetAsync(url, ct);

    if (!response.IsSuccessStatusCode)
    {
        // وصلت استجابة لكنّها فشلت: يمكن التفريع بحسب رمز الحالة
        if (response.StatusCode == HttpStatusCode.NotFound)
        {
            return null; // مثال على معاملة "غير موجود" كحالة طبيعيّة
        }
        response.EnsureSuccessStatusCode(); // أيّ شيء آخر يتحوّل إلى HttpRequestException
    }

    return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
    // فشل الاتّصال، أو فشل ناتج عن EnsureSuccessStatusCode.
    // ابتداءً من .NET 5، يمكن الوصول إلى رمز حالة الفشل عبر ex.StatusCode
    logger.LogError(ex, "فشل استدعاء واجهة API الخاصّة بالطلبات. StatusCode={StatusCode}", ex.StatusCode);
    throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
    // إلغاء ناتج عن الرمز (token) الممرَّر من جهة الاستدعاء (كإغلاق الشاشة مثلاً).
    // ليس خطأً، لذا يُعاد رفعه دون تلويث السجلّ (log)
    throw;
}
catch (TaskCanceledException ex)
{
    // انتهاء مهلة HttpClient.Timeout، أو انتهاء مهلة CTS الخاصّ بك
    logger.LogError(ex, "انتهت مهلة استدعاء واجهة API الخاصّة بالطلبات");
    throw;
}

القرار حول «هل تُعامَل حالة 404 كاستثناء أم كقيمة null» يعتمد على دلاليّات واجهة API الوجهة. فتحويل كلّ شيء إلى استثناء عبر EnsureSuccessStatusCode وحده يُضخِّم كتلة catch لدى جهة الاستدعاء. الخط الفاصل بين ما يُعامَل كاستثناء وما يُعبَّر عنه بقيمة الإرجاع يمكن تطبيقه كما هو من الفكرة الواردة في «الممارسة العمليّة لالتقاط الاستثناءات وتسجيلها ومعالجة الأخطاء».

بخصوص إرسال واستقبال JSON، يوفِّر System.Net.Http.Json عبر GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync طريقةً تُغنيك عن كتابة التسلسل (serialization) عبر نصّ بنفسك.

8. مطبّات خاصّة بتطبيقات Windows للأعمال

أخيراً، نجمع المطبّات الشائعة في العمل الفعليّ ببيئة Windows.

  • بطء أوّل طلب بسبب الكشف التلقائيّ عن البروكسي. بالإعداد الافتراضيّ في Windows، يستخدم HttpClient إعدادات بروكسي نظام التشغيل (بما فيها الكشف التلقائيّ). إن عُلِم أنّ البروكسي غير مطلوب، فتعطيله عبر HttpClientHandler.UseProxy = false يزيل انتظار الكشف2. وبالعكس، في البيئات التي يلزم فيها بروكسي داخليّ، يساعد التحديد الصريح عبر WebProxy في تجنّب حالة «يعمل على جهاز التطوير لكن لا يعمل على الخادم».
  • أنجِز إعداد البروكسي قبل أوّل طلب. لا تُطبَّق تغييرات إعدادات اتّصال الـ handler بعد إرسال طلب واحد ولو مرّة2.
  • القيمة الافتراضيّة لعدد الاتّصالات المتزامنة معاكسة تماماً بين .NET وNET Framework. في .NET (SocketsHttpHandler)، عدد الاتّصالات المتزامنة لـ HTTP/1.1 غير محدود افتراضيّاً، ما قد يؤدّي إلى تزايد الاتّصالات باستمرار عند كثرة الطلبات المتوازية والاصطدام بحدود جدار الحماية أو الخادم. في العمليّات ذات التوازي العالي، ضع حدّاً عبر MaxConnectionsPerServer2. أمّا في .NET Framework فالقيمة الافتراضيّة لـ ServicePointManager.DefaultConnectionLimit صغيرة (2 في البيئات غير ASP.NET)، ما يسبِّب مشكلة عكسيّة: انتظار الطلبات المتوازية داخليّاً وحدوث مهلة زمنيّة. عند رفع درجة التوازي في .NET Framework، ارفع هذا الحدّ صراحةً6.
  • سياق البروكسي وTLS يختلف عن المستخدم عند الاستدعاء من خدمة Windows. لا يملك حساب تشغيل الخدمة إعدادات بروكسي المستخدم أو بيانات اعتماده، ما يجعله سبباً شائعاً لعطل اتّصال من نوع «يعمل مع المستخدم التفاعليّ لكن لا يعمل مع الخدمة». راجع سياق التشغيل الخاصّ بالخدمات في «كيفيّة بناء خدمات Windows وتشغيلها».
  • لا تُضمِّن عنوان URL أو مفتاح API الخاصّ بالوجهة في الشيفرة. اجعل تبديل الوجهة عبر ملفّ إعدادات («الممارسة العمليّة لإدارة الإعدادات»)، واحفظ المعلومات السرّيّة باتّباع طريقة «تجنّب الإعدادات النصّيّة الصريحة عبر DPAPI».

الخلاصة

الجودة في العمل الفعليّ مع HttpClient تتحدَّد بـ«كيفيّة الاحتفاظ به» أكثر من «كيفيّة استدعائه». الإنشاء في كلّ طلب يسبِّب استنزاف المقابس، وجعله static بسذاجة يسبِّب عدم متابعة تغيّر DNS، وكلاهما لا يظهر أثناء التطوير. الجواب في .NET هو نسخة مُشترَكة مزوَّدة بـ PooledConnectionLifetime أو IHttpClientFactory، وفي .NET Framework هو إدخال IHttpClientFactory. بعد ذلك، حدِّد المهلة الزمنيّة صراحةً لكلّ وجهة، واترك إعادة المحاولة لمعالج resilience القياسيّ ── عندها فقط يصبح التطبيق مبنيّاً على افتراض «شبكة قد تفشل أحياناً».

مراجعة اتّصال تطبيق قائم (اتّصال يفشل فقط في ساعات الذروة، إعادة تنظيم تصميم المهلات الزمنيّة، تنفيذ جديد لتكامل واجهة API خارجيّة) غالباً ما تحتاج قراراً مبنيّاً على الشيفرة الفعليّة وبيئة التشغيل، فلا تتردَّد في التواصل معنا عند التردّد.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة Komura Soft LLC مع تطوير تطبيقات Windows للأعمال بما في ذلك التكامل مع واجهات API الخارجيّة، بالإضافة إلى الاستشارات التقنيّة حول تحقيق أعطال الاتّصال في التطبيقات القائمة (استنزاف المقابس، المهلات الزمنيّة، انقطاع الاتّصال المتقطّع) واتّجاه إصلاحها.

المراجع

  1. Microsoft Learn، Guidelines for using HttpClient. حول استخدام عميل طويل العمر مضبوط بـPooledConnectionLifetime أو عميل قصير العمر مُنشَأ عبر IHttpClientFactory في .NET Core/.NET 5 وما بعده، وتوصية استخدام IHttpClientFactory في .NET Framework، وضرورة تجنّب IHttpClientFactory في التطبيقات التي تستخدم الكوكيز بسبب مشاركة CookieContainer 2 3 4 5 6

  2. Microsoft Learn، HttpClient Class. حول تسبُّب الإنشاء في كلّ طلب باستنزاف المقابس وظهور SocketException، وأنّ DNS لا يُحلّ إلا عند إنشاء الاتّصال دون مراعاة TTL، وعدم إمكانيّة تغيير إعدادات اتّصال الـ handler بعد أوّل طلب، وأنّ عدد الاتّصالات المتزامنة لـ HTTP/1.1 غير محدود افتراضيّاً، والتوصية بالبثّ (streaming) في التنزيلات الكبيرة، والسلوك الافتراضيّ للبروكسي وتعطيله عبر UseProxy 2 3 4 5 6 7 8 9

  3. Microsoft Learn، IHttpClientFactory with .NET. حول أنّ العمر الافتراضيّ للـ handler دقيقتان، وأنّ HttpClient المُنشَأ عبر الـ factory يُفترَض استخدامه لمدّة قصيرة، وأنّ Dispose الخاصّ بالعميل المُنشَأ عبر الـ factory لا يتخلّص من الـ handler، وأنّ حقن عميل مُصنَّف بنوع في singleton يوقف متابعة تغيّر DNS.  2 3 4

  4. Microsoft Learn، Build resilient HTTP apps: Key development patterns. حول الاستراتيجيّات الخمس التي يُشكِّلها AddStandardResilienceHandler (محدِّد المعدّل/مهلة زمنيّة إجماليّة 30 ثانية/حدّ أقصى 3 محاولات إعادة بتراجع أُسّيّ/قاطع دارة/مهلة زمنيّة 10 ثوانٍ لكلّ محاولة)، ورموز الحالة والاستثناءات المستهدَفة (408/429/5xx)، وتعطيل إعادة المحاولة لـ POST وما شابه عبر DisableForUnsafeHttpMethods 2 3 4

  5. Microsoft Learn، HttpClient.Timeout Property. حول أنّ القيمة الافتراضيّة 100 ثانية، وأنّه ابتداءً من .NET 5 يُرمى عند المهلة الزمنيّة TaskCanceledException يحمل TimeoutException كاستثناء داخليّ.  2

  6. Microsoft Learn، ServicePointManager.DefaultConnectionLimit Property. حول أنّ القيمة الافتراضيّة لعدد الاتّصالات المتزامنة هي 10 في التطبيقات المُستضافة على ASP.NET، و2 فيما عداها (كتطبيقات سطح المكتب). 

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

HttpClient يُنفِّذ IDisposable، فلماذا لا يجوز إحاطته بـ using؟
المشكلة تكمن في نمط «الإنشاء والتخلّص في كلّ طلب». يحتفظ HttpClient داخليّاً بمجمّع اتّصالات (connection pool)، وهو مصمَّم على افتراض إعادة استخدامه طوال عمر التطبيق. عند إنشائه والتخلّص منه في كلّ مرّة، تبقى المقابس (sockets) في حالة TIME_WAIT لبعض الوقت حتّى بعد التخلّص منها، فتُستنزف المقابس المتاحة تحت الحمل المرتفع ويظهر SocketException. اجعل التخلّص منه مرّة واحدة فقط عند إنهاء التطبيق، أو احصل عليه عبر IHttpClientFactory. أمّا HttpClient الذي يُستلَم عبر الـ factory فإحاطته بـ using آمنة، لأنّ التخلّص منه (Dispose) لا يتخلّص من الـ handler الفعليّ.
هل ينبغي الانتقال من WebClient أو HttpWebRequest في .NET Framework؟
يُنصح بتوحيد الشيفرة الجديدة على HttpClient. فـ WebClient وHttpWebRequest واجهتان قديمتان أُبقيتا لأغراض التوافق، وتوصي Microsoft نفسها باستخدام HttpClient في أيّ تطوير جديد. لا حاجة لاستبدال الشيفرة القائمة دفعةً واحدة، لكن الانتقال التدريجيّ نحو HttpClient عند التعامل مع أجزاء الاتّصال يُسهِّل الصيانة من ناحية التحكّم بالمهلات، ودعم async، وسهولة الاختبار.
كم مرّة ينبغي إعادة المحاولة، وبأيّ فاصل زمنيّ؟
الأكثر أماناً هو الانطلاق من القيم الافتراضيّة للمعالج القياسيّ في Microsoft.Extensions.Http.Resilience (حدّ أقصى 3 محاولات، تراجع أُسّيّ (exponential backoff) مع jitter، وتأخير أوّليّ مقداره ثانيتان) بدل تحديدها بنفسك. الأهمّ من العدد هو تمييز «هل هذا طلب يجوز إعادة محاولته؟» - فالعمليّات مثل POST التي قد تسبّب تسجيلاً مزدوجاً عند إعادة تنفيذها يجب تعطيل إعادة المحاولة عليها افتراضيّاً، أو التأكّد من أنّ الخادم يضمن idempotency (أي أنّ استقبال نفس الطلب مرّتين لا يغيّر النتيجة) قبل تفعيل إعادة المحاولة.
لماذا يكون أوّل طلب فقط بطيئاً جدّاً في بيئة بروكسي (proxy) داخليّة؟
بالإعداد الافتراضيّ في Windows، يحاول HttpClient الكشف التلقائيّ عن البروكسي، ما قد يستغرق وقتاً في أوّل اتّصال. في البيئات التي يُعرَف فيها أنّ البروكسي غير مطلوب (كالاتّصال الداخليّ بين خوادم)، يحسِّن الأمر تعطيلُ الكشف التلقائيّ بضبط UseProxy على false في HttpClientHandler. وبالعكس، في البيئات الداخليّة التي يلزم فيها بروكسي، يكون التحديد الصريح عبر WebProxy بدل الاعتماد على الكشف التلقائيّ أكثر استقراراً.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

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