हमने अपनी इमेज एक CDN पर भेज दीं और ऑरिजिन उन्हें फिर भी परोसता रहा
हमारे दस्तावेज़ स्क्रीनशॉट से भरे हुए हैं। हर पोर्टल फ़ीचर, लाइट और डार्क में कैप्चर किया गया, दो चौड़ाइयों पर, दस लोकेल में, जो जुड़कर सौ मेगाबाइट से कुछ ऊपर की इमेज बन जाता है। यह सब ऐप के अपने ही स्टैटिक बंडल में पड़ा था, ऑरिजिन से परोसा जाता था, जो सौ मेगाबाइट ऐसी तस्वीरों के लिए गलत जगह है जो कभी नहीं बदलतीं। तो हमने इसे Cloudflare R2 पर भेज दिया और आगे एक CDN लगा दिया, हर एनवायरनमेंट के लिए एक: प्रोडक्शन के लिए cdn.authagonal.io और हमारी dev साइट के लिए एक मेल खाता CDN होस्ट।
ऐप रनटाइम पर सही CDN होस्ट चुनता है, उस होस्टनेम को देखकर जहाँ से उसे परोसा जा रहा है। अगर पेज authagonal.io पर है, तो इमेज cdn.authagonal.io से आती हैं। सरल, कोई बिल्ड-टाइम कॉन्फ़िगरेशन नहीं, एक इमेज संदर्भ जो दोनों में से किसी भी एनवायरनमेंट पर सही तरह हल हो जाता है। हमने इसे भेजा, देखा कि ब्राउज़र हर इमेज को साफ़-साफ़ CDN से खींच रहा है, और फिर ऑरिजिन के ट्रैफ़िक को देखा। ऑरिजिन अब भी इमेज परोस रहा था। हर एक, पहली लोड पर, बिल्कुल पहले जैसी।
जो पेज एक क्रॉलर देखता है वह वह पेज नहीं जिसे ब्राउज़र बनाता है
यह क्यों हुआ यह समझाने के लिए, मुझे यह समझाना होगा कि हमारे मार्केटिंग पेज असल में क्या हैं। ये एक सिंगल-पेज ऐप हैं, पर एक सिंगल-पेज ऐप तब तक एक खाली खोल होता है जब तक JavaScript नहीं चलता, और एक खाली खोल सर्च इंजन के लिए बुरा है और पहली अर्थपूर्ण चीज़ के दिखने में लगने वाले समय के लिए भी बुरा है। तो बिल्ड के समय हम प्रीरेंडर करते हैं: हम साइट शुरू करते हैं, हर पेज को एक हेडलेस ब्राउज़र में खोलते हैं, उसे पूरी तरह रेंडर होने देते हैं, और नतीजे में मिली HTML को सहेज लेते हैं। वही प्रीरेंडर की गई HTML हम सबसे पहले परोसते हैं। उसमें पहले से ही सामग्री होती है, क्रॉलर एक असली पेज देखता है, उपयोगकर्ता तुरंत टेक्स्ट और इमेज देखता है, और फिर JavaScript कमान संभाल लेता है।
उस विवरण में एक समस्या छिपी है। प्रीरेंडर साइट को 127.0.0.1 पर चलाता है, जो बिल्ड मशीन पर एक लूपबैक पता है। तो जब ऐप पूछता है "मैं किस होस्टनेम पर हूँ, ताकि मैं एक CDN चुन सकूँ," तो प्रीरेंडर के दौरान जवाब होता है "एक लोकल IP पता," जो न तो authagonal.io से मेल खाता है और न ही dev होस्ट से। रनटाइम होस्ट-निष्कर्षण, वह चतुर हिस्सा, चुपचाप अपने एकमात्र बचे विकल्प पर गिर जाता है: ऑरिजिन-सापेक्ष पथ। और वही ऑरिजिन पथ प्रीरेंडर की गई HTML में जम जाता है।
तो हर विज़िटर को ऐसी HTML मिली जिसमें ऑरिजिन इमेज URL पके हुए थे, और ब्राउज़र ने वही किया जो स्पष्ट था: उसने उन्हें ऑरिजिन से, पहली पेंट पर, ला लिया। एक पल बाद JavaScript ने पेज को फिर से रेंडर किया, होस्टनेम को फिर से निकाला, इस बार असली वाला पाया, और इमेज को CDN पर बदल दिया। हर इमेज के लिए दो फ़ेच, पहला ठीक उसी सर्वर पर पड़ता जिसे हम बचाने की कोशिश कर रहे थे, और पूरी तरह अदृश्य जब तक कि आप ऑरिजिन के लॉग न देख रहे हों, क्योंकि पेज देखने और काम करने में बिल्कुल सही था।
वह फ़िक्स जिसने ग्यारह मिनट के लिए हालात और बिगाड़ दिए
पहला फ़िक्स लुभावना वाला था। अगर समस्या यह है कि एक गलत URL प्रीरेंडर की गई HTML में जम जाता है, तो प्रीरेंडर के दौरान कोई URL निकालो ही मत। स्नैपशॉट में इमेज सोर्स खाली छोड़ दो, और JavaScript को सही CDN URL भरने दो जब वह चले और असली होस्टनेम जान ले। कोई गलत URL नहीं, कोई ऑरिजिन फ़ेच नहीं।
यह भेजा गया, और यह गलत था, और हमने इसे करीब ग्यारह मिनट बाद बदल दिया। प्रीरेंडर की गई HTML में इमेज सोर्स को खाली करना उस पूरी वजह को ही मात दे देता है जिसके लिए हम प्रीरेंडर करते हैं। जो क्रॉलर स्टैटिक पेज पढ़ता है वह अब कोई इमेज नहीं देखता, और ब्राउज़र के पास तब तक दिखाने को कुछ नहीं होता जब तक JavaScript न चले, जो वही धीमा रास्ता है जिससे बचने के लिए हम प्रीरेंडर की गई HTML बनाते हैं। हमने दोहरे फ़ेच को हटाया, पर इमेज को पेज की उस इकलौती वर्शन से हटाकर जो JavaScript से पहले मौजूद होती है। यह एक ट्रैफ़िक समस्या को एक सर्च-और-पहली-पेंट समस्या से बदल देता है, जो एक बदतर सौदा है।
उन ग्यारह मिनटों का सबक: प्रीरेंडर की गई HTML में एक असली, काम करने वाला इमेज संदर्भ होना चाहिए। बग कभी यह नहीं था कि एक URL मौजूद था। बग यह था कि गलत URL मौजूद था, और उसे खाली करना बस एक खामी को दूसरी से बदल देता है। हमें असल में यह चाहिए था कि सही URL प्रीरेंडर के समय जानने योग्य हो, और वह नहीं है, क्योंकि प्रीरेंडर के समय कोई नहीं जानता कि कौन सा एनवायरनमेंट पेज परोसेगा।
आख़िरी संभव पल पर तय करना
सही URL रिक्वेस्ट होस्ट पर निर्भर करता है, और रिक्वेस्ट होस्ट तब तक पता नहीं होता जब तक कोई रिक्वेस्ट न हो। प्रीरेंडर एक ही बार होता है, बिल्ड के समय, और एक फ़ाइल बनाता है जो दोनों एनवायरनमेंट पर परोसी जाती है। तो URL का एनवायरनमेंट-विशिष्ट हिस्सा बिल्ड के समय तय नहीं किया जा सकता। इसे हर रिक्वेस्ट पर तय करना पड़ता है, और स्टैक में एकमात्र चीज़ जो हर रिक्वेस्ट और उसका होस्ट देखती है वह एज पर मौजूद वेब सर्वर है।
तो प्रीरेंडर अब न कोई URL लिखता है न कोई खाली जगह। वह एक प्लेसहोल्डर लिखता है, एक शाब्दिक सेंटिनल स्ट्रिंग, https://cdn.authagonal.io, CDN होस्ट की जगह। जो प्रीरेंडर की गई HTML बिल्ड से निकलती है वह कहती है https://cdn.authagonal.io/screenshots/whatever.webp, जो न तो ऑरिजिन URL है न खाली सोर्स। यह एक ऐसा URL है जिसमें एक छेद है।
nginx बाहर जाते समय उस छेद को भर देता है। वह रिक्वेस्ट के होस्ट को सही CDN बेस से मैप करता है, नॉन-प्रोडक्शन होस्ट को उसके अपने dev CDN से, authagonal.io को https://cdn.authagonal.io से, बाकी सब कुछ को खाली से ताकि URL एक सुरक्षित लोकल फ़ॉलबैक के रूप में वापस ऑरिजिन-सापेक्ष पर सिमट जाए। फिर एक ही निर्देश उस सेंटिनल को उस मान से बदल देता है जब HTML क्लाइंट तक स्ट्रीम होती है:
map $host $asset_cdn_base {
default "";
"~*example\.dev$" "https://cdn.example.dev";
"~*authagonal\.io$" "https://cdn.authagonal.io";
}
sub_filter 'https://cdn.authagonal.io' $asset_cdn_base;
अब स्टैटिक HTML ऑरिजिन से पहले से ही असली CDN URL लेकर निकलती है, जो भी होस्ट ने पूछा उसके लिए सही। ब्राउज़र पहली पेंट पर सीधे CDN से फ़ेच करता है। पूर्ववत करने के लिए कोई गलत पहला फ़ेच नहीं है, क्योंकि कोई गलत URL नहीं है, और कभी कोई खाली भी नहीं था। क्रॉलर असली इमेज लिंक के साथ एक पूरा पेज देखता है। एक फ़ाइल दोनों एनवायरनमेंट को परोसती है, और किसी को भी अलग बिल्ड की ज़रूरत नहीं।
प्रतिक्रिया के कौन से हिस्से फिर से लिखे जाते हैं इसमें एक छोटी सी सुंदरता है। यह पुनर्लेखन केवल HTML तक सीमित है, जो nginx का डिफ़ॉल्ट है। यह मायने रखता है क्योंकि वही सेंटिनल स्ट्रिंग JavaScript बंडल में भी आती है, होस्ट-निष्कर्षण कोड की फ़ॉलबैक शाखा के रूप में। क्लाइंट पर वह शाखा मृत कोड है, क्योंकि ब्राउज़र के पास असली होस्टनेम होता है और वह कभी सेंटिनल नहीं निकालता, इसलिए हम खासकर नहीं चाहते कि nginx बंडल को छुए। पुनर्लेखन को उसके केवल-HTML डिफ़ॉल्ट पर छोड़ देने से यह मुफ़्त में मिल जाता है। हमें संयोगवश यह पता चला कि डिफ़ॉल्ट को स्पष्ट रूप से लिख देना उसे छोड़ देने से बदतर है, क्योंकि एक फ़ालतू घोषणा nginx को एक डुप्लिकेट के बारे में चेतावनी देने पर मजबूर कर देती है।
फ़ाइल को स्टार्टअप पर एक बार ही क्यों न फिर से लिख दें
स्पष्ट आपत्ति यह है कि यह एक ऐसे मान के लिए हर रिक्वेस्ट पर बहुत सारा काम है जिसकी सिर्फ़ दो ही संभावनाएँ हैं। क्यों न HTML को एक बार फिर से लिख दें जब कंटेनर बूट होता है, सही होस्ट पका दें, और बिना किसी फ़िल्टर के एक स्टैटिक फ़ाइल परोसें?
क्योंकि कंटेनर खुद पर नहीं लिख सकता। स्टैटिक साइट nginx में एक नॉन-रूट उपयोगकर्ता के रूप में चलती है, जिसमें एक रीड-ओनली रूट फ़ाइलसिस्टम है और हर Linux क्षमता हटा दी गई है, सिर्फ़ nginx की अपनी अस्थायी फ़ाइलों के लिए एक छोटी सी लिखने योग्य स्क्रैच डायरेक्टरी के साथ। यह एक जानबूझकर की गई हार्डनिंग मुद्रा है: जो वेब सर्वर अविश्वसनीय ट्रैफ़िक परोसता है उसे उन फ़ाइलों को बदल पाने में सक्षम नहीं होना चाहिए जिन्हें वह परोसता है, ताकि कोई हमलावर जो पैर जमाने की जगह पा ले उसे फिर से लिखने को कुछ न मिले। HTML का स्टार्टअप-समय पुनर्लेखन ठीक वही लिखने की पहुँच माँगेगा जिसे हमने जानबूझकर हटा दिया था। रीड-ओनली फ़ाइलसिस्टम कोई ऐसी बाधा नहीं जिसके इर्द-गिर्द से काम किया जाए, यह एक ऐसा गुण है जो हम चाहते हैं, और यह स्टार्टअप-समय पुनर्लेखन को साफ़-साफ़ बाहर कर देता है। हर-रिक्वेस्ट पुनर्लेखन किसी भी फ़ाइल को बिल्कुल नहीं छूता, और यही वजह है कि यह एक ऐसे सर्वर के अनुकूल है जिसके पास बदलने लायक कुछ भी अपना नहीं।
हम CDN होस्ट को बिल्ड के समय पका कर दो अलग-अलग इमेज बंडल भी नहीं बनाना चाहते थे, हर एनवायरनमेंट के लिए एक, क्योंकि इससे वही एनवायरनमेंट-विशिष्ट बिल्ड फिर से आ जाती है जिससे हम अभी-अभी छुटकारा पाए थे। होस्ट-निष्कर्षण का मकसद दोनों एनवायरनमेंट के लिए एक ही आर्टिफ़ैक्ट था। सेंटिनल वह वादा निभाता है: एक बिल्ड, एक फ़ाइल, और एनवायरनमेंट आख़िरी संभव पल पर एज पर हल किया गया।
प्रीरेंडरिंग एनवायरनमेंट के मान जमा देती है, इसलिए उन्हें एज पर हल करें
प्रीरेंडरिंग एक पल को जमा देती है। आपका ऐप अपने एनवायरनमेंट को देखकर जो भी तय करता है वह प्रीरेंडर के समय बिल्ड के एनवायरनमेंट के हिसाब से तय हो जाता है, जो एक लूपबैक पता है और किसी का असली होस्ट नहीं। तो जो मान रनटाइम पर सही होता है वह उसी पल गलत हो जाता है जब उसका स्नैपशॉट लिया जाता है, और यह चुपचाप विफल हो जाता है, क्योंकि प्रीरेंडर किया गया पेज फिर भी रेंडर होता है, बस गलत जगह की ओर इशारा करता हुआ।
सामान्य हल यह है कि प्रीरेंडर के दौरान एनवायरनमेंट-विशिष्ट मानों को बिल्कुल हल ही न किया जाए। एक प्लेसहोल्डर निकालो, उसे स्टैटिक आर्टिफ़ैक्ट के आर-पार बरकरार ले जाओ, और उसे उस परत पर हल करो जो वाकई एनवायरनमेंट देखती है, जो हर-रिक्वेस्ट वाले मान के लिए एज है। यह लेट बाइंडिंग है, एक HTML फ़ाइल में एक स्ट्रिंग पर लागू: हर उस चरण में छेद खुला छोड़ दो जो जवाब नहीं जानता, और उसे उस एक चरण पर भर दो जो जानता है।
अगर आप चाहेंगे कि आपके पहचान प्रदाता के अपने दस्तावेज़ और एसेट पहले से ही एज से सही तरह परोसे जाएँ, तो यह उन कई छोटी-छोटी चीज़ों में से एक है जिन्हें Authagonal पहले ही सुलझा चुका है, ताकि आप वह बहस अपने खुद के प्रोडक्ट पर लगा सकें।