ساخت پاسخ خطای استاندارد در API های HTTP به زبان آدمیزاد

ساختن API کار سختی به نظر نمیآید، تا اینکه شروع به فکرکردن در مورد برگرداندن خطاها میکنی.
ما در HTTP یک سری status code داریم که بخشی از آنها نشاندهندهی خطاهای عمومی هستند. امّا مشکل اینجا است که برای کاربردهای واقعی، این چندتا شناسهی خطای محدود و کلّی کافی نیستند.
از طرفی اگر مجبور به استفاده از API های دیگران بشوید، میبینید که اکثر اوقات پیام خطایی که برمیگردد هیچ کمکی به شما برای فهمیدن اینکه چطوری میتوان مشکل را حل کرد نمیکند.
در این نوشته میخواهیم راه حل استانداردی که از سال ۲۰۱۶ معرّفی شده ولی هنوز خیلیها ازش استفاده نمیکننند را یادبگیریم. آن هم همراه با یاد و خاطرهی بزرگترین ساختههای سینمایی بشر.
صد و یک راه برای عصبانیکردن توسعهدهندگان
در حال حاضر هرکسی برای خودش روشی را پیشگرفته و خطاها را به همان روش برمیگرداند. شاید با خودتان فکرکنید که همین که یک مدل انتخاب شود و به همان پایبند بمانیم همهچیز خوب خواهد بود و مشکلی پیش نخواهد آمد.
امّا زندگی به همین سادگیها نیست. اگر همان یک روش، روش ناکارامدی باشد، پایبندی به آن تنها زجرکشیدن ما را طولانیتر میکند.
به علاوه ما خیلی وقتها مجبوریم که از API های مختلف استفاده کنیم. حالا اگر هرکدام از اینها برای خودشان یک ساختار را در پیش بگیرند، حتّی اگر آن ساختار خوب هم باشد، باز مصرفکننده باید برای میلیاردها استاندارد مختلف کد مجزا تولید کند.
بیایید قبل از اینکه سراغ روش استاندارد برویم، در اینجا دو مشکل اساسی نبود استاندارد برای توصیف خطا در API ها را ببینیم.
مرد انگلیسی که از یک تپه بالارفت ولی از یک کوه پایین آمد

خیلی از افراد خطاها را به مسخرگی اسم فیلم «The Englishman Who Went Up a Hill But Came Down a Mountain» مدیریت میکنند.
یعنی شما در پاسخ درخواستتان یک پیام که مقدار status code آن برابر ۲۰۰ است (یعنی موفّق) دریافت میکنید که درون دادههای آن نوشته شده است که درخواست شما با مشکلی روبهرو شده است.
این کار عملاً سوء استفاده از پروتکل محسوب میشود. چون شما منطق انجام موفّقیّتآمیز عملیّات را با منطق ناموفّق آن قاطی کردهاید.
نتیجهاش میشود مصرفکنندهای که پدرش در میآید تا بفهمد که بالأخره درخواستش موفق بوده یا نه.
موجودات به شکل باورنکردنیای عجیب که دست از زندگی کشیدهاند و تبدیل به نیمهزامبی شدهاند
اگر مجبور باشید از چند API استفاده کنید که هرکدام ساختار اعلام مشکل خودشان را دارند، احتمالاً نتیجهی نهایی کدتان شبیه به فیلم «The Incredibly Strange Creatures Who Stopped Living and Became Mixed-Up Zombies» خواهد بود.
حالا ما اینجا نمیخواهیم به API شرکت شایانسازان پارسا اشاره کنیم، ولی بیایید به استانداردهای دوتا شرکت درست و حسابی نگاهی بکنیم.
این ساختار پیام خطای استاندارد گوگل است:
{ "error": { "errors": [ { "domain": "global", "reason": "invalidParameter", "message": "Invalid string value: 'asdf'. Allowed values: [mostpopular]", "locationType": "parameter", "location": "chart" } ], "code": 400, "message": "Invalid string value: 'asdf'. Allowed values: [mostpopular]" } }
و این هم ساختار پیام خطای استاندارد فیسبوک:
{ "error": { "message": "Invalid OAuth access token.", "type": "OAuthException", "code": 190, "error_subcode": 1234567, "fbtrace_id": "BLBz/WZt8dN" } }
همانطوری که میبینید محض آرامش خاطر Ray Dennis Steckler هم که شده (همان کارگردان شاهکار: موجودات به شکل باورنکردنیای عجیب…) حتّی در یک فیلد هم با هم اشتراک ندارند.
این تفاوت زیاد در روشهای مختلف، توسعهی نرمافزارهایی را که به API های مختلف وابستهاند را به شدّت سخت میکند.
توصیف استاندارد مشکلات با RFC 7807
خبر خوش این است که در سال ۲۰۱۶ سازمان فخیمهی Internet Engineering Task Force منّت بر دیدگان توسعهدهندگان سراسر جهان گذاشت و با انتشار RFC 7807 یک روش استاندارد و خیلی مناسب را برای توضیح مشکل در API های HTTP ارائه کرد.
با استفاده از این استاندارد، میتوان مشکلات و خطاها را به شکلی در API مشخّص کرد که هم انسانها که کاربر نهایی هستند بتوانند به بهترین شکل متوجّه دلیل خطا بشوند و هم نرمافزارها بتوانند به راحتی خطاها را مدیریت کنند.
حالا بیایید با هم ببینم که این استاندارد چه میگوید.
پاسخ خطا باید با چه فرمتی ساخته شود؟
طبق این استاندارد ما میتوانیم خطاها را به شکل یک شئ JSON یا یک مستند XML توصیف کنیم. هرچند که استفاده از JSON ترجیح بیشتری دارد و در این نوشته هم ما تنها به آن میپردازیم.
پیامهای HTTP که حامل توصیف مشکل هستند و از این استاندارد پیروی میکنند، باید Content-Type
شان یکی از مقادیر: application/problem+json
یا application/problem+xml
باشد.
اینطوری از همان ابتدای پیام مشخّص است که قرار است با یک خطا که طبق این استاندارد ساخته شده است کار کنیم.
مثلاً فرضکنید که ما یک وبسایت مدیریت دروس برای دانشگاه داریم. در این وبسایت یک API وجود دارد که میتوان با فرستادن متد GET به آن لیست تکالیف هفتهی بعد را گرفت.
بگذارید بگوییم آدرس این API این است:
https://university.xyz/api/2/course/<id>/assignments
خب حالا فرضکنید که این API به افرادی که در این درس ثبت نام نکردهاند خطا برمی گرداند. از اینجا به بعد بخشهای مختلف استاندارد توصیف خطا را روی همین مثال جلو میبریم.
خب حالا ما میخواهیم به کاربری که در درس مهندسی اینترنت ثبت نام نکرده ولی درخواست گرفتن لیست تکالیف آن را دارد خطا نشان بدهیم (فرضکنید که id این درس عدد ۱ است).
پاسخی که کاربر به عنوان یک پیام HTTP میگیرد چیزی شبیه به این است (بخش header میتواند خیلی بیشتر باشد ولی چون اکثر بخشهایش به این نوشته مربوط نیستند، آنها را نادیده میگیریم):
HTTP/1.1 403 Forbidden Content-Type: application/problem+json; charset=UTF-8 Content-Language: fa
خب حالا ببینیم که چطوری محتوای مرتبط با این پیام را میتوانیم بسازیم.
نوع خطا
هر خطا میتواند بخشهای مختلفی داشته باشد که وجود هیچکدام از آنها اجباری نیست. امّا چند بخش آن به صورت صریح درون استاندارد مشخّص شده اند.
یکی از بخشهای خیلی مهم یک پیام خطا، نوع خطا است.
نوع خطا با کلید type
در شئ json ما قرار میگیرد. در استانداردی که درون این RFC مشخّص شده است، ما نوع خطا را به جای استفاده از مقادیر عددی یا رشتهها، با یک URI مشخّص میکنیم.
این آدرس باید به یک صفحهی HTML ختم بشود که درون آن توضیحات کامل درمورد این خطا و روش حل آن نوشته شده باشد.
ما header پاسخ HTTP را در بخش قبل دیدیم و از اینجا به بعد دیگر آن را تکرار نمیکنیم. حالا محتوایی که آن پاسخ HTTP با خودش حمل میکند چیزی شبیه به این میشود:
{ "type": "https://university.xyz/api/2/errors/course-not-accessible" }
خب پیام خطای ما تا اینجا این شکلی است. حالا اگر کاربر آدرسی که برای type
وارد کردهایم را درون مرورگرش وارد کند، باید به صفحهای برسد که به صورت کامل توضیح داده که این خطا برای چه به وجود میآید و برای حل آن چه کاری میتوان کرد.
مثلاً در این مورد باید توضیح بدهد که کاربران تنها به لیست تکالیف دروسی که در آن شرکت میکنند دسترسی دارند و بگوید که برای ثبت نام در درس، کاربر باید چه کاری انجام بدهد.
عنوان خطا
یکی دیگر از بخشهای خطا، عنوان آن است. عنوان با کلمهی کلیدی title
درون شئ خطا مشخّص میشود.
عنوان خطا یک متن کوتاه و قابل فهم برای انسانها است که باید برای تمام خطاهایی که نوع یکسانی دارند، مشابه باشد. مگر اینکه سایت شما کاربرانی با زبانهای مختلف داشته باشد و بخواهید عنوان خطا را برای زبانهای مختلف ترجمه کنید.
{ "type": "https://university.xyz/api/2/errors/course-not-accessible", "title": "شما اجازهی دسترسی به این درس را ندارید." }
شناسهی خطا
شناسهی خطا همان status code پیامهای HTTP است. یعنی چیزی نیست که درون شئ json ما قرار بگیرد. هدف از اشاره به آن صرفاً این است که حواستان باشد که باید مرتبطترین status code را به خطایی که رخ داده به عنوان http status code انتخاب کنید.
مثلاً در مثالی که ما در این نوشته داریم روی آن کار میکنیم، status را برابر ۴۰۳ گذاشتهایم. چون کاربر اجازهی دسترسی به این بخش را ندارد.
یک چیز دیگری که باید حواستان به آن باشد این است که اگر در پاسخی که دارید برمیگردانید به بیش از یک مشکل اشاره شده است (در بخشهای بعدی میبینیم که چطوری میتوان این کار را کرد) باید از شناسهی 207 استفاده کنید.
توضیحات خطا
بخش دیگری که میتواند درون شئ خطا قرار بگیرد مقدار detail
است. در این بخش ما باید یک توضیح درمورد مشکلی خاصی که الان پیش آمده است بدهیم.
مثلاً در مثال ما، باید توضیح بدهیم که چرا کاربر نمیتواند لیست تکالیف را ببیند:
{ "type": "https://university.xyz/api/2/errors/course-not-accessible", "title": "شما اجازهی دسترسی به این درس را ندارید.", "detail": "شما امکان دریافت لیست تکالیف درس مهندسی اینترنت را ندارید. چون این درس در لیست دروس ثبت نامی شما قرار ندارد." }
شناسهی رویداد خطا
هر خطایی که برای یک کاربر رخ میدهد، میتواند یک شناسهی اختصاصی داشته باشد. این شناسه نوع خطا باید یک URI باشد.
این شناسه به چه دردی میخورد؟ خب شاید در مثال ما کاربرد دقیقی برایش پیدا نکنید. ولی مثلاً فرضکنید که اپلیکیشن پرداخت دارید.
کاربر از طریق برنامهی شما میخواهد کارتبهکارت انجام بدهد ولی با خطای نداشتن موجودی کافی روبهرو میشود. شما در شئ json پیام خطایی که به او ارسال میکنید، کلید instance
را قرار میدهید. مقدار این کلید یک آدرس اینترنتی به صفحهای است که در آن کاربر میتواند اطّلاعاتی مثل کارت مبدأ و مقصد، مبلغ انتقال، موجودی کارتش در آن زمان و… را ببیند.
علاوه بر خود کاربر، این آدرس میتواند بعداً مورد استفادهی بخشهای امور مشتریان، بازاریابی یا … قرار بگیرد.
افزودن بخشهای اضافی به خطا
شما میتوانید علاوه بر اطّلاعات بالا، هر دادهی دیگری را که دوستداشتید درون شئ خطای خودتان قرار بدهید. طبق استاندارد هر مصرفکنندهای که نمیداند با یک کلید خاص درون شئ خطا چه کار کند، باید آن را نادیده بگیرد. پس لازم نیست نگران این باشید که اضافهکردن دادههای جدید ممکن است backward compatibility این API را از بین ببرد.
مثلاً فرضکنید ما میخواهیم در پیام خطایی که تا الان ساختهایم، به کاربر لیست درسهایی را که میتواند تکالیف آنها را ببیند را هم برگردانیم.
خب خیلی راحت میتوانیم یک کلید را به نام user-active-courses
به خطا اضافه کنیم:
{ "type": "https://university.xyz/api/2/errors/course-not-accessible", "title": "شما اجازهی دسترسی به این درس را ندارید.", "detail": "شما امکان دریافت لیست تکالیف درس مهندسی اینترنت را ندارید. چون این درس در لیست دروس ثبت نامی شما قرار ندارد.", "user-active-courses": [0, 5, 22, 185] }
شما از این امکان حتّی میتوانید برای ارسال چندین خطا در پاسخ یک درخواست استفاده کنید. برای این کار کافی است یک کلید به این شئ اضافه کنید که مقدارش یک شئ JSON دیگر باشد. اینطوری خطاهای دیگر را میتوانید درون آن قرار بدهید.
آیا هیرونیموس مرکین هرگز میتواند مرسی هامپ را فراموش کند و شادی واقعی را پیدا کند؟

حالا شاید از خودتان میپرسید که این استاندارد میتواند جایگزین حالتهای فعلی بشود یا باید سالها صبرکنیم تا دیگران از آن پشتیبانی کنند؟
خبر خوب این است که طبق این استاندارد شما همیشه باید مرتبطترین کد خطا را برای status code تعیین کنید. به همین خاطر حتّی مصرفکنندههایی که ابداً از وجود این استاندارد خبر ندارند، بدون مشکل میتوانند به کارشان ادامه بدهند. چون اینطوری همان منطق قدیمی HTTP حفظ شده است و اگر مصرفکننده پیش از این درست کار میکرده، الان هم باید درست کار کند.
الان که خیالتان راحت شد که میتوانید بدون مشکل از این استاندارد استفاده کنید و روشهای پیشین را به فراموشی بسپارید، شما هم میتوانید به اندازهی تماشاگران «Can Heironymus Merkin Ever Forget Mercy Humppe and Find True Happiness?» از شادی واقعی لذّت ببرید.
تخت مرگ: تختی که انسانها را میخورد
خب این استاندارد خیلی چیز جالبی است و میتواند مشکلات بزرگی را حل کند. امّا این وسط یک مشکلی وجود دارد. اگر خطای ما به خوبی با همان status code های عادی HTTP قابل بیان بود چی؟
طبق RFC، مقدار پیشفرض type
برابر با about:blank
است. معنی این مقدار این است که این خطا هیچ معنایی اضافهتر از معنای status code ندارد.
طبق RFC شما در این حالت باید مقدار title
را برابر عبارت متناظر با آن status code قرار بدهید. یعنی اگر status code شما برابر با عدد 403
است، شما باید title
را برابر مقدار Forbidden
بگذارید.
انجام این کار شاید در نگاه اوّل خوب به نظر برسد، ولی ما عبارت Forbidden
را یک بار هم درون header پاسخ HTTP نوشتهایم. یعنی داریم یک مقدار بیمعنی را دوبار ارسال میکنیم.
این کار به اندازهی عنوان فیلم «Death Bed: The Bed That Eats» زائد و به اندازهی خود این فیلم بیمعنی است.
شاید بهتر باشد که در چنین حالاتی بیخیال فرستادن مقداری همراه با پاسخ خودتان بشوید.
به هرحال این استاندارد طوری طراحی شده که مصرفکنندگانی که اصلاً از وجودش هم خبری ندارند بتوانند از آن استفاده کنند.
خب این هم از طنابی که میخواهد ما را از چاه عمیق پیامهای خطای گوناگون و بیدر و پیکر نجات بدهد. با اینکه من تمام تلاشم را کردم تا با اسامی و کلیپهای فاجعهبارترین فیلمهای تاریخ حواستان را پرت کنم، ولی امیدوارم که این نوشته به دردتان خورده باشد و از این به بعد بتوانید به شکلی بهتر خطاها را در API هایتان مدیریت کنید.
خداقوت، مطلب خیلی خفنی بود.
لطفا بیشتر بنویسید، مطالبتون خیلی شاخه!
ممنون.
چشم، تلاش میکنم بیشتر بنویسم.