> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# لماذا يفشل رفع الملفات الكبيرة؟ حدود الحجم في Cloudflare والـ Reverse Proxy والتطبيق
- URL: https://arabroot.io/articles/حدود-حجم-رفع-الملفات/
- Published: 2026-10-04T10:15:00.000Z
- Updated: 2026-10-05T06:20:51.000Z
- Description: يمر الملف المرفوع بعدة طبقات، ولكل طبقة حد للحجم وحد للزمن، والحد الأصغر هو الذي يقرر، وفي هذا المقال سوف تعرف الطبقة التي رفضت الملف من شكل الخطأ، وأين تعدل الحد، وكيف تصمم رفعاً يتحمل الملفات الكبيرة.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, DevOps وCI/CD, الاستضافة الذاتية

لنفرض أن لديك تطبيقاً على السيرفر فيه صفحة لرفع الملفات، ويختار المستخدم ملفاً حجمه 150 ميجابايت ويضغط «رفع»، فيتقدم شريط التحميل قليلاً ثم يتوقف، وتظهر له رسالة عامة مثل «فشل الرفع» أو «خطأ في الشبكة»، وقد يحدث ما هو أغرب من ذلك، فيصل الطلب إلى التطبيق ويرد بالرمز 200 والملف غير موجود. وسوف تراجع كود الرفع سطراً سطراً ولن تجد فيه أي خطأ، والسبب أن المشكلة في الغالب ليست في الكود أصلاً، وإنما في الطريق الذي يقطعه الملف قبل أن يصل إلى الكود.

فالملف في طريقه إلى التخزين يمر بعدة طبقات Layers، ولكل طبقة حد أقصى لحجم الطلب ومهلة Timeout لاستقباله، وأصغر هذه الحدود هو الذي يحكم الرفع كله. والمشكلة أن كل طبقة ترفض الطلب بطريقتها، فترد Cloudflare بالرمز `413` في صفحة خاصة بها، ويرد Nginx بالرمز نفسه في صفحة أخرى، ويقطع Traefik الاتصال بعد دقيقة واحدة، أما PHP فيستقبل الطلب ثم يحذف منه الملفات دون أن يظهر أي خطأ.

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

- الطبقات التي يمر بها الملف المرفوع، والحد الافتراضي للحجم والزمن في كل طبقة كما يذكره التوثيق الرسمي.
- حدود Cloudflare التي لا تملك تعديلها، وحدود ال Reverse Proxy في Nginx وNginx Proxy Manager وTraefik وCaddy وHAProxy.
- حدود التطبيق نفسه في PHP وLaravel وExpress وDjango وASP.NET Core وSpring Boot وGo، وفي تطبيقات الاستضافة الذاتية الشائعة.
- كيف تعرف الطبقة التي رفضت الملف من شكل الخطأ، وكيف تختبر كل طبقة بملف تولده أنت.
- تصميم مقترح لرفع الملفات الكبيرة، ثم جدول يلخص الأعراض وحلولها.

وإذا كان تطبيقك يعمل خلف Reverse Proxy لأول مرة فاقرأ معه دليل [تطبيقك خلف Reverse Proxy](https://arabroot.io/articles/%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%83-%D8%AE%D9%84%D9%81-reverse-proxy/)، وراجع قبل النشر [قائمة تحقق المطور قبل نشر تطبيقه](https://arabroot.io/articles/%D9%82%D8%A7%D8%A6%D9%85%D8%A9-%D8%AA%D8%AD%D9%82%D9%82-%D8%A7%D9%84%D9%85%D8%B7%D9%88%D8%B1-%D9%82%D8%A8%D9%84-%D9%86%D8%B4%D8%B1-%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D9%87/).

## ما الطبقات التي يمر بها الملف؟

عندما يرفع المتصفح Browser ملفاً فإنه يرسله في جسم الطلب Request Body، وغالباً بصيغة `multipart/form-data`، ويعبر الطلب هذه الطبقات بالترتيب:

1. المتصفح، وفيه الواجهة الأمامية Frontend التي تختار الملف وترسله.
2. Cloudflare، إذا كان النطاق Domain يمر عبر شبكتها بالسحابة البرتقالية، أو كنت تستخدم Cloudflare Tunnel.
3. ال Reverse Proxy على السيرفر، مثل Nginx أو Nginx Proxy Manager أو Traefik أو Caddy أو HAProxy.
4. سيرفر التطبيق وبيئة التشغيل Runtime، مثل PHP أو Node.js أو Kestrel أو Tomcat.
5. إطار العمل Framework وقواعد التحقق Validation في تطبيقك.
6. التخزين Storage، سواءً كان القرص أو تخزين الكائنات Object Storage مثل S3 وR2 وMinIO.

والصورة التالية تبين هذه الطبقات مع الحد الافتراضي في كل منها:

![مخطط يوضح الطبقات التي يمر بها الملف المرفوع من المتصفح إلى Cloudflare ثم الـ Reverse Proxy ثم التطبيق ثم التخزين، مع الحد الافتراضي لكل طبقة والخطأ الذي تعيده](https://arabroot.io/content/images/2026/10/upload-limits-01-upload-layers-1.webp)

الحدود الافتراضية في كل طبقة: يرفض الملف عند أول طبقة يتجاوز حدها

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

| الطبقة                 | حد الحجم الافتراضي                               | حد الزمن الافتراضي                  | ما يحدث عند التجاوز       |
| ---------------------- | ------------------------------------------------ | ----------------------------------- | ------------------------- |
| Cloudflare (Free وPro) | 100 MB للطلب                                     | 125 ثانية بانتظار رد السيرفر        | 413 من Cloudflare، أو 524 |
| Nginx                  | 1m                                               | 60 ثانية بين كل قراءتين             | 413، أو 408، أو 504       |
| Nginx Proxy Manager    | 2000m                                            | 90 ثانية للتطبيق                    | مثل Nginx                 |
| Traefik                | لا يوجد حد                                       | 60 ثانية للطلب كاملاً               | قطع الاتصال بعد 60 ثانية  |
| Caddy                  | لا يوجد حد                                       | دقيقة من التوقف عن الإرسال          | قطع الاتصال               |
| HAProxy                | لا يوجد حد                                       | بحسب timeout client وtimeout server | 408 أو 504                |
| PHP                    | upload\_max\_filesize = 2M وpost\_max\_size = 8M | max\_execution\_time = 30           | رد 200 مع ملفات فارغة     |
| إطارات العمل           | تختلف كثيراً                                     | غالباً لا يوجد حد                   | 413 أو 400 أو 500         |

والحل الأول الذي يخطر على بال أي مطور هو أن يرفع حد الطبقة التي ظهر منها الخطأ، فيضيف `client_max_body_size` في Nginx ويعيد المحاولة، فينتقل الخطأ إلى الطبقة التالية، فيرفض PHP الملف بصمت، فيرفع حدوده أيضاً، ثم يكتشف أن الملف لا يصل أصلاً لأن Cloudflare ترفض كل طلب فوق 100 ميجابايت مهما فعل على السيرفر. لذلك الطريقة الصحيحة ليست مطاردة الخطأ من طبقة إلى طبقة، وإنما أن تعرف حد كل طبقة مسبقاً، وتضع حداً صريحاً في كل منها، ثم تنتقل إلى الرفع المجزأ أو الرفع المباشر إلى التخزين عندما تكون ملفاتك أكبر من حد أي طبقة لا تملكها، وهذا ما سوف نقوم به في بقية المقال.

## Cloudflare: الحد الذي لا تملك تعديله

### حجم الطلب بحسب الخطة

إذا كان سجل DNS لنطاقك يمر عبر Cloudflare، أي في [وضع Proxied](https://arabroot.io/articles/%D8%B4%D8%B1%D8%AD-dns-%D9%88%D8%B3%D8%AC%D9%84%D8%A7%D8%AA%D9%87-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، فإن Cloudflare تستقبل الطلب قبل السيرفر، وتفرض عليه [حداً أقصى لحجم الرفع](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/4xx-client-error/error-413/?ref=arabroot.io) بحسب خطتك كما يلي:

| الخطة      | أقصى حجم للطلب الواحد                   |
| ---------- | --------------------------------------- |
| Free       | 100 MB                                  |
| Pro        | 100 MB                                  |
| Business   | 200 MB                                  |
| Enterprise | حتى 5 GB، وأكثر بالتنسيق مع فريق الحساب |

وتجد هذا الحد في إعداد Maximum Upload Size بصفحة Network الخاصة بالنطاق، وعملاء Enterprise فقط يستطيعون رفعه بأنفسهم حتى 5 GB، أما في الخطط الأخرى فلن يرفعه أي إعداد على السيرفر. ولاحظ أن الحد على الطلب كله وليس على الملف وحده، فطلب يحمل ثلاثة ملفات حجم كل منها 40 ميجابايت يتجاوز حد الخطة المجانية رغم أن كل ملف أقل من نصف الحد.

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

### مهلة انتظار الرد: الخطأ 524

بعد أن يصل الملف إلى السيرفر تنتظر Cloudflare رد التطبيق مدة محددة، ويذكر جدول [حدود الاتصال](https://developers.cloudflare.com/fundamentals/reference/connection-limits/?ref=arabroot.io) أن مهلة القراءة من السيرفر الأصلي Proxy Read Timeout هي 125 ثانية، فإذا لم يرد التطبيق خلالها ظهر للمستخدم [الخطأ 524](https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-5xx-errors/error-524/?ref=arabroot.io). ولا يعدل هذه المهلة إلا عملاء Enterprise، وتصل عندهم إلى 6000 ثانية.

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

### ماذا عن Cloudflare Tunnel؟

إذا نشرت تطبيقك عبر Cloudflare Tunnel كما في دليل [تشغيل خادمك من إنترنت المنزل](https://arabroot.io/articles/%D8%AA%D8%B4%D8%BA%D9%8A%D9%84-%D8%AE%D8%A7%D8%AF%D9%85%D9%83-%D9%85%D9%86-%D8%A5%D9%86%D8%AA%D8%B1%D9%86%D8%AA-%D8%A7%D9%84%D9%85%D9%86%D8%B2%D9%84/)، فكل طلب يمر بشبكة Cloudflare قبل أن يصل إلى النفق، وبالتالي تنطبق عليه حدود الخطة نفسها ومهلة 125 ثانية. ولا يوجد لديك هنا خيار السحابة الرمادية DNS Only، والسبب أن السجل يشير إلى النفق وليس إلى عنوان عام Public IP للسيرفر، لذلك إذا احتجت إلى رفع ملفات أكبر من حد الخطة فاختر الرفع المجزأ أو الرفع المباشر إلى التخزين، أو اتصل بالسيرفر عبر شبكة خاصة افتراضية VPN.

### ثلاثة حلول للملفات الأكبر من حد الخطة

**الرفع المجزأ Chunked Upload.** تقسم الواجهة الملف إلى أجزاء أصغر من الحد، مثل 50 ميجابايت للجزء، وترسل كل جزء في طلب مستقل، ثم يجمعها السيرفر، فإذا انقطع الاتصال استأنف الرفع من آخر جزء وصل، وهذا ما يسمى الرفع القابل للاستئناف Resumable Upload. ولا تحتاج إلى كتابة ذلك بنفسك، فقد عرفه [بروتوكول tus](https://tus.io/protocols/resumable-upload?ref=arabroot.io) المفتوح، وله مكتبات جاهزة للسيرفر والمتصفح، منها [Uppy](https://uppy.io/docs/tus/?ref=arabroot.io).

**الرفع المباشر إلى التخزين بروابط موقعة مسبقاً Presigned URLs.** يطلب المتصفح من تطبيقك إذناً بالرفع، فيولد التطبيق رابطاً موقعاً لملف واحد صالحاً لمدة قصيرة، ثم يرفع المتصفح الملف بهذا الرابط إلى التخزين مباشرة، فلا يمر بـ Cloudflare ولا بال Reverse Proxy ولا بالتطبيق. وتدعم ذلك [Amazon S3](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html?ref=arabroot.io) و[Cloudflare R2](https://developers.cloudflare.com/r2/api/s3/presigned-urls/?ref=arabroot.io) و[MinIO](https://github.com/minio/minio-js/blob/master/docs/API.md?ref=arabroot.io#presignedPutObject) وغيرها من الخدمات المتوافقة مع S3، وأقصى حجم للطلب الواحد 5 GB في S3، وأقل من 5 GiB بقليل في [R2](https://developers.cloudflare.com/r2/platform/limits/?ref=arabroot.io)، وما فوق ذلك يحتاج إلى الرفع متعدد الأجزاء Multipart Upload. ولا تنس أن تضبط [سياسة CORS](https://developers.cloudflare.com/r2/buckets/cors/?ref=arabroot.io) على ال Bucket حتى يقبل الطلبات من نطاق تطبيقك.

**نطاق فرعي للرفع دون Proxy.** تنشئ نطاقاً فرعياً Subdomain مثل `upload.example.com` بوضع [DNS Only](https://developers.cloudflare.com/dns/proxy-status/?ref=arabroot.io)، فيصل الرفع إلى السيرفر مباشرة دون حد Cloudflare. ولكن لهذا الحل ثمن، فهو يكشف العنوان العام للسيرفر، وبعدها يستطيع أي شخص أن يتصل بالسيرفر مباشرة ويتجاوز حماية Cloudflare عن كل النطاقات التي يخدمها نفس السيرفر، وتخسر على هذا النطاق جدار حماية تطبيقات الويب WAF والحماية من هجمات حجب الخدمة DDoS، وتحتاج إلى شهادة TLS على السيرفر. لذلك لا تلجأ إليه إلا إذا تعذر الحلان السابقان.

## ال Reverse Proxy: الحد الذي ينساه الجميع

### Nginx

القيمة الافتراضية للتوجيه [client\_max\_body\_size](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fcore%5Fmodule.html?ref=arabroot.io#client%5Fmax%5Fbody%5Fsize) هي `1m`، أي ميجابايت واحد، فإذا لم تغيرها فشل أي رفع أكبر من ذلك بالرمز `413 Request Entity Too Large`. والتوجيه يقبل في `http` و`server` و`location`، وبالتالي تستطيع أن ترفع الحد لمسار الرفع وحده وتبقيه منخفضاً لبقية الموقع. أما القيمة `0` فتلغي الفحص تماماً، فلا تستخدمها على موقع عام. والمثال التالي يبين ذلك:

```nginx
server {
    server_name app.example.com;
    client_max_body_size 10m;

    location /api/upload {
        client_max_body_size 110m;
        client_body_timeout 120s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
        proxy_pass http://app:3000;
    }
}
```

ثم تحقق من الإعداد وأعد تحميله:

```bash
sudo nginx -t && sudo systemctl reload nginx
```

في الإعداد أعلاه لاحظ التالي، مع العلم أن القيمة الافتراضية للمهل الثلاث هي 60 ثانية:

- [client\_body\_timeout](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fcore%5Fmodule.html?ref=arabroot.io#client%5Fbody%5Ftimeout) هي أقصى مدة بين قراءتين متتاليتين من المستخدم وليست مدة الرفع كله، فإذا توقف الإرسال مدة أطول منها أنهى Nginx الطلب بالرمز `408`.
- [proxy\_send\_timeout](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fproxy%5Fmodule.html?ref=arabroot.io#proxy%5Fsend%5Ftimeout) هي أقصى مدة بين عمليتي كتابة متتاليتين نحو التطبيق.
- [proxy\_read\_timeout](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fproxy%5Fmodule.html?ref=arabroot.io#proxy%5Fread%5Ftimeout) هي أقصى مدة ينتظر فيها Nginx رد التطبيق، فإذا كان التطبيق يعالج الملف قبل أن يرد ظهر الخطأ `504 Gateway Time-out`. وإذا كان التطبيق يعمل على PHP-FPM فالتوجيه المقابل هو [fastcgi\_read\_timeout](https://nginx.org/en/docs/http/ngx%5Fhttp%5Ffastcgi%5Fmodule.html?ref=arabroot.io#fastcgi%5Fread%5Ftimeout)، وقيمته الافتراضية 60 ثانية أيضاً.

ويجدر الإشارة هنا إلى أن Nginx يخزن جسم الطلب كاملاً قبل أن يرسله إلى التطبيق، والسبب أن القيمة الافتراضية لـ [proxy\_request\_buffering](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fproxy%5Fmodule.html?ref=arabroot.io#proxy%5Frequest%5Fbuffering) هي `on`، وما يزيد على [client\_body\_buffer\_size](https://nginx.org/en/docs/http/ngx%5Fhttp%5Fcore%5Fmodule.html?ref=arabroot.io#client%5Fbody%5Fbuffer%5Fsize) (من 8 إلى 16 كيلوبايت) يكتبه في ملف مؤقت Temporary File على القرص. ولهذا السلوك فائدة، فالتطبيق لا ينشغل بمستخدم بطيء ولا يصله الطلب إلا كاملاً، ولكنه يحتاج إلى مساحة على القرص تكفي كل الرفعات المتزامنة. وإذا أردت أن يصل الملف إلى التطبيق أولاً بأول، كما في سيرفرات tus، فاجعل القيمة `off` لهذا المسار وحده.

### Nginx Proxy Manager

أما [Nginx Proxy Manager](https://arabroot.io/articles/nginx-proxy-manager-%D9%86%D8%B7%D8%A7%D9%82%D8%A7%D8%AA-%D9%88%D8%B4%D9%87%D8%A7%D8%AF%D8%A7%D8%AA-tls/) فيرفع الحد الافتراضي كثيراً، ففي [ملف nginx.conf الخاص به](https://github.com/NginxProxyManager/nginx-proxy-manager/blob/v2.16.0/docker/rootfs/etc/nginx/nginx.conf?ref=arabroot.io) القيمة `client_max_body_size 2000m`، والمهلتان `proxy_send_timeout` و`proxy_read_timeout` على 90 ثانية. لذلك إذا ظهر `413` مع ملف صغير خلف NPM، فالأرجح أن الحد مفروض في Cloudflare أو في التطبيق، أو أن أحداً كتب حداً أصغر في الإعدادات المتقدمة للمضيف.

ولتعديل الحد أو المهل لمضيف واحد افتح ال Proxy Host ثم تبويب [Advanced](https://nginxproxymanager.com/advanced-config/?ref=arabroot.io#custom-nginx-configurations) (رمز الترس)، وأضف هذه الأسطر واحفظ:

```nginx
client_max_body_size 5g;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
```

ويكتب NPM سجل الأخطاء لكل مضيف في الملف `/data/logs/proxy-host-<id>_error.log` داخل ال Container، فإذا كان NPM هو الذي رفض الطلب فسوف تجد رسالة الرفض فيه.

### Traefik

لا يضع [Traefik](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-traefik-reverse-proxy/) حداً لحجم الطلب افتراضياً، فإذا أردت حداً فأضف Middleware من نوع [Buffering](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/buffering/?ref=arabroot.io) وحدد فيه `maxRequestBodyBytes` بالبايت، وقيمته الافتراضية `0` أي دون حد، وعند التجاوز يرد Traefik بالرمز `413` ونص قصير هو `Request Entity Too Large`. ولاحظ أن هذا ال Middleware يقرأ الطلب كاملاً قبل إرساله، فيحفظ أول ميجابايت في الذاكرة Memory والباقي على القرص:

```yaml
    labels:
      - "traefik.http.middlewares.upload-limit.buffering.maxRequestBodyBytes=115343360"
      - "traefik.http.routers.app.middlewares=upload-limit"
```

أما المهلة فهي التي تفاجئ أكثر المستخدمين، فالقيمة الافتراضية لـ [transport.respondingTimeouts.readTimeout](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/?ref=arabroot.io) على نقطة الدخول EntryPoint هي 60 ثانية، وهي أقصى مدة لقراءة الطلب كاملاً بما فيه الجسم. وهذا يعني أنه إذا استغرق رفع الملف أكثر من دقيقة على اتصال بطيء قطع Traefik الطلب بعد 60 ثانية بالضبط مهما كان الحجم صغيراً، وقد يسجله في سجل الوصول Access Log بالرمز `499 Client Closed Request`، فتظن أن المستخدم أغلق الصفحة والحقيقة أن المهلة انتهت. وتعدل هذه المهلة في الإعداد الثابت `traefik.yml` ثم تعيد تشغيل Traefik:

```yaml
entryPoints:
  websecure:
    address: ":443"
    transport:
      respondingTimeouts:
        readTimeout: 30m
```

وتذكر أن المهلة على نقطة الدخول كلها وليست على تطبيق واحد، لذلك اختر قيمة تكفي أبطأ رفع تتوقعه، ولا تجعلها `0`، والسبب أن هذا يترك الاتصالات البطيئة مفتوحة دون نهاية.

### Caddy

ولا يحد [Caddy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-caddy-reverse-proxy/) حجم الطلب افتراضياً أيضاً، وتضيف الحد بالتوجيه [request\_body](https://caddyserver.com/docs/caddyfile/directives/request%5Fbody?ref=arabroot.io) وخياره `max_size`، ويرد Caddy بالرمز `413` عند تجاوزه:

```nginx
app.example.com {
	request_body /api/upload* {
		max_size 110MB
	}
	reverse_proxy app:3000
}
```

وفي [الخيارات العامة Global Options](https://caddyserver.com/docs/caddyfile/options?ref=arabroot.io#timeouts) لا توجد مهلة افتراضية لقراءة الجسم كاملاً (`read_body`)، أما `read_body_idle` فقيمته الافتراضية دقيقة واحدة، ويبدأ العد من جديد بعد كل قراءة ناجحة، وبالتالي لا يقطع Caddy رفعاً بطيئاً ما دامت البيانات تصل، وإنما يقطع الاتصال الذي يتوقف عن الإرسال دقيقة كاملة.

### HAProxy

أما [HAProxy](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-haproxy-load-balancer/) فلا يحد حجم الطلب افتراضياً، ولا يوجد فيه توجيه مخصص لذلك، ولكنك تستطيع رفض الطلب بحسب الترويسة Header `Content-Length` بقاعدة [http-request deny](https://docs.haproxy.org/3.4/configuration.html?ref=arabroot.io#4.4-deny) في قسم `frontend`:

```ini
    http-request deny deny_status 413 if { req.hdr_val(content-length) gt 115343360 }
```

وهذه القاعدة لا تشمل الطلبات المرسلة بترميز Chunked Transfer Encoding، والسبب أنها لا تحمل `Content-Length`، لذلك أبق الحد الفعلي في التطبيق أيضاً. ولكل مهلة معنى محدد في [توثيق HAProxy](https://docs.haproxy.org/3.4/configuration.html?ref=arabroot.io) كما يلي:

- [timeout client](https://docs.haproxy.org/3.4/configuration.html?ref=arabroot.io#4.2-timeout%20client): مدة عدم النشاط Inactivity المسموحة من جهة المستخدم، والرفع البطيء لا يتأثر بها ما دامت البيانات تصل.
- [timeout http-request](https://docs.haproxy.org/3.4/configuration.html?ref=arabroot.io#4.2-timeout%20http-request): أقصى مدة لاستقبال الطلب، وتشمل افتراضياً الترويسات فقط، فإذا فعلت `option http-buffer-request` شملت الجسم أيضاً، وعندها يقطع HAProxy الرفع الطويل بالرمز `408`.
- [timeout server](https://docs.haproxy.org/3.4/configuration.html?ref=arabroot.io#4.2-timeout%20server): مدة انتظار التطبيق، فإذا تأخر رده بعد وصول الملف ظهر الخطأ `504`.

## حدود التطبيق وإطار العمل

### PHP

حدود PHP هي أكثر الحدود إرباكاً، والسبب أنه لا يرفض الطلب برمز خطأ، وإنما يقبله ويحذف منه ما تجاوز الحد. وفيما يلي أهم الإعدادات وقيمها الافتراضية كما في [توثيق php.ini](https://www.php.net/manual/en/ini.core.php?ref=arabroot.io) و[إعدادات وقت التشغيل](https://www.php.net/manual/en/info.configuration.php?ref=arabroot.io):

| الإعداد               | القيمة الافتراضية                      | ما يحدده                                                           |
| --------------------- | -------------------------------------- | ------------------------------------------------------------------ |
| upload\_max\_filesize | 2M                                     | أقصى حجم للملف الواحد                                              |
| post\_max\_size       | 8M                                     | أقصى حجم للطلب كله، ويجب أن يكون أكبر من upload\_max\_filesize     |
| max\_file\_uploads    | 20                                     | أقصى عدد للملفات في الطلب الواحد                                   |
| max\_execution\_time  | 30 ثانية                               | أقصى مدة لتنفيذ السكربت                                            |
| max\_input\_time      | \-1، أي يأخذ قيمة max\_execution\_time | أقصى مدة لقراءة المدخلات                                           |
| memory\_limit         | 128M                                   | أقصى ذاكرة للسكربت، ويوصي التوثيق بأن تكون أكبر من post\_max\_size |

والفرق بين الحدين الأولين يغير ما تراه أمامك:

- إذا تجاوز الملف `upload_max_filesize` وبقي الطلب تحت `post_max_size`، يصل الطلب إلى السكربت بالرمز 200 والملف فيه دون محتوى، والحقل `$_FILES['file']['error']` يحمل القيمة `1`، أي [UPLOAD\_ERR\_INI\_SIZE](https://www.php.net/manual/en/features.file-upload.errors.php?ref=arabroot.io).
- وإذا تجاوز الطلب كله `post_max_size`، تصل المصفوفتان `$_POST` و`$_FILES` فارغتين تماماً، ويكتب PHP تحذيراً مثل `POST Content-Length of 9437394 bytes exceeds the limit of 8388608 bytes`، فإذا كان تطبيقك يتحقق من حقل في النموذج قبل الملف فسوف يرى المستخدم خطأ لا علاقة له بالحجم، مثل «حقل العنوان مطلوب»، وهذه من أكثر الأخطاء التي تضيع وقت المطور في المكان الخطأ.

و[ال Image الرسمي لـ PHP](https://hub.docker.com/%5F/php?ref=arabroot.io) لا يحمل ملف `php.ini` مفعلاً، لذلك تعمل القيم الافتراضية السابقة، وتعدلها بملف `.ini` تضعه في المجلد `$PHP_INI_DIR/conf.d/`، وهو `/usr/local/etc/php/conf.d/`. ونفس الطريقة تصلح لل Image الرسمي لـ WordPress كما في دليل [تثبيت WordPress مع Docker Compose](https://arabroot.io/articles/%D8%AA%D8%AB%D8%A8%D9%8A%D8%AA-wordpress-%D9%85%D8%B9-docker-compose/). الآن سوف نقوم بإنشاء الملف `uploads.ini` بجانب ملف Compose:

```ini
upload_max_filesize = 100M
post_max_size = 105M
max_execution_time = 300
max_input_time = 300
memory_limit = 256M
```

في الإعداد أعلاه لاحظ أن `post_max_size` أكبر قليلاً من `upload_max_filesize`، والسبب أن الطلب يحمل مع الملف حقول النموذج، وأن `memory_limit` أكبر من الاثنين كما يوصي التوثيق. ثم اربط الملف داخل ال Container في الخدمة التي تشغل PHP:

```yaml
    volumes:
      - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
```

بعد ذلك أعد إنشاء ال Container، وتأكد أن القيم الجديدة مفعلة:

```bash
docker compose up -d
docker compose exec app php -i | grep -E "upload_max_filesize|post_max_size"
```

وإذا كان PHP-FPM خلف Nginx فارفع `client_max_body_size` و`fastcgi_read_timeout` في Nginx أيضاً، وإلا بقي حد Nginx هو الأصغر وعدت إلى نفس الخطأ.

### Laravel

قاعدة التحقق [max](https://laravel.com/docs/13.x/validation?ref=arabroot.io#rule-max) تحسب حجم الملفات بالكيلوبايت وليس بالبايت، فحد 100 ميجابايت يكتب `max:102400`:

```php
$request->validate([
    'file' => ['required', 'file', 'max:102400'],
]);
```

وفي Laravel يوجد Middleware اسمه `ValidatePostSize` يقارن الترويسة `Content-Length` بقيمة `post_max_size` في PHP، فإذا كان الطلب أكبر رد Laravel بالرمز `413` ورسالة `The POST data is too large.`، لذلك إذا رأيت هذه الرسالة فالحل في `uploads.ini` وليس في ال Reverse Proxy.

### Node.js وExpress

القيمة الافتراضية للخيار `limit` في [express.json()](https://expressjs.com/en/5x/api/express/?ref=arabroot.io#expressjson) و`express.urlencoded()` هي `100kb`، وعند تجاوزها يرد Express بالرمز `413` ورسالة `request entity too large`. وتظهر هذه المشكلة عادة عندما ترسل الواجهة الملف داخل JSON بترميز Base64، وهذا الأسلوب يزيد الحجم بنحو الثلث ويحمل الملف كاملاً في الذاكرة، لذلك أرسل الملفات بصيغة `multipart/form-data`، فهذان المحللان لا يقرآنها أصلاً.

ولقراءة الملفات تستخدم [multer](https://github.com/expressjs/multer?ref=arabroot.io)، وهي مبنية على [busboy](https://github.com/mscdex/busboy?ref=arabroot.io)، وقيمة `limits.fileSize` فيها افتراضياً `Infinity` أي دون حد. فإذا حددتها رمت multer عند التجاوز خطأ `MulterError` بالرمز `LIMIT_FILE_SIZE`، وهذا الخطأ لا يحمل رمز HTTP، فيرد معالج الأخطاء الافتراضي في Express بالرمز `500` ويظن المستخدم أن السيرفر معطل، لذلك عالجه بنفسك كما يلي:

```javascript
const upload = multer({
  dest: '/var/app/uploads',
  limits: { fileSize: 100 * 1024 * 1024 },
});

app.post('/api/upload', upload.single('file'), (req, res) => {
  res.json({ size: req.file.size });
});

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
    return res.status(413).json({ error: 'حجم الملف أكبر من 100 ميجابايت' });
  }
  next(err);
});
```

واستخدم `dest` أو `diskStorage`، ولا تستخدم `memoryStorage` التي تحفظ الملف كاملاً في الذاكرة. وإذا استخدمت busboy مباشرة فتذكر أنه لا يرفض الطلب عند تجاوز `fileSize`، وإنما يقطع الملف ويضع `truncated` على `true`، فإذا لم تتحقق من هذه الخاصية في نهاية التدفق فسوف تحفظ ملفاً ناقصاً دون أي خطأ.

### Django

يخلط كثير من المطورين بين إعدادين في [إعدادات Django](https://docs.djangoproject.com/en/6.1/ref/settings/?ref=arabroot.io)، والقيمة الافتراضية لكل منهما 2.5 ميجابايت:

- [DATA\_UPLOAD\_MAX\_MEMORY\_SIZE](https://docs.djangoproject.com/en/6.1/ref/settings/?ref=arabroot.io#data-upload-max-memory-size): حد حجم الطلب من غير الملفات المرفوعة، أي الحقول وجسم JSON، فإذا تجاوزه الطلب رمى Django خطأ `RequestDataTooBig` ورد بالرمز `400`، ولهذا يصطدم به من يرسل الملفات داخل JSON.
- [FILE\_UPLOAD\_MAX\_MEMORY\_SIZE](https://docs.djangoproject.com/en/6.1/ref/settings/?ref=arabroot.io#file-upload-max-memory-size): ليس حداً للرفع، وإنما الحجم الذي ينتقل بعده Django من حفظ الملف في الذاكرة إلى [ملف مؤقت على القرص](https://docs.djangoproject.com/en/6.1/topics/http/file-uploads/?ref=arabroot.io).

وهذا يعني أن Django لا يحد حجم الملفات المرفوعة افتراضياً، لذلك ضع الحد في ال Reverse Proxy، وتحقق من `file.size` في النموذج Form أو في ال View. ويوصي التوثيق تحت ASGI بحماية إضافية أمام Django، والسبب أن الطلب قد يحفظ كاملاً على القرص قبل أن يطبق أي حد.

### ASP.NET Core

يفرض Kestrel حداً افتراضياً للطلب قدره 30,000,000 بايت، أي نحو 28.6 ميجابايت، كما يشرح [توثيق رفع الملفات](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?ref=arabroot.io)، وفوقه حد `MultipartBodyLengthLimit` في `FormOptions` وقيمته الافتراضية 128 MB لكل جزء من النموذج. وإذا كان التطبيق خلف IIS فحد IIS نفسه `maxAllowedContentLength` هو 30,000,000 بايت أيضاً، ويرفض IIS الطلب قبل أن يصل إلى التطبيق. والطريقة الأنسب لرفع الحد لمسار واحد هي السمة `[RequestSizeLimit]` على ال Action:

```csharp
[HttpPost("api/upload")]
[RequestSizeLimit(115_343_360)]
[RequestFormLimits(MultipartBodyLengthLimit = 115_343_360)]
public async Task<IActionResult> Upload(IFormFile file)
{
    // ...
}
```

وإذا أردت حداً عاماً لكل الطلبات فاضبط [MaxRequestBodySize](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel/options?ref=arabroot.io) في إعداد Kestrel:

```csharp
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 115_343_360;
});
```

### Spring Boot

القيم الافتراضية في [خصائص Spring Boot](https://docs.spring.io/spring-boot/appendix/application-properties/index.html?ref=arabroot.io) صغيرة، فالخاصية `spring.servlet.multipart.max-file-size` هي `1MB` للملف الواحد، و`spring.servlet.multipart.max-request-size` هي `10MB` للطلب كله، وعند تجاوزهما يرمي Spring خطأ [MaxUploadSizeExceededException](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/multipart/MaxUploadSizeExceededException.html?ref=arabroot.io)، وتلتقطه في `@ControllerAdvice` لترد برسالة واضحة. وترفع الحدين في `application.properties`:

```ini
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=105MB
```

### Go

مكتبة `net/http` لا تحد حجم الطلب افتراضياً، والمعامل `maxMemory` في [ParseMultipartForm](https://pkg.go.dev/net/http?ref=arabroot.io#Request.ParseMultipartForm) ليس حداً للرفع، وإنما مقدار ما يحفظ من الملفات في الذاكرة، والباقي يكتب في ملفات مؤقتة، و`FormFile` يستدعي `ParseMultipartForm` تلقائياً. لذلك ضع الحد الفعلي بـ [http.MaxBytesReader](https://pkg.go.dev/net/http?ref=arabroot.io#MaxBytesReader) قبل قراءة الطلب، ورد بالرمز `413` إذا تجاوزه:

```go
func upload(w http.ResponseWriter, r *http.Request) {
	r.Body = http.MaxBytesReader(w, r.Body, 110<<20)
	if err := r.ParseMultipartForm(32 << 20); err != nil {
		var tooBig *http.MaxBytesError
		if errors.As(err, &tooBig) {
			http.Error(w, "حجم الملف أكبر من المسموح", http.StatusRequestEntityTooLarge)
			return
		}
		http.Error(w, "طلب غير صالح", http.StatusBadRequest)
		return
	}
	// ...
}
```

### تطبيقات الاستضافة الذاتية الشائعة

- **WordPress**: يعتمد على حدود PHP، فعدلها بملف `uploads.ini` كما سبق، ثم ارفع حد ال Reverse Proxy.
- **Nextcloud**: يرفع من المتصفح بأجزاء حجمها الافتراضي 100 MiB كما يذكر [دليل رفع الملفات الكبيرة](https://docs.nextcloud.com/server/latest/admin%5Fmanual/configuration%5Ffiles/big%5Ffile%5Fupload%5Fconfiguration.html?ref=arabroot.io)، وهذا الحجم أكبر قليلاً من حد Cloudflare في الخطة المجانية، لذلك اجعل الأجزاء 50 ميجابايت إذا كان Nextcloud خلف Cloudflare:

```bash
sudo -E -u www-data php occ config:system:set --type int --value 52428800 files.chunked_upload.max_size
```

- **Seafile**: لا يحد حجم الرفع إذا لم تضبط `max_upload_size` في قسم `[fileserver]` من [ملف seafile.conf](https://manual.seafile.com/latest/config/seafile-conf/?ref=arabroot.io)، وعندها يبقى حد ال Reverse Proxy هو الحاكم.

## كيف تعرف الطبقة التي رفضت الملف؟

### اقرأ الرد قبل أن تغير أي إعداد

غالباً يخفي المتصفح تفاصيل الخطأ، وتوثيق Nginx نفسه ينبه إلى أن المتصفحات لا تعرض الرمز `413` بشكل صحيح، لذلك افتح أدوات المطور Developer Tools في المتصفح، ثم تبويب Network، واقرأ رد طلب الرفع وترويساته، وسوف تجد أن رد كل طبقة يختلف كما يلي:

| ما تراه في الرد                                                                                      | الطبقة المرجحة                   |
| ---------------------------------------------------------------------------------------------------- | -------------------------------- |
| صفحة HTML تنتهي بكلمة cloudflare، مع الترويستين server: cloudflare وcf-ray، ولا شيء في سجلات السيرفر | Cloudflare                       |
| صفحة HTML تنتهي بـ nginx أو nginx/1.27.5، والترويسة Server: nginx                                    | Nginx                            |
| الصفحة نفسها ولكن تنتهي بـ openresty                                                                 | Nginx Proxy Manager أو OpenResty |
| نص قصير Request Entity Too Large دون ترويسة Server                                                   | Traefik                          |
| رد JSON أو صفحة خطأ بتصميم تطبيقك                                                                    | إطار العمل أو التطبيق            |
| رد 200 والملف غير موجود، أو أخطاء تحقق غريبة                                                         | PHP وpost\_max\_size             |

ثم راجع السجلات، فإذا رفض Nginx الطلب بسبب الحجم فسوف تجد في سجل الأخطاء سطراً مثل هذا:

```bash
client intended to send too large body: 15728851 bytes, client: 203.0.113.50, server: app.example.com, request: "POST /api/upload HTTP/1.1"
```

### اختبر كل طبقة بملف مولد

لا تختبر بملف حقيقي من جهازك، وإنما ولد ملفاً بالحجم الذي يفشل عند المستخدم، والبيانات العشوائية لا تنضغط، وبالتالي لا يخدعك أي ضغط Compression في الطريق:

```bash
head -c 150M /dev/urandom > test.bin
```

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

```bash
curl -sS -o /dev/null -w "%{http_code}\n" -F file=@test.bin http://127.0.0.1:3000/api/upload
```

بعد ذلك ارفعه عبر ال Reverse Proxy دون Cloudflare، بتوجيه النطاق إلى العنوان العام للسيرفر من سطر الأوامر، والخيار `-D -` يطبع ترويسات الرد فتعرف مصدره:

```bash
curl -sS -o /dev/null -D - --resolve app.example.com:443:203.0.113.10 -F file=@test.bin https://app.example.com/api/upload
```

وأخيراً ارفعه عبر النطاق العام نفسه، أي عبر Cloudflare:

```bash
curl -sS -o /dev/null -D - -F file=@test.bin https://app.example.com/api/upload
```

وأول خطوة تفشل تدلك على الطبقة، فإذا نجح الرفع مباشرة إلى التطبيق وفشل عبر ال Reverse Proxy فالحد في ال Reverse Proxy، وإذا نجح عبره وفشل عبر النطاق العام فالحد في Cloudflare.

### مشكلات المهلة على الاتصالات البطيئة

قد ينجح الرفع من مكتبك على شبكة سريعة، ثم يفشل عند مستخدم على شبكة جوال ضعيفة، والسبب هنا الزمن وليس الحجم. وتستطيع أن تحاكي الاتصال البطيء بالخيار `--limit-rate`، فالأمر التالي يرفع بسرعة 100 كيلوبايت في الثانية تقريباً:

```bash
time curl -sS -o /dev/null -w "%{http_code}\n" --limit-rate 100k -F file=@test.bin https://app.example.com/api/upload
```

ثم انظر بعد كم من الوقت فشل الطلب، فإذا انقطع بعد 60 ثانية بالضبط خلف Traefik فالسبب المهلة `readTimeout`، وإذا فشل بالرمز `408` من Nginx فالمستخدم توقف عن الإرسال مدة أطول من `client_body_timeout`، وإذا وصل الملف كاملاً ثم ظهر `504` أو `524` فالتطبيق تأخر في الرد بعد الرفع.

## تصميم مقترح لرفع الملفات الكبيرة

### حد صريح في كل طبقة، والتطبيق هو من يرفض

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

| الطبقة           | القيمة                                               |
| ---------------- | ---------------------------------------------------- |
| الواجهة الأمامية | تمنع اختيار ملف أكبر من 100 ميجابايت                 |
| التطبيق          | max:102400 أو fileSize: 100 \* 1024 \* 1024          |
| PHP عند الحاجة   | upload\_max\_filesize = 100M وpost\_max\_size = 105M |
| ال Reverse Proxy | client\_max\_body\_size 110m لمسار الرفع وحده        |
| Cloudflare       | 100 MB في الخطة المجانية                             |

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

### رسالة واضحة للمستخدم

افحص الحجم في المتصفح قبل أن يبدأ الرفع بالخاصية [File.size](https://developer.mozilla.org/en-US/docs/Web/API/File/size?ref=arabroot.io)، حتى لا ينتظر المستخدم دقائق ليعرف أن ملفه أكبر من المسموح:

```javascript
const MAX_BYTES = 100 * 1024 * 1024;

input.addEventListener('change', () => {
  const file = input.files[0];
  if (file && file.size > MAX_BYTES) {
    showError('حجم الملف أكبر من 100 ميجابايت');
    input.value = '';
  }
});
```

ولا تعتمد على هذا الفحص وحده، والسبب أن أي شخص يستطيع تجاوز الواجهة وإرسال الطلب مباشرة، لذلك أبق التحقق في السيرفر دائماً. وعالج الرمز [413](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/413?ref=arabroot.io) في كود الرفع برسالة خاصة به، ولا تعرض للمستخدم «خطأ في الشبكة» مع كل رد غير ناجح.

### الرفع المجزأ أو المباشر للملفات الكبيرة

إذا كانت ملفاتك تتجاوز مئة ميجابايت بانتظام، مثل مقاطع الفيديو والنسخ الاحتياطية Backups وملفات الأقراص، فلا ترفعها في طلب واحد، والسبب أن الطلب الطويل يفشل مع أي انقطاع بسيط ويعود المستخدم إلى الصفر. واختر أحد الحلين الموصوفين في قسم Cloudflare، فالرفع المجزأ بـ tus يناسبك إذا أردت أن تبقى الملفات على السيرفر، والروابط الموقعة مسبقاً تناسبك إذا كان التخزين أصلاً في S3 أو R2 أو MinIO، وهي ترفع الحمل عن السيرفر تماماً.

### لا تحمل الملف في الذاكرة، وتحقق من محتواه

- اكتب الملف إلى القرص أو التخزين وأنت تستقبله، ولا تحمله كاملاً في الذاكرة، فعشرة مستخدمين يرفعون في وقت واحد ملفات حجمها 500 ميجابايت يحتاجون إلى 5 جيجابايت من الذاكرة إذا حملتها، وعندما تنفد الذاكرة يتوقف التطبيق.
- راقب مساحة المجلدات المؤقتة، والسبب أن Nginx وPHP وإطارات العمل تكتب فيها قبل أن يصل الملف إلى مكانه النهائي، وداخل ال Container يكون `/tmp` غالباً على نفس القرص الذي يعمل عليه النظام.
- تحقق من نوع الملف بقراءة محتواه وليس بامتداده، وأعطه اسماً عشوائياً تولده أنت، واحفظه خارج المجلد الذي يخدمه الموقع مباشرة.
- إذا كان المستخدمون يتبادلون الملفات فيما بينهم فافحصها ببرنامج مكافحة البرمجيات الخبيثة Malware مثل [ClamAV](https://docs.clamav.net/?ref=arabroot.io) في مهمة خلفية قبل أن تتيحها للتنزيل.

## ملخص الأعراض وحلولها (Troubleshooting)

| العرض                                                                    | الطبقة                         | الحل                                                                              |
| ------------------------------------------------------------------------ | ------------------------------ | --------------------------------------------------------------------------------- |
| 413 في صفحة تنتهي بـ cloudflare، ولا أثر للطلب في سجلاتك                 | Cloudflare                     | رفع مجزأ، أو روابط موقعة مسبقاً، أو نطاق فرعي بوضع DNS Only                       |
| 524 بعد نحو دقيقتين                                                      | Cloudflare تنتظر رد التطبيق    | رد فوراً بعد حفظ الملف، وعالجه في مهمة خلفية                                      |
| 413 في صفحة تنتهي بـ nginx أو openresty                                  | Nginx أو NPM                   | ارفع client\_max\_body\_size للمسار أو المضيف                                     |
| 413 بنص Request Entity Too Large فقط                                     | Traefik                        | ارفع maxRequestBodyBytes في Buffering Middleware                                  |
| انقطاع بعد 60 ثانية تماماً، و499 في سجل Traefik                          | Traefik                        | ارفع respondingTimeouts.readTimeout على نقطة الدخول                               |
| 408 أثناء الرفع                                                          | Nginx أو HAProxy               | client\_body\_timeout، أو timeout http-request مع http-buffer-request             |
| 504 بعد اكتمال الرفع                                                     | ال Reverse Proxy ينتظر التطبيق | proxy\_read\_timeout أو fastcgi\_read\_timeout أو timeout server، أو معالجة خلفية |
| رد 200 و$\_FILES فارغة، وتحذير POST Content-Length ... exceeds the limit | PHP                            | ارفع post\_max\_size                                                              |
| $\_FILES\['file'\]\['error'\] يساوي 1                                    | PHP                            | ارفع upload\_max\_filesize                                                        |
| 413 برسالة The POST data is too large.                                   | Laravel وحد PHP                | ارفع post\_max\_size                                                              |
| 413 برسالة request entity too large مع JSON                              | Express                        | أرسل الملف بـ multipart، أو ارفع limit                                            |
| 500 برسالة File too large                                                | multer                         | عالج LIMIT\_FILE\_SIZE وأعد 413                                                   |
| ملف محفوظ أصغر من الأصل دون خطأ                                          | busboy                         | تحقق من truncated في نهاية التدفق                                                 |
| 400 وRequestDataTooBig                                                   | Django                         | DATA\_UPLOAD\_MAX\_MEMORY\_SIZE، أو أرسل الملف بـ multipart                       |
| رفض الطلبات فوق 28.6 ميجابايت تقريباً                                    | Kestrel أو IIS                 | \[RequestSizeLimit\] أو MaxRequestBodySize أو maxAllowedContentLength             |
| MaxUploadSizeExceededException مع ملف أكبر من 1 ميجابايت                 | Spring Boot                    | max-file-size وmax-request-size                                                   |

## الخلاصة

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

- الملف المرفوع يمر بعدة طبقات، ولكل طبقة حد للحجم وحد للزمن، والحد الأصغر هو الذي يقرر، لذلك لا تطارد الخطأ من طبقة إلى طبقة، وإنما ضع حداً صريحاً في كل طبقة واجعل التطبيق هو من يرفض.
- Cloudflare ترفض كل طلب فوق 100 MB في الخطتين Free وPro قبل أن يصل إلى السيرفر، وتنتظر رد التطبيق 125 ثانية فقط، ولا يغير ذلك أي إعداد على السيرفر، والحل هو الرفع المجزأ أو الروابط الموقعة مسبقاً.
- Nginx يرفض افتراضياً كل ما فوق `1m`، وTraefik يقطع أي طلب يستغرق أكثر من 60 ثانية، فراجع `client_max_body_size` و`readTimeout` قبل أي شيء آخر.
- PHP لا يرفض الطلب برمز خطأ وإنما يفرغه، واجعل `post_max_size` أكبر قليلاً من `upload_max_filesize` دائماً.
- اقرأ الرد وترويساته في أدوات المطور قبل أن تغير أي إعداد، واختبر من الداخل إلى الخارج بملف مولد، وأول طبقة يفشل عندها الرفع هي صاحبة الحد.
- لا تحمل الملف كاملاً في الذاكرة، وعالج المعالجة الثقيلة في مهمة خلفية حتى يرد التطبيق فوراً.

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

- أكتوبر 2026: كتابة المقال.