शुरुआती वेब प्लेन टेक्स्ट पर चलता था। आप किसी फॉर्म में यूजरनेम या कमेंट टाइप करते थे, सबमिट दबाते थे, और एक छोटा सा स्ट्रिंग HTTP के माध्यम से सर्वर तक पहुँच जाता था। उस सरल रिक्वेस्ट-रिस्पॉन्स चक्र (request-response cycle) ने बुनियादी ढांचे (infrastructure) को परिभाषित किया। जब लोगों ने फोटो, डॉक्यूमेंट और वीडियो साझा करना शुरू किया, तो इंजीनियरों को यह समझना पड़ा कि पूरी तरह से पठनीय टेक्स्ट (readable text) के लिए बने सिस्टम के माध्यम से रॉ बाइनरी डेटा (raw binary data) को कैसे भेजा जाए।
यदि आप किसी JSON ऑब्जेक्ट में JPEG डालने की कोशिश करते हैं, तो आप एक बुनियादी बाधा का सामना करते हैं। JSON एक टेक्स्ट प्रोटोकॉल है। यह वैध Unicode कैरेक्टर्स, कोट्स, ब्रेसेस और सही ढंग से एस्केप किए गए स्ट्रिंग्स (escaped strings) की अपेक्षा करता है। एक बाइनरी फ़ाइल केवल बाइट्स का एक लंबा क्रम होती है, जिनमें से कई का कोई प्रिंट करने योग्य प्रतिनिधित्व (printable representation) नहीं होता है। यदि आप उन बाइट्स को JSON स्ट्रिंग में जबरदस्ती भर देते हैं, तो पार्सर (parser) टूट जाता है, एस्केप सीक्वेंस पेलोड को दूषित कर देते हैं, और दूसरी तरफ पूरा मैसेज अपठनीय हो जाता है।
Base64 एन्कोडिंग एक स्पष्ट समाधान के रूप में उभरी। यह बाइनरी डेटा को साठ-चार प्रिंट करने योग्य ASCII कैरेक्टर्स के एक सीमित सेट में फिर से मैप करता है। बाइनरी के हर तीन बाइट्स टेक्स्ट के चार कैरेक्टर्स बन जाते हैं। पेलोड अब वैध JSON है, जिसका अर्थ है कि यह किसी भी मानक API के माध्यम से सुरक्षित रूप से जा सकता है। लेकिन इसकी कीमत तुरंत चुकानी पड़ती है। वह री-एन्कोडिंग फ़ाइल के आकार को लगभग तैंतीस प्रतिशत तक बढ़ा देती है। तीन मेगाबाइट की इमेज वायर पर चार मेगाबाइट की हो जाती है। डेटा को आगे-पीछे ट्रांसलेट करने के लिए क्लाइंट और सर्वर दोनों को अतिरिक्त CPU साइकिल खर्च करने पड़ते हैं। इससे भी महत्वपूर्ण बात यह है कि कई JSON सर्वर फ्रेमवर्क पार्स करने से पहले पूरी बॉडी को मेमोरी में पढ़ लेते हैं। कुछ ही एक साथ होने वाले बड़े अपलोड एक मामूली सर्वर को ठप कर सकते हैं क्योंकि प्रत्येक अपलोड को डिस्क पर सहेजने से पहले एक भारी टेक्स्ट स्ट्रिंग के रूप में RAM में रखा जाता है। Base64 ज़रूरत पड़ने पर काम तो करता है, लेकिन इसे प्रोडक्शन स्केल पर भारी फ़ाइलें ले जाने के लिए कभी डिज़ाइन नहीं किया गया था।
बेहतर उत्तर multipart/form-data है। यह फॉर्मेट एक एकल HTTP रिक्वेस्ट को अलग-अलग हिस्सों के संग्रह के रूप में मानता है, जिसमें प्रत्येक हिस्सा एक अद्वितीय बाउंड्री स्ट्रिंग (boundary string) द्वारा विभाजित होता है। एक हिस्सा प्लेन टेक्स्ट फ़ील्ड हो सकता है। अगला हिस्सा एक रॉ बाइनरी इमेज हो सकता है, जिसे अपने स्वयं के Content-Type और Content-Disposition हेडर के साथ चिह्नित किया गया हो। सर्वर आने वाले स्ट्रीम को क्रमिक रूप से (sequentially) पढ़ता है, बाउंड्री मार्कर्स पर नज़र रखता है, और प्रत्येक सेक्शन को उचित हैंडलर को सौंप देता है, बिना पूरे पेलोड को टेक्स्ट के एक एकल ब्लॉक के रूप में देखे।
Node.js में, यह अंतर विशेष रूप से महत्वपूर्ण है। express.json() मिडलवेयर JSON बॉडीज़ को पार्स करना जानता है, लेकिन यह फ़ाइल स्ट्रीम्स को हैंडल नहीं करता है। multipart अपलोड को प्रोसेस करने के लिए, आपको Multer या Busboy जैसे स्ट्रीमिंग पार्सर की आवश्यकता होती है। ये टूल्स रॉ रिक्वेस्ट स्ट्रीम से जुड़ते हैं और इसे टुकड़ों में (chunk by chunk) पढ़ते हैं। उदाहरण के लिए, Multer आपको यह चुनने की अनुमति देता है कि आने वाली फ़ाइलों को डिस्क पर एक अस्थायी फ़ोल्डर में लिखना है या छोटी फ़ाइलों को मेमोरी में रखना है। वह कॉन्फ़िगरेशन निर्णय मायने रखता है। यदि आप सब कुछ मेमोरी में रखते हैं और आपका ऐप अचानक एक साथ कई बड़ी फ़ाइलें प्राप्त करता है, तो आपका प्रोसेस हीप स्पेस (heap space) खत्म होने के कारण क्रैश हो सकता है। डिस्क पर लिखना स्थिरता के लिए I/O का त्याग करता है, लेकिन यह सफाई (cleanup) और पाथ सुरक्षा (path security) के बारे में अपने स्वयं के सवाल पैदा करता है।
एक छोटे एप्लिकेशन के लिए, फ़ाइलों को ./uploads जैसे स्थानीय फ़ोल्डर में सहेजना स्वाभाविक और तेज़ लगता है। फ़ाइल उसी मशीन पर पहुँचती है जो आपके कोड को चला रही है, और उसे वापस सर्व करना केवल सही पाथ की ओर इशारा करने का मामला है। यह तब तक काम करता है जब तक कि यह काम करना बंद न कर दे।
जिस क्षण आप दूसरे एप्लिकेशन सर्वर के सामने लोड बैलेंसर (load balancer) लगाते हैं, स्थानीय स्टोरेज एक बग बन जाता है। एक यूजर प्रोफाइल पिक्चर अपलोड करता है। लोड बैलेंसर रिक्वेस्ट को Server A पर भेजता है, और फ़ाइल Server A की डिस्क पर लिखी जाती है। बाद में, वह यूजर इमेज देखने के लिए कहता है, लेकिन लोड बैलेंसर रिक्वेस्ट को Server B पर भेज देता है। Server B अपने स्वयं के फाइलसिस्टम की जाँच करता है और उसे कुछ नहीं मिलता। फ़ाइल प्रभावी रूप से गायब हो जाती है। आप यूजर को उसी मशीन से जोड़ने के लिए स्टिकी सेशन्स (sticky sessions) लागू कर सकते हैं, लेकिन वह एक नाजुक समाधान है। यदि Server A रीस्टार्ट होता है, फिर से तैनात (redeploy) किया जाता है, या ऑटोस्केलिंग इंस्टेंस द्वारा बदला जाता है, तो डेटा गायब हो जाता है। कंटेनराइज्ड वातावरण (containerized environments) में, स्थानीय डिस्क और भी क्षणभंगुर (ephemeral) होती हैं। एक Docker कंटेनर का फाइलसिस्टम डिस्पोजेबल होने के लिए बनाया गया है। इसे स्थायी स्टोरेज के रूप में मानना यूजर डेटा खोने का एक विश्वसनीय तरीका है।
मानक समाधान कंप्यूट (compute) को स्टोरेज से अलग करना है। आप अपने एप्लिकेशन सर्वर को स्टेटलेस (stateless) रखते हैं और अपलोड की गई फ़ाइलों को AWS S3 या Google Cloud Storage जैसे समर्पित ऑब्जेक्ट स्टोरेज (object storage) पर भेजते हैं। ये सेवाएँ स्थायित्व (durability), भौगोलिक वितरण (geographic distribution) और व्यापक समवर्ती (massive concurrency) के लिए बनाई गई हैं। एप्लिकेशन सर्वर रिक्वेस्ट को हैंडल करता है, मेटाडेटा को वैलिडेट करता है, और फिर बाइट्स को विशेष रूप से उन्हें रखने के लिए डिज़ाइन किए गए इंफ्रास्ट्रक्चर को सौंप देता है।
फिर भी, यदि आप इसे लापरवाही से लागू करते हैं, तो यह पैटर्न भी एक बाधा (bottleneck) पैदा करता है। कई टीमें ब्राउज़र द्वारा फ़ाइल को backend पर अपलोड करने से शुरुआत करती हैं, और फिर backend द्वारा हर एक बाइट को object storage पर फॉरवर्ड करवाती हैं। यदि कोई उपयोगकर्ता पाँच सौ मेगाबाइट का वीडियो अपलोड करता है, तो आपका सर्वर एक बिचौलिए (middleman) की तरह काम करने लगता है। यह फ़ाइल को अंदर खींचने में बैंडविड्थ खर्च करता है, और फिर उसे S3 पर भेजने में और अधिक बैंडविड्थ खर्च करता है। ट्रांसफर की पूरी अवधि के दौरान कनेक्शन खुला रहता है। खराब नेटवर्क वाली स्थितियों वाले उपयोगकर्ताओं से होने वाले धीमे अपलोड सर्वर कनेक्शन को मिनटों तक व्यस्त रख सकते हैं। यदि सर्वर स्ट्रीम को बफर करता है, तो मेमोरी का उपयोग बढ़ा रहता है, और यदि आप metered hosting पर चल रहे हैं, तो आप एक ही डेटा ट्रांसफर के लिए दो बार भुगतान कर रहे हैं। हॉरिजॉन्टल स्केलिंग (Horizontal scaling) भी इसका समाधान नहीं करती है, क्योंकि आपके द्वारा जोड़ा गया प्रत्येक अतिरिक्त सर्वर अभी भी उन बाइट्स को ले जाने में फंसा रहता है जिन्हें देखने की उसे आवश्यकता नहीं है।
आधुनिक प्रणालियाँ डेटा पाथ (data path) से backend को पूरी तरह से हटाकर इस समस्या का समाधान करती हैं। फ़ाइल स्वीकार करने के बजाय, backend केवल अपलोड करने की अनुमति के लिए अनुरोध स्वीकार करता है। प्रवाह कुछ इस तरह दिखता है:
- ब्राउज़र backend से अपलोड शुरू करने का अनुरोध करता है, जिसमें आमतौर पर केवल फ़ाइल का नाम, फ़ाइल का प्रकार और इच्छित उद्देश्य भेजा जाता है।
- backend उपयोगकर्ता को प्रमाणित करता है, व्यावसायिक नियमों के विरुद्ध अनुरोध को मान्य करता है, और object storage provider से एक अस्थायी presigned URL जेनरेट करने के लिए SDK का उपयोग करता है।
- backend वह URL ब्राउज़र को वापस कर देता है। यह URL एक विशिष्ट bucket और key तक सीमित होता है, पाँच या पंद्रह मिनट जैसे कम समय के लिए मान्य होता है, और एक ऐसे टोकन के साथ हस्ताक्षरित (signed) होता है जो केवल आवश्यक सटीक अनुमतियाँ ही प्रदान करता है।
- ब्राउज़र एक मानक PUT या POST का उपयोग करके सीधे S3 या GCS पर फ़ाइल अपलोड करता है। बाइट्स सीधे उपयोगकर्ता के डिवाइस से
