الأدلة التقنية

تطبيقك خلف Reverse Proxy: عنوان الزائر الحقيقي وHTTPS والروابط الصحيحة

إذا كان تطبيقك يرى عنوان الـ Proxy بدلاً من عنوان الزائر، أو يولد روابط http مع أن موقعك يعمل بـ HTTPS، فستعرف هنا السبب والإعداد الصحيح لإطار عملك دون أن تفتح باباً لانتحال العناوين.

تطبيقك خلف Reverse Proxy: عنوان الزائر الحقيقي وHTTPS والروابط الصحيحة

عندما تنشر تطبيقك خلف Reverse Proxy فغالباً سوف يعمل النطاق Domain ويظهر القفل في المتصفح من أول مرة، وتظن أن المهمة انتهت، ثم تفتح السجلات Logs بعد يوم فتجد كل الطلبات قادمة من عنوان واحد مثل 172.18.0.2، ويشتكي مستخدم من أن تسجيل الدخول يدور في حلقة لا تنتهي، ويصلك من آخر أن رسالة البريد التي أرسلها التطبيق فيها رابط يبدأ بـ http:// مع أن موقعك يعمل بـ HTTPS.

مخطط يبين مسار الطلب من جهاز الزائر بعنوان 203.0.113.50 إلى Cloudflare الذي يضيف CF-Connecting-IP، ثم إلى ال Reverse Proxy الذي يضيف X-Forwarded-For وX-Real-IP وX-Forwarded-Proto، ثم إلى التطبيق الذي يرى في الاتصال عنوان ال Proxy 172.18.0.2 ويقرأ عنوان الزائر من الترويسة، وفي الأسفل ترويسة X-Forwarded-For مزيفة يتجاهلها التطبيق لأنه لا يثق إلا بال Proxy
مسار الطلب من الزائر إلى التطبيق، وما تضيفه كل محطة، ولماذا يتجاهل التطبيق الترويسة المزيفة

والكود Code لم يتغير، وإنما تغير الطرف الذي يتصل بالتطبيق، فالتطبيق لم يعد يكلم المتصفح مباشرة، بل يكلم ال Reverse Proxy الذي استلم اتصال HTTPS وفك تشفيره، ثم أرسل الطلب إلى التطبيق عبر HTTP عادي من داخل شبكة Docker Network، وبالتالي يرى التطبيق عنوان ال Proxy وبروتوكوله، ولا يعرف عن الزائر إلا ما يخبره به ال Proxy في الترويسات Headers. وفي دليل شبكات Docker ذكرنا أننا نضع ال Reverse Proxy والتطبيقات على شبكة proxy مشتركة دون أي منفذ منشور للتطبيقات، وهذا المقال هو الوجه الآخر لنفس النمط، أي ما يحتاجه التطبيق نفسه حتى يعمل صحيحاً خلف هذا ال Proxy.

وسوف نناقش في هذا المقال ما يلي:

  • ما الذي تحمله ترويسات X-Forwarded-*، وكيف يقرؤها تطبيقك دون أن يصدق ترويسة كتبها مهاجم.
  • ما الذي يجب أن يرسله ال Proxy نفسه في Nginx وNginx Proxy Manager وTraefik وCaddy.
  • الإعداد الصحيح في Express وDjango وLaravel وASP.NET Core وSpring Boot وGo وPHP.
  • Cloudflare أمام ال Proxy، والعنوان العام والمسارات الفرعية وروابط OAuth.
  • WebSocket وSSE ومهلة الطلبات البطيئة، ثم كيف تتحقق مما يصل فعلاً، والمشكلات الشائعة وحلولها.

وإذا لم تثبت Reverse Proxy بعد، فابدأ بدليل Nginx Proxy Manager أو Traefik أو Caddy، وتجد الفروق بينها في مقارنة أدوات ال Reverse Proxy، أما بقية ما يلزم قبل نشر تطبيقك ففي قائمة تحقق المطور قبل نشر تطبيقه.

كيف يصل الطلب إلى تطبيقك؟

في الإعداد المعتاد يمر الطلب بثلاث محطات أو أربع كما في الصورة أعلاه، وهي كما يلي:

  1. الزائر يفتح https://app.example.com من جهازه، وعنوانه العام مثلاً 203.0.113.50.
  2. Cloudflare، إذا كان النطاق يمر به (السحابة البرتقالية Orange Cloud)، حيث يفك TLS ثم يفتح اتصالاً جديداً بسيرفرك من أحد عناوينه.
  3. ال Reverse Proxy على سيرفرك، مثل Nginx Proxy Manager أو Traefik أو Caddy، ويستقبل الاتصال على المنفذ 443 ثم يفتح اتصالاً جديداً بالتطبيق، وعنوانه داخل شبكة Docker مثلاً 172.18.0.2.
  4. ال Container الذي يعمل فيه التطبيق، على منفذ داخلي Port مثل 3000.

ولاحظ أن كل محطة تفتح اتصال TCP جديداً، لذلك لا يرى التطبيق في الاتصال نفسه إلا عنوان المحطة التي قبله مباشرة، أي ال Proxy، أما عنوان الزائر الأصلي والبروتوكول واسم النطاق فتنقلها كل محطة إلى التالية في ترويسات تضيفها إلى الطلب Request.

الترويسات التي تحمل معلومات الزائر

الترويسةما تحملهمثال
X-Forwarded-Forقائمة عناوين، تضيف كل محطة إلى آخرها عنوان من اتصل بها203.0.113.50, 198.51.100.20
X-Real-IPعنوان واحد يكتبه ال Proxy، ولا يتبع أي معيار رسمي203.0.113.50
X-Forwarded-Protoالبروتوكول الذي استخدمه الزائرhttps
X-Forwarded-Hostاسم النطاق الذي طلبه الزائرapp.example.com
X-Forwarded-Portالمنفذ الذي اتصل عليه الزائر443
Forwardedالترويسة القياسية التي تجمع كل ما سبق، ويعرفها المعيار RFC 7239for=203.0.113.50;proto=https;host=app.example.com
CF-Connecting-IPعنوان الزائر كما رآه Cloudflare، ويكتبه Cloudflare وحده203.0.113.50

وأهم ما تفهمه من هذا الجدول ترتيب X-Forwarded-For، فالعنوان الأول من اليسار هو ما ادعت المحطة الأولى أنه الزائر، والعنوان الأخير من اليمين هو آخر من مر به الطلب قبل ال Proxy الأخير، وكل Proxy يضيف إلى اليمين ولا يحذف شيئاً إلا إذا أعددته ليكتب الترويسة من جديد. والترويسة Forwarded هي المعيار الرسمي، ولكن أغلب الأدوات وأطر العمل Frameworks ما زالت تعتمد على ترويسات X-Forwarded-*، لذلك سوف نركز عليها في بقية المقال.

لماذا يهمك عنوان الزائر الحقيقي؟

إذا رأى التطبيق عنوان ال Proxy في كل الطلبات فسوف تتعطل أمور كثيرة دون أن يظهر لك أي خطأ، ومنها:

  • السجلات: لن تعرف من فعل ماذا، ولن تستطيع تتبع أي هجوم إلى مصدره.
  • الحد من معدل الطلبات Rate Limiting: يعامل التطبيق كل الزوار كأنهم عنوان واحد، فإذا تجاوز أحدهم الحد حظر الجميع، وإذا رفعت الحد لم يحظر أحداً.
  • سجل التدقيق Audit Log: لا قيمة له أمام المراجع إذا كان كل دخول مسجلاً من 172.18.0.2.
  • الموقع الجغرافي Geolocation: يظهر كل الزوار في مركز البيانات الذي فيه سيرفرك.
  • أدوات الحظر التلقائي مثل fail2ban: تحظر عنوان ال Proxy نفسه، فيتوقف الموقع عن الجميع.

الخطر: ترويسة يكتبها أي أحد

والحل الأول الذي يخطر على بال المطور أن يقرأ X-Forwarded-For مباشرة في الكود، أو أن يفعل في إطار العمل خيار الثقة بكل Proxy مثل trust proxy: true في Express، وهذا الحل غير مناسب إطلاقاً، والسبب أن هذه الترويسة نص عادي يرسله العميل Client، ويكتب فيها أي أحد ما يشاء:

curl -H 'X-Forwarded-For: 198.51.100.7' https://app.example.com/login

فإذا صدق التطبيق كل ما يصله، فسوف يسجل المهاجم دخوله بعنوان مزيف، ويتجاوز الحد من معدل الطلبات بتغيير الترويسة في كل محاولة، وقد يصل إلى صفحة مقصورة على عناوين معينة، وهذا ما يسمى انتحال العنوان IP Spoofing.

📌
في 27 سبتمبر 2026 نشر مشروع Zipline، وهو سيرفر لرفع الملفات ومشاركتها يستضيفه الكثيرون على سيرفراتهم، تحذيراً أمنياً يشرح أن تفعيل الخيار core.trustProxy، وهو الإعداد الذي ينصح به المشروع لكل من يشغله خلف Reverse Proxy أو CDN، يجعل Fastify يأخذ عنوان الزائر من أول قيمة في X-Forwarded-For، وبالتالي يستطيع المهاجم أن يغير الترويسة مع كل طلب فيحصل على عداد جديد للحد من معدل الطلبات في كل مرة، ويخمن الباسورد ورموز التحقق بخطوتين TOTP دون أي حد. وقد شمل التحذير الإصدار 4.7.0 وما قبله، ولم يكن فيه إصدار مصحح عند نشره. والدرس أن خيار «الثقة بال Proxy» بقيمة true هو بالضبط الخطأ الذي يشرحه هذا القسم، فلا تثق إلا بعنوان ال Proxy الذي تعرفه.

لذلك القاعدة الصحيحة هي: لا يقرأ التطبيق ترويسات X-Forwarded-* إلا إذا جاء الاتصال من Proxy تثق به، ويعرف التطبيق ذلك من عنوان الاتصال الفعلي، وهو الشيء الوحيد الذي لا يستطيع العميل تزويره، فإذا جاء الاتصال من عنوان ال Proxy قرأ الترويسة، وإذا جاء من أي عنوان آخر تجاهلها.

🛑
لا تنشر منفذ التطبيق على السيرفر (ports في Compose) إذا كان يعمل خلف Proxy، والسبب أن الإعداد الذي يثق بأول Proxy في الطريق يصبح ثغرة إذا استطاع المهاجم أن يتصل بالتطبيق مباشرة، لأن التطبيق سوف يعامله معاملة ال Proxy. لذلك اجعل ال Container على شبكة Docker الداخلية وحدها، ودع ال Proxy يصل إليه بالاسم.

أي عنوان تختار من X-Forwarded-For؟

لنفرض أن مهاجماً أرسل الترويسة المزيفة السابقة، وأن الطلب مر بـ Cloudflare ثم بال Proxy، فسوف يصل إلى التطبيق بهذا الشكل:

X-Forwarded-For: 198.51.100.7, 203.0.113.50, 172.71.0.10

العنوان الأول كتبه المهاجم بنفسه، والثاني أضافه Cloudflare وهو عنوان المهاجم الحقيقي، والثالث أضافه ال Proxy وهو عنوان Cloudflare. لذلك لا تأخذ العنوان الأول أبداً، وإنما اقرأ القائمة من اليمين وتخط كل عنوان تعرف أنه Proxy لديك، وأول عنوان لا تعرفه هو الزائر. وتطبق أطر العمل هذه الطريقة بإحدى صيغتين:

  • قائمة بالعناوين الموثوقة: عنوان ال Proxy أو شبكة Docker، ومعها نطاقات Cloudflare إن كان أمامك، وهذه الصيغة أدق لأنها لا تتأثر إذا أضفت محطة أو أزلتها.
  • عدد المحطات Hops: تخبر الإطار أن أمامه Proxy واحداً، أو اثنين مع Cloudflare، وهذه الصيغة أبسط ولكنها تفترض أن أحداً لا يصل إلى التطبيق إلا عبر هذه المحطات بالضبط.
⚠️
الثقة بشبكة Docker كلها، مثل 172.18.0.0/16، تشمل بوابة الشبكة Gateway 172.18.0.1، وقد يرى ال Proxy بعض الزوار من هذا العنوان، مثل زوار IPv6 عندما تكون شبكة Docker بدون IPv6، حيث يحولهم Docker إلى IPv4 عبر docker-proxy. وفي هذه الحالة يتخطى التطبيق البوابة لأنه يثق بها، فيأخذ العنوان الذي كتبه الزائر، لذلك الأدق أن تثق بعنوان ال Proxy وحده، أو بعدد المحطات، كلما استطعت.

لماذا يظن التطبيق أن الطلب جاء عبر HTTP؟

ينهي ال Proxy اتصال TLS عنده، وهذا ما يسمى إنهاء TLS أو TLS Termination، ثم يكلم التطبيق عبر HTTP، فإذا سأل التطبيق نفسه هل هذا الطلب آمن فالإجابة لا، ومن هنا تأتي خمس مشكلات معروفة:

  • حلقة التحويل Redirect Loop: إذا فعلت في التطبيق خيار «فرض HTTPS» فسوف يحول كل طلب إلى https://، فيعود الطلب عبر ال Proxy ويصل مرة أخرى عبر HTTP، فيحوله التطبيق من جديد، ويستمر ذلك حتى يظهر في المتصفح ERR_TOO_MANY_REDIRECTS.
  • ملفات تعريف الارتباط Cookies بدون Secure: بعض الأطر لا تضع العلامة Secure على ملف الجلسة Session إلا إذا كان الطلب آمناً، وبعضها لا يرسل ملف الجلسة أبداً إذا رأى الطلب HTTP، فيخرج المستخدم بعد الدخول مباشرة.
  • روابط تبدأ بـ http://: في رسائل البريد، وفي روابط إعادة تعيين كلمة المرور، وفي ترويسة Location عند التحويل.
  • المحتوى المختلط Mixed Content: تطلب الصفحة ملفات CSS أو JavaScript بروابط http://، فيحظرها المتصفح وتظهر الصفحة بدون تنسيق.
  • رفض رابط العودة في OAuth: يرسل التطبيق إلى مزود الدخول Identity Provider رابط عودة Redirect URI يبدأ بـ http://، فيرفضه المزود بخطأ مثل redirect_uri_mismatch.

والحل من جزأين، الأول أن يقرأ التطبيق X-Forwarded-Proto من ال Proxy الموثوق فقط، بنفس القاعدة التي طبقناها على العنوان، والثاني أن تعطي التطبيق عنوانه العام صراحة في متغير مثل APP_URL أو SITE_URL أو BASE_URL، فيبني منه كل رابط يرسله خارج الطلب الحالي، مثل رسائل البريد والمهام المجدولة Cron Jobs، وسوف تجد الإعدادين لكل إطار في القسم التالي.

ما الذي ينبغي أن يرسله ال Proxy؟

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

Nginx

إذا كنت تكتب إعداد Nginx بنفسك، فهذه الأسطر هي ما تحتاجه في location الذي يمرر الطلب إلى التطبيق:

location / {
    proxy_pass http://app:3000;

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header Forwarded         "";
}

في الإعداد أعلاه لاحظ التالي:

  • كل ترويسة تكتب من جديد، فلا يصل إلى التطبيق شيء كتبه العميل.
  • استخدمنا $remote_addr لـ X-Forwarded-For لأن Nginx هنا أول محطة، أما المتغير $proxy_add_x_forwarded_for الشائع في الأمثلة فيضيف العنوان إلى ما أرسله العميل ولا يحذفه، وعندها يجب على التطبيق أن يقرأ القائمة من اليمين.
  • السطر الأخير يحذف الترويسة Forwarded، والسبب أن Nginx لا يمرر الترويسة إذا كانت قيمتها فارغة، وبدونه تصل ترويسة Forwarded من العميل كما هي إلى إطار قد يقرؤها.

Nginx Proxy Manager

يكتب Nginx Proxy Manager الترويسة X-Real-IP من جديد، ويضيف عنوان الزائر إلى X-Forwarded-For، ويرسل X-Forwarded-Proto، ولكنه في الإصدار 2.16.0 يمرر قيمة X-Forwarded-Proto التي يرسلها العميل بنفسه إذا كانت http أو https، لذلك فعل الخيار Force SSL في كل Proxy Host، حتى لا يصل إلى التطبيق إلا طلب جاء فعلاً عبر HTTPS. وتجد بقية الإعداد في دليل Nginx Proxy Manager.

Traefik وCaddy

يرسل Traefik الترويسات X-Forwarded-For وX-Real-Ip وX-Forwarded-Proto وX-Forwarded-Host تلقائياً، ولا يصدق ما أرسله العميل منها إلا إذا جاء الطلب من عنوان في forwardedHeaders.trustedIPs الخاص بنقطة الدخول EntryPoint، لذلك اترك هذا الخيار فارغاً ما لم تكن أمام Traefik محطة أخرى، ولا تستخدم forwardedHeaders.insecure في بيئة الإنتاج Production.

أما Caddy فيضبط الترويسات X-Forwarded-For وX-Forwarded-Proto وX-Forwarded-Host، ويتجاهل ما أرسله العميل منها إلا إذا كان عنوان العميل في trusted_proxies، ولكنه لا يضيف X-Real-IP، بل يمررها إلى التطبيق كما أرسلها العميل، فإذا كان تطبيقك يقرأ X-Real-IP فاكتبها في Caddy صراحة:

app.example.com {
	reverse_proxy app:3000 {
		header_up X-Real-IP {client_ip}
	}
}

الإعداد في إطار عملك

سوف نفترض في الأمثلة التالية أن ال Proxy والتطبيق على شبكة Docker عنوانها 172.18.0.0/16، وأن النطاق العام app.example.com، فضع شبكتك مكان هذه الشبكة، وتعرفها بالأمر التالي:

docker network inspect proxy --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'

وإذا أردت أن تثق بعنوان ال Proxy وحده، فثبت له عنواناً في الشبكة بالخيار ipv4_address في ملف Compose، واكتب هذا العنوان بدلاً من الشبكة كلها.

Node.js مع Express

يعتمد Express على الإعداد trust proxy، وقيمته الافتراضية false، وعندما تفعله تأخذ req.ip وreq.ips قيمتهما من X-Forwarded-For، وreq.protocol من X-Forwarded-Proto، وreq.hostname من X-Forwarded-Host.

const express = require('express');
const app = express();

// Proxy واحد أمام التطبيق، ومنفذ التطبيق غير منشور على الخادم
app.set('trust proxy', 1);

// أو: الثقة بشبكة الـ Proxy وحدها
// app.set('trust proxy', ['loopback', '172.18.0.0/16']);

الرقم 1 يعني محطة واحدة، فيأخذ Express أول عنوان من يمين X-Forwarded-For. وإذا كان أمامك Cloudflare ثم ال Proxy فاكتب 2، إلا إذا كان ال Proxy يستعيد عنوان الزائر من CF-Connecting-IP كما في قسم Cloudflare، فعندها يرى التطبيق محطة واحدة وتبقى القيمة 1. ولا تستخدم القيمة true أبداً، والسبب أنها تأخذ العنوان الأول من اليسار، أي العنوان الذي يكتبه العميل.

⚠️
إذا قرأت القيمة من متغير بيئة Environment Variable فحولها إلى رقم: Number(process.env.TRUST_PROXY_HOPS)، والسبب أن النص '1' ليس الرقم 1 عند Express، بل يفهمه عنواناً، فيبقى req.ip عنوان ال Proxy دون أي رسالة خطأ.

وبعد هذا الإعداد يصبح req.secure صحيحاً للطلبات القادمة عبر HTTPS، وهذا شرط ليعمل الخيار cookie.secure في express-session، لأنه لا يرسل ملف الجلسة إلا على طلب آمن.

Django

لا يوجد في Django إعداد يقرأ عنوان الزائر من X-Forwarded-For، فيبقى request.META["REMOTE_ADDR"] عنوان ال Proxy، أما البروتوكول واسم النطاق فلهما إعدادان رسميان في settings.py:

ALLOWED_HOSTS = ["app.example.com"]

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True

SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

TRUSTED_PROXY_NETWORKS = ["172.18.0.0/16"]

MIDDLEWARE = [
    "myproject.realip.RealIPMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ... بقية الـ Middleware كما هي
]

مع SECURE_PROXY_SSL_HEADER تعيد الدالة request.is_secure() القيمة True إذا وصلت الترويسة بالقيمة https، ومع USE_X_FORWARDED_HOST تبنى الروابط من X-Forwarded-Host. ولاحظ أن الإعدادين لا يتحققان من مصدر الطلب، بل يصدقان الترويسة أياً كان مرسلها، لذلك لا تفعلهما إلا إذا كان ال Proxy يكتب الترويسة من جديد، وكان التطبيق لا يقبل اتصالاً إلا منه.

أما عنوان الزائر فيكفيه Middleware صغير يقرأ X-Forwarded-For فقط إذا جاء الاتصال من شبكة موثوقة، ويمشي في القائمة من اليمين، فاحفظه في الملف myproject/realip.py:

import ipaddress

from django.conf import settings

TRUSTED = [ipaddress.ip_network(n) for n in settings.TRUSTED_PROXY_NETWORKS]


def is_trusted(addr):
    return any(addr in net for net in TRUSTED)


class RealIPMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        try:
            peer = ipaddress.ip_address(request.META.get("REMOTE_ADDR", ""))
        except ValueError:
            return self.get_response(request)
        if is_trusted(peer):
            chain = request.META.get("HTTP_X_FORWARDED_FOR", "").split(",")
            for item in reversed(chain):
                try:
                    addr = ipaddress.ip_address(item.strip())
                except ValueError:
                    break
                if not is_trusted(addr):
                    request.META["REMOTE_ADDR"] = str(addr)
                    break
        return self.get_response(request)

ضع هذا ال Middleware أول القائمة حتى يرى كل ما بعده العنوان الصحيح، وإذا وجد في القائمة قيمة ليست عنواناً صالحاً فإنه يتوقف ويبقي عنوان ال Proxy. وإذا فضلت مكتبة جاهزة فهناك django-ipware، وعليك فيها أن تمرر عناوين ال Proxy أو عددها بنفسك.

Laravel

في Laravel يأتي ال Middleware المسمى TrustProxies جاهزاً، ومنذ الإصدار 11 تضبطه في الملف bootstrap/app.php كما في توثيق Laravel 13:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: [
        '172.18.0.0/16',
    ]);
})

وبعدها تعيد $request->ip() عنوان الزائر، ويبني ال Helper url() روابط https://، أما الروابط التي تبنى خارج الطلب، مثل رسائل البريد من ال Queue، فثبت لها العنوان العام في .env:

APP_URL=https://app.example.com

ويذكر التوثيق القيمة at: '*' للثقة بكل عنوان، وهذه القيمة مقصودة لل Load Balancer السحابي الذي لا تعرف عناوينه، فلا تستخدمها على سيرفرك. ولا تلجأ إلى URL::forceScheme('https') بديلاً عن الإعداد الصحيح، والسبب أنها تصلح الروابط فقط وتترك العنوان والجلسات على حالها.

ASP.NET Core

يعالج ASP.NET Core الترويسات بال Middleware المسمى Forwarded Headers، ولا يثق افتراضياً إلا بعنوان الجهاز المحلي Loopback، وفي .NET 10 أصبحت القائمة KnownNetworks مهملة Obsolete وحلت محلها KnownIPNetworks التي تستخدم النوع System.Net.IPNetwork، والإعداد في Program.cs كما يلي:

using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
                             | ForwardedHeaders.XForwardedProto
                             | ForwardedHeaders.XForwardedHost;
    options.KnownIPNetworks.Clear();
    options.KnownProxies.Clear();
    options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("172.18.0.0/16"));
    options.AllowedHosts.Add("app.example.com");
});

var app = builder.Build();

app.UseForwardedHeaders();
// بعده: UseHsts وUseHttpsRedirection وUseAuthentication وبقية الـ Middleware

في الإعداد أعلاه لاحظ التالي:

  • اكتب System.Net.IPNetwork باسمه الكامل، والسبب أن النوع القديم بنفس الاسم ما زال موجوداً في Microsoft.AspNetCore.HttpOverrides.
  • ضع UseForwardedHeaders قبل كل Middleware يعتمد على العنوان أو البروتوكول.
  • AllowedHosts يحصر قيم X-Forwarded-Host المقبولة، وإذا وصلت قيمة غيرها بقي اسم المضيف الأصلي.

ويعالج الإطار افتراضياً عنواناً واحداً من يمين القائمة، لأن قيمة ForwardLimit هي 1، فإذا كان أمامك Cloudflare فاجعلها 2 وأضف نطاقات Cloudflare إلى KnownIPNetworks. أما المتغير ASPNETCORE_FORWARDEDHEADERS_ENABLED=true فيفعل المعالجة دون أي قائمة ثقة، لذلك لا يناسب سيرفراً يصل إليه الإنترنت مباشرة.

Spring Boot

الخيار server.forward-headers-strategy يحدد من يعالج الترويسات، وقيمته الافتراضية none خارج المنصات السحابية المعروفة، فاختر native مع Tomcat، وعندها يقرأ سيرفر الويب نفسه X-Forwarded-For وX-Forwarded-Proto، ويثق بالعناوين التي يحددها internal-proxies، والإعداد في application.yml:

server:
  forward-headers-strategy: native
  tomcat:
    remoteip:
      internal-proxies: "172\\.18\\.\\d{1,3}\\.\\d{1,3}"

والقيمة الافتراضية لـ internal-proxies تعبير نمطي Regular Expression يشمل عناوين الجهاز المحلي والعناوين الخاصة، ومنها 10.0.0.0/8 و192.168.0.0/16 و172.16.0.0/12، لذلك يعمل الإعداد داخل Docker بالسطر الأول وحده، ولكن حصره في شبكتك أدق. وبعدها تعيد request.getRemoteAddr() عنوان الزائر، وتعيد request.isSecure() القيمة true لطلبات HTTPS.

أما القيمة framework فتستخدم الفلتر ForwardedHeaderFilter من Spring، وهو يدعم Forwarded وX-Forwarded-Prefix، ولكن توثيق Spring يوضح أنه لا يتحقق من مصدر الترويسات، ويترك لل Proxy أن يحذف ما يرسله العميل، لذلك ابق على native ما لم تحتج إلى هاتين الترويستين.

Go

مكتبة net/http لا تقرأ أياً من هذه الترويسات، فيبقى r.RemoteAddr عنوان ال Proxy ويبقى r.TLS فارغاً، ويكتب كثيرون دالة تأخذ أول قيمة من X-Forwarded-For، وهذا هو الخطأ الذي شرحناه. وكانت الحزمة chi تقدم middleware.RealIP لهذا الغرض، ولكن وثائقها الحالية تصفها بأنها مهملة Deprecated ومعرضة للانتحال، والسبب أنها تصدق الترويسات دون أن تنظر إلى مصدر الاتصال.

والمثال التالي Middleware بالمكتبة القياسية وحدها، يقرأ القائمة فقط إذا جاء الاتصال من شبكة موثوقة:

package main

import (
	"context"
	"net"
	"net/http"
	"net/netip"
	"strings"
)

var trustedProxies = []netip.Prefix{
	netip.MustParsePrefix("172.18.0.0/16"),
}

type clientIPKey struct{}

func isTrusted(a netip.Addr) bool {
	for _, p := range trustedProxies {
		if p.Contains(a) {
			return true
		}
	}
	return false
}

func RealIP(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		host, _, _ := net.SplitHostPort(r.RemoteAddr)
		client, err := netip.ParseAddr(host)
		if err != nil {
			http.Error(w, "bad remote address", http.StatusBadRequest)
			return
		}
		client = client.Unmap()
		if isTrusted(client) {
			hops := strings.Split(strings.Join(r.Header.Values("X-Forwarded-For"), ","), ",")
			for i := len(hops) - 1; i >= 0; i-- {
				a, err := netip.ParseAddr(strings.TrimSpace(hops[i]))
				if err != nil {
					break
				}
				client = a.Unmap()
				if !isTrusted(client) {
					break
				}
			}
		}
		ctx := context.WithValue(r.Context(), clientIPKey{}, client)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func ClientIP(r *http.Request) netip.Addr {
	a, _ := r.Context().Value(clientIPKey{}).(netip.Addr)
	return a
}

في الكود أعلاه لاحظ أن الدالة Unmap تحول العناوين بصيغة ::ffff:172.18.0.2 إلى IPv4، وبدونها لن تطابق الشبكة الموثوقة. وإذا كنت تستخدم chi فإصداراتها الحديثة فيها بدائل مثل ClientIPFromXFF("172.18.0.0/16")، ولكن هذه البدائل لا تنظر في r.RemoteAddr، فهي تفترض أن أحداً لا يصل إلى التطبيق إلا عبر ال Proxy. أما البروتوكول فلا تعتمد على r.TLS في بناء الروابط، بل خذ العنوان العام من متغير مثل BASE_URL.

PHP مع Nginx وPHP-FPM

إذا كان Container التطبيق يجمع Nginx مع PHP-FPM، كما في كثير من ال Images الخاصة بـ WordPress وLaravel، فالأسهل أن يصحح Nginx الداخلي العنوان قبل أن يصل إلى PHP، وهذا عمل الوحدة Module المسماة realip، وهي موجودة في ال Image الرسمي لـ Nginx:

set_real_ip_from  172.18.0.0/16;
real_ip_header    X-Forwarded-For;
real_ip_recursive on;

map $http_x_forwarded_proto $fastcgi_https {
    default "";
    https   on;
}

server {
    listen 80;
    # ...
    location ~ \.php$ {
        include       fastcgi_params;
        fastcgi_param HTTPS $fastcgi_https;
        fastcgi_pass  127.0.0.1:9000;
    }
}

في الإعداد أعلاه لاحظ التالي:

  • set_real_ip_from لا يقبل الترويسة إلا من الشبكة الموثوقة.
  • real_ip_recursive on يجعل الوحدة تمشي في القائمة من اليمين وتتخطى العناوين الموثوقة، ولولاه لأخذت العنوان الأخير فقط، فيصل إلى PHP عنوان الزائر في $_SERVER['REMOTE_ADDR']، وتصل القيمة on في $_SERVER['HTTPS'] للطلبات الآمنة.
  • map لا تتحقق من المصدر، وتصح هنا لأن ال Container لا يصل إليه إلا ال Proxy.

إذا كان Cloudflare أمام ال Proxy

Cloudflare محطة إضافية، فيرى ال Proxy على سيرفرك أن كل الطلبات جاءت من عناوين Cloudflare، ويرسل Cloudflare عنوان الزائر في CF-Connecting-IP ويضيفه إلى X-Forwarded-For. ونفس القاعدة تنطبق هنا، فلا تصدق CF-Connecting-IP إلا من نطاقات Cloudflare المنشورة، وإلا كتبها أي أحد يصل إلى سيرفرك مباشرة. والأفضل أن تستعيد العنوان في ال Proxy نفسه، فيرى التطبيق ما كان سيراه دون Cloudflare، كما يشرح توثيق Cloudflare.

Nginx

الأمر التالي يولد ملفاً بنطاقات Cloudflare الحالية، ثم يتحقق من الإعداد ويعيد التحميل:

{
  for range in $(curl -fsS https://www.cloudflare.com/ips-v4) $(curl -fsS https://www.cloudflare.com/ips-v6); do
    echo "set_real_ip_from $range;"
  done
  echo "real_ip_header CF-Connecting-IP;"
} | sudo tee /etc/nginx/conf.d/cloudflare-realip.conf
sudo nginx -t && sudo systemctl reload nginx

وبعدها يصبح $remote_addr عنوان الزائر، فتصل القيمة الصحيحة في X-Real-IP وX-Forwarded-For من إعداد location السابق، ولاحظ أن نطاقات Cloudflare تتغير أحياناً، لذلك أعد توليد الملف دورياً بمهمة مجدولة.

Nginx Proxy Manager

يجلب Nginx Proxy Manager نطاقات Cloudflare وCloudFront عند الإقلاع ويثق بها، ما لم تعطل ذلك بالمتغير IP_RANGES_FETCH_ENABLED، ولكنه في الإصدار 2.16.0 يقرأ العنوان من X-Real-IP، وCloudflare لا يرسل هذه الترويسة، وبالتالي يبقى عنوان Cloudflare هو ما يصل إلى تطبيقك. والحل سطر واحد في تبويب Advanced الخاص بال Proxy Host:

real_ip_header CF-Connecting-IP;

هذا السطر يغير الترويسة التي يقرأ منها NPM العنوان في هذا المضيف وحده، ويبقي قائمة الثقة كما هي. ولاحظ أن NPM يثق افتراضياً بالشبكات الخاصة أيضاً، أي 10.0.0.0/8 و172.16.0.0/12 و192.168.0.0/16، فإذا كان NPM في شبكة منزلية فأي جهاز فيها يستطيع أن يكتب الترويسة.

Traefik وCaddy

في Traefik أضف نطاقات Cloudflare إلى forwardedHeaders.trustedIPs في نقطة الدخول، وعندها يحتفظ بقائمة X-Forwarded-For القادمة من Cloudflare ويضيف إليها، وفي هذه الحالة أمام التطبيق محطتان، فاجعل الإطار يثق بعدد 2 أو بنطاقات Cloudflare مع شبكة Docker.

أما في Caddy فتستطيع أن تجعل ال Proxy يستعيد العنوان بنفسه، ثم يكتبه للتطبيق في X-Real-IP:

{
	servers {
		trusted_proxies static 173.245.48.0/20 103.21.244.0/22
		client_ip_headers CF-Connecting-IP
	}
}

app.example.com {
	reverse_proxy app:3000 {
		header_up X-Real-IP {client_ip}
	}
}

والنطاقان في المثال من قائمة Cloudflare للتوضيح فقط، فاكتب القائمة كاملة من ips-v4 وips-v6، وعندها يقبل Caddy CF-Connecting-IP من هذه النطاقات وحدها، ويضع العنوان في {client_ip}.

مع Cloudflare Tunnel

إذا كنت تستخدم Cloudflare Tunnel، كما في دليل تشغيل خادمك من إنترنت المنزل، فالطلب لا يصل من نطاقات Cloudflare العامة، وإنما من ال Container الذي يشغل cloudflared داخل شبكة Docker، لذلك المحطة الموثوقة هنا هي عنوان cloudflared، وما زال Cloudflare يرسل CF-Connecting-IP بعنوان الزائر.

العنوان العام واسم المضيف والمسار الفرعي

اسم المضيف Host

أغلب أدوات ال Proxy، ومنها Traefik وCaddy وNPM، تمرر الترويسة Host كما طلبها الزائر، وفي Nginx تحتاج إلى السطر proxy_set_header Host $host، فإذا كانت Host صحيحة فغالباً لا حاجة إلى X-Forwarded-Host. وإذا فعلت قراءتها في الإطار فاقرنها بقائمة الأسماء المسموحة: ALLOWED_HOSTS في Django، وAllowedHosts في ASP.NET Core، وtrustHosts في Laravel، والسبب أنه بدون هذه القائمة يستطيع مهاجم أن يجعل التطبيق يرسل رابط إعادة تعيين كلمة المرور إلى نطاقه هو.

ولا تبن الروابط من الطلب وحده، فالمهمة المجدولة وال Queue لا يوجد لهما طلب أصلاً، لذلك عرف العنوان العام في متغير ثابت، ومن أمثلته APP_URL في Laravel، وurl في Ghost، وROOT_URL في Gitea، وSITE_URL أو BASE_URL في كثير من التطبيقات.

التشغيل تحت مسار فرعي مثل /app

قد تريد تشغيل التطبيق على https://example.com/app بدلاً من نطاق فرعي Subdomain، ولذلك طريقتان، الأولى أن يحذف ال Proxy البادئة قبل التمرير، مثل StripPrefix في Traefik أو handle_path في Caddy، فيظن التطبيق أنه على /، ولكنه سوف يولد عندها روابط مثل /login و/static/app.css بدون البادئة، فتفتح صفحة خاطئة. والثانية أن يعرف التطبيق مساره الأساسي Base Path، مثل FORCE_SCRIPT_NAME في Django، وUsePathBase في ASP.NET Core، وserver.servlet.context-path في Spring Boot، ولاحظ أن StripPrefix في Traefik يرسل البادئة المحذوفة في X-Forwarded-Prefix، وبعض الأطر تقرؤها.

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

روابط العودة في OAuth وOIDC

يقارن مزود الدخول رابط العودة الذي يرسله التطبيق بالرابط المسجل لديه حرفاً بحرف، كما توصي أفضل ممارسات أمان OAuth، ويكفي اختلاف بين http وhttps، أو منفذ زائد مثل :3000، أو شرطة مائلة في الآخر، حتى يرفض المزود الطلب. لذلك سجل الرابط العام عند المزود، مثل https://app.example.com/auth/callback، وتأكد أن التطبيق يولد نفس الرابط بعد إعداد ال Proxy، والأضمن أن تكتب رابط العودة صراحة في إعداد التطبيق إن كان يسمح بذلك.

WebSocket وSSE

WebSocket

يبدأ اتصال WebSocket بطلب HTTP عادي يحمل الترويستين Upgrade: websocket وConnection: Upgrade، وال Proxy لا يمرر هاتين الترويستين إلى المحطة التالية إلا إذا طلبت منه ذلك، ففي Nginx تحتاج إلى إعداد صريح:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    # ...
    location /ws/ {
        proxy_pass http://app:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_read_timeout 1h;
    }
}

وفي Nginx Proxy Manager يضيف الخيار Websockets Support هذه الترويسات، أما Traefik وCaddy فيدعمان WebSocket تلقائياً دون إعداد، ولكن Caddy يغلق اتصالات WebSocket القائمة مع كل إعادة تحميل للإعداد، ما لم تضبط stream_close_delay.

والمشكلة الثانية هي المهلة Timeout، فالقيمة الافتراضية لـ proxy_read_timeout في Nginx هي 60 ثانية، ويرفعها NPM إلى 90 ثانية، فإذا لم يرسل الطرفان شيئاً خلالها أغلق Nginx الاتصال. والحل الأفضل أن يرسل التطبيق نبضة Heartbeat كل 30 ثانية مثلاً، فلا يبقى الاتصال صامتاً. ويدعم Cloudflare اتصالات WebSocket في كل الخطط، ولكنه يغلق الاتصال الخامل، وقد يقطع الاتصالات عندما يحدث سيرفراته، لذلك اجعل العميل يعيد الاتصال تلقائياً إذا انقطع.

أحداث الخادم (SSE)

في أحداث الخادم Server-Sent Events يبقى الرد مفتوحاً، ويكتب فيه التطبيق حدثاً بعد حدث، فإذا جمع ال Proxy الرد في ذاكرة مؤقتة Buffering أو ضغطه Compression قبل إرساله، وصلت الأحداث إلى المتصفح دفعة واحدة في النهاية، ويحدث هذا مثلاً في Nginx إذا فعلت gzip للنوع text/event-stream. ولذلك حلان، الأول من التطبيق، حيث يرسل الترويسة X-Accel-Buffering: no مع الرد فيلغي Nginx التجميع لهذا الرد وحده، والثاني من Nginx:

location /events {
    proxy_pass http://app:3000;
    proxy_buffering off;
    proxy_read_timeout 1h;
}

الخيار proxy_buffering off يعطل التجميع في هذا المسار، وproxy_read_timeout يمنع إغلاق الاتصال بعد دقيقة، أما Caddy فيرسل ردود text/event-stream فور وصولها دون إعداد.

الطلبات البطيئة وخطأ 504

إذا تأخر رد التطبيق أكثر من مهلة ال Proxy، توقف ال Proxy عن الانتظار وأعاد للزائر 504 Gateway Timeout، مع أن التطبيق قد يكمل العمل في الخلفية، ومهلة Nginx الافتراضية 60 ثانية كما ذكرنا. وإذا كان Cloudflare أمامك فهو ينتظر رد سيرفرك 125 ثانية ثم يعيد الخطأ 524، ولا تستطيع رفع هذه المهلة إلا في خطة Enterprise.

ورفع المهلة حل مؤقت، والحل الأنسب أن ينقل التطبيق العمل الطويل، مثل تقرير كبير أو تصدير بيانات، إلى مهمة في الخلفية Background Job، فيرد فوراً برقم المهمة، ثم يسأل المتصفح عن حالتها. أما خطأ 413 ورفع الملفات الكبيرة فتجد تفاصيلهما في مقال حدود حجم رفع الملفات.

كيف تتحقق مما يصل إلى تطبيقك؟

لا تخمن، وإنما انظر إلى ما يصل فعلاً، والأسهل أن تشغل Container صغيراً يعرض كل ترويسة يستلمها، مثل traefik/whoami، على نفس شبكة ال Proxy، وتربطه مؤقتاً بنطاق تجريبي:

services:
  whoami:
    image: traefik/whoami:v1.12.0
    networks: [proxy]

networks:
  proxy:
    external: true

يريك whoami ما يرسله ال Proxy، ولكنه لا يريك ما يستنتجه إطار عملك منه، لذلك أضف إلى تطبيقك نقطة نهاية Endpoint مؤقتة تعرض القيم المحسوبة، وفي Express مثلاً:

app.get('/debug/request', (req, res) => {
  res.json({
    peer: req.socket.remoteAddress,
    ip: req.ip,
    ips: req.ips,
    protocol: req.protocol,
    hostname: req.hostname,
    xForwardedFor: req.get('x-forwarded-for') ?? null,
  });
});
⚠️
احذف نقطة النهاية هذه بعد الاختبار، وأوقف whoami، والسبب أن عرض الترويسات للعموم قد يكشف ملفات تعريف الارتباط وترويسة Authorization وعناوين شبكتك الداخلية.

الآن أرسل من جهازك طلبين، أحدهما عادي والآخر بترويسات مزيفة:

curl -s https://app.example.com/debug/request
curl -s -H 'X-Forwarded-For: 198.51.100.7' -H 'X-Real-IP: 198.51.100.7' -H 'X-Forwarded-Host: evil.example' https://app.example.com/debug/request

وفي الحالتين يجب أن يكون ip عنوانك العام، وprotocol هو https، وhostname هو app.example.com. فإذا كان التطبيق خلف Nginx Proxy Manager مع trust proxy بالقيمة 1، فالمخرج للطلب المزيف سوف يكون كما يلي:

{
  "peer": "::ffff:172.18.0.2",
  "ip": "203.0.113.50",
  "ips": ["203.0.113.50"],
  "protocol": "https",
  "hostname": "app.example.com",
  "xForwardedFor": "198.51.100.7, 203.0.113.50"
}

ولاحظ أن NPM يضيف العنوان إلى القائمة، فيظهر العنوان المزيف في القائمة الخام، ولكن الإطار يتجاهله. فإذا ظهر 198.51.100.7 في ip فتطبيقك يصدق ما يكتبه العميل، فراجع قسم إطار عملك، وإذا ظهر 172.18.0.2 فالتطبيق لا يثق بال Proxy بعد. ولاحظ أيضاً أن Node.js يعرض عنوان الاتصال بصيغة ::ffff:، وأن Express يطابقها مع 172.18.0.0/16 دون مشكلة.

مشكلات شائعة وحلولها

يسجل التطبيق 172.18.0.x لكل الزوار

التطبيق لا يثق بال Proxy بعد، أو يثق بعنوان آخر، فاعرف عنوان ال Proxy الفعلي بالأمر docker network inspect proxy وقارنه بالقيمة في إعداد إطارك. وتذكر أن بعض الأطر ترى العنوان بصيغة IPv6 مثل ::ffff:172.18.0.2، فإذا كان إطارك لا يطابقها مع شبكة IPv4 فأضف الصيغتين أو حول العنوان قبل المقارنة.

يظهر 172.18.0.1 حتى في سجلات ال Proxy

هذا عنوان بوابة شبكة Docker، ويظهر غالباً عندما يصل الزائر عبر IPv6 إلى منفذ منشور على شبكة Docker لا تدعم IPv6، فيمرر docker-proxy الاتصال ويضيع العنوان الأصلي، ويظهر أيضاً للطلبات التي ترسلها من السيرفر نفسه إلى 127.0.0.1. والحل أن تفعل IPv6 على شبكة ال Proxy كما في دليل شبكات Docker، أو أن تشغل ال Proxy بالشبكة host، ولا تضع البوابة في قائمة الثقة.

يظهر في السجلات عنوان كتبه الزائر

جرب الطلب المزيف من قسم التحقق، فإذا نجح الانتحال فالسبب واحد من ثلاثة: الأول أن الإطار يأخذ العنوان الأول من اليسار، مثل trust proxy: true في Express، والثاني أن الثقة واسعة جداً، مثل at: '*' في Laravel، والثالث أن منفذ التطبيق منشور على السيرفر، فيصل إليه المهاجم دون المرور بال Proxy.

ERR_TOO_MANY_REDIRECTS بعد تفعيل HTTPS في التطبيق

التطبيق لا يقرأ X-Forwarded-Proto، فيظن أن كل طلب جاء عبر HTTP ويحوله من جديد، لذلك فعل الثقة بالبروتوكول في إطارك، مثل SECURE_PROXY_SSL_HEADER في Django أو ForwardedHeaders.XForwardedProto في ASP.NET Core. وإذا كان Cloudflare أمامك بوضع SSL المسمى Flexible فهو يكلم سيرفرك عبر HTTP، فغيره إلى Full (Strict).

الروابط والتحويلات تبدأ بـ http://

السبب نفسه، أو أن الرابط يبنى خارج الطلب كما في رسائل البريد من ال Queue، فعل الثقة بالبروتوكول، وثبت العنوان العام في APP_URL أو ما يقابله في تطبيقك. وإذا ظهر في المتصفح تحذير المحتوى المختلط، فافتح أدوات المطور Developer Tools وابحث عن الملفات التي تحمل بـ http://.

يرفض مزود الدخول الطلب بخطأ redirect_uri_mismatch

افتح رابط الدخول، وانسخ قيمة redirect_uri من شريط العنوان، وقارنها بالرابط المسجل عند المزود حرفاً بحرف، فإذا كانت تبدأ بـ http:// أو فيها منفذ داخلي فالتطبيق لا يقرأ ترويسات ال Proxy، أو لا يعرف عنوانه العام.

ينقطع اتصال WebSocket كل دقيقة تقريباً

هذه مهلة proxy_read_timeout في Nginx أو NPM، فاجعل التطبيق يرسل نبضة أقصر من المهلة، أو ارفع المهلة لمسار WebSocket وحده. وإذا كان الانقطاع يحدث مع كل تعديل في إعداد Caddy فالسبب إعادة التحميل، وعلاجه الخيار stream_close_delay.

تصل أحداث SSE دفعة واحدة

هناك شيء يجمع الرد قبل إرساله، وغالباً هو الضغط أو التجميع في ال Proxy، أو Middleware للضغط في التطبيق نفسه، فأرسل X-Accel-Buffering: no من التطبيق، أو أضف proxy_buffering off للمسار، واستثن text/event-stream من الضغط.

يظهر عنوان Cloudflare بدلاً من عنوان الزائر

ال Proxy لا يستعيد العنوان من CF-Connecting-IP، فاتبع قسم Cloudflare للأداة التي تستخدمها، وتذكر أن NPM يحتاج إلى السطر real_ip_header CF-Connecting-IP في تبويب Advanced.

504 أو 524 للطلبات الطويلة

الرمز 504 يأتي من ال Proxy على سيرفرك، والرمز 524 من Cloudflare بعد 125 ثانية، فارفع مهلة ال Proxy للمسار البطيء وحده إن لزم، ولكن الحل الدائم أن تنقل العمل الطويل إلى مهمة في الخلفية.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • التطبيق خلف Reverse Proxy يرى في الاتصال عنوان ال Proxy دائماً، وعنوان الزائر والبروتوكول واسم النطاق يصله في ترويسات X-Forwarded-*.
  • لا تثق بالترويسة إلا إذا جاء الاتصال من ال Proxy الذي تعرفه، واقرأ X-Forwarded-For من اليمين، ولا تستخدم trust proxy: true أو at: '*' على سيرفرك، والسبب أن العنوان الأول من اليسار يكتبه العميل.
  • أول Proxy يستقبل الطلب من الإنترنت يكتب الترويسات من جديد، ولا تنشر منفذ التطبيق على السيرفر حتى لا يتجاوز أحد هذا ال Proxy.
  • ثبت العنوان العام للتطبيق في متغير مثل APP_URL، فهو مصدر الروابط في البريد وال Queue وروابط العودة في OAuth.
  • خلف Cloudflare استعد عنوان الزائر من CF-Connecting-IP في ال Proxy نفسه، وفي NPM يكفي سطر real_ip_header CF-Connecting-IP في تبويب Advanced.
  • WebSocket وSSE يحتاجان إلى مهلة أطول ونبضة من التطبيق، والطلب الذي يتجاوز 125 ثانية خلف Cloudflare مكانه مهمة في الخلفية.

سجل التحديثات (Changelog)

  • أكتوبر 2026: كتابة المقال.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.