معماری REST یک معماری stateless است که در آن مشتریان میتوانند به منابع روی یک سرور دسترسی داشته باشند و آنها را تغییر دهند. به طور کلی، سرویسهای REST از HTTP برای معرفی مجموعهای از منابعی که برای مدیریت و ارائهی API نیاز دارند، استفاده میکنند. در این یادداشت ما درباره برخی از بهترین روشها برای مدیریت خطا REST API استفاده میشود، صحبت میکنیم.
آشنایی با کدهای وضعیت HTTP
هنگامی که یک کلاینت درخواستی را به یک سرور HTTP ارسال میکند و سرور با موفقیت درخواست را دریافت میکند، سرور باید به مشتری اطلاع دهد که آیا درخواست با موفقیت انجام شده است یا خیر.
HTTP این کار را با پنج دسته از کدهای وضعیت انجام میدهد:
سطح ۱۰۰ (اطلاعاتی): سرور درخواست را تایید میکند.
سطح ۲۰۰ (موفقیت): سرور درخواست را همانطور که انتظار میرود، تکمیل کرده است.
سطح ۳۰۰ (ریدایرکتشده): کلاینت باید برای تکمیل درخواست، اقدامات بیشتری را انجام دهد.
سطح ۴۰۰ (خطای کلاینت): کلاینت بک درخواست نامعتبر ارسال کرده است.
سطح ۵۰۰ (خطای سرور): سرور به دلیل خطا در سرور، درخواست معتبر را انجام نداد.
بر اساس کد پاسخ، کلاینت میتواند نتیجه یک درخواست خاص را حدس بزند.
رسیدگی به خطا
اولین گام در رسیدگی به خطاها ارائهی کد وضعیت مناسب به مشتری است. علاوهبراین، ممکن است لازم باشد اطلاعات بیشتری را در بدنه پاسخ ارائه کنیم.
پاسخهای اساسی
سادهترین راه برای رسیدگی به خطاها این است که با یک کد وضعیت مناسب پاسخ دهیم. در اینجا چند کد پاسخ متداول را معرفی خواهیم کرد.
کد Bad Request 400: کلاینت یک درخواست نامعتبر مانند عدم وجود بدنه یا پارامتر درخواست مورد نیاز را ارسال کرده است.
کد ۴۰۱ Unauthorized: کلاینت نتوانسته است که به کمک سرور احراز هویت کند.
کد ۴۰۳ Forbidden: کلاینت احراز هویت شده است، اما اجازه دسترسی به منبع درخواستی را ندارد.
کد ۴۰۴ Not Found: منبع مورد درخواست وجود ندارد.
کد ۴۱۲ Precondition Failed: یک یا چند شرط در فیلدهای سرصفحه درخواست به نادرست ارزیابی شده است.
کد ۵۰۰ Internal Server Error: یک خطای عمومی در سرور رخ داده است.
کد ۵۰۳ Service Unavailable : سرویس مورد درخواست در دسترس نیست.
در حالی که این کدها اولیه هستند، به کلاینت اجازه میدهند تا ماهیت گسترده خطای رخ داده را درک کند. ما میدانیم که اگر برای مثال خطای ۴۰۳ دریافت کنیم، مجوز دسترسی به منبع درخواستی را نداریم. با این حال، در بسیاری از موارد، باید جزئیات تکمیلی را در پاسخهای خود ارائه کنیم.
کد خطای ۵۰۰ نشان میدهد که برخی از مشکلات یا استثناها در سرور هنگام رسیدگی به یک درخواست رخ داده است. اما به طور کلی، این خطای داخلی مربوط به کلاینت ما نیست.
بنابراین، برای به حداقل رساندن این نوع پاسخها به مشتری، باید با جدیت تلاش کنیم تا خطاهای داخلی را کنترل یا شناسایی کنیم و در صورت امکان با سایر کدهای وضعیت مناسب پاسخ دهیم.
به عنوان مثال، اگر به دلیل وجود نداشتن منبع درخواستی، یک محدودیت رخ دهد، باید آن را به جای خطای ۵۰۰، به عنوان یک خطای ۴۰۴ نشان دهیم.
این بدان معنا نیست که ۵۰۰ هرگز نباید نشان داده شود، بلکه فقط باید در صورتی استفاده شود که سرور توانایی انجام درخواست را ندارد؛ مانند قطع سرویس.
پاسخهای پیش فرض خطاهای اسپرینگ
این اصول آنقدر در همه جا وجود دارند که اسپرینگ آنها را در مکانیزم مدیریت خطای پیش فرض خود مدون کرده است.
برای نشان دادن، فرض کنید یک برنامه ساده Spring REST داریم که کتابها را با یک نقطه پایانی برای بازاریابی کتاب یا شناسه، مدیریت میکند:
۱ ۲ |
curl -X GET -H "Accept: application/json" http://localhost:8082/spring-rest/api/book/1 |
اگر کتابی با شناسه ۱ وجود نداشته باشد، ما انتظار داریم که کنترلکننده ما یک BookNotFoundException نشان دهد.
با انجام یک GET در این نقطه پایانی، میبینیم که این استثنا نشان داده شده است . این بدنه پاسخ است:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ |
{ "timestamp":"۲۰۱۹-۰۹-16T22:14:45.624+0000", "status":۵۰۰, "error":"Internal Server Error", "message":"No message available", "path":"/api/book/1" } |
در نظر داشته باشید که این کنترل کننده خطای پیشفرض شامل یک مهر زمانی از زمان وقوع خطا، کد وضعیت HTTP، یک عنوان (فیلد خطا)، پیامی در صورت فعال بودن در خطای پیشفرض و مسیر URL یعنی جایی که خطا رخ داده، است.
این فیلدها اطلاعاتی را در اختیار مشتری یا توسعهدهنده قرار میدهند تا به عیبیابی مشکل کمک کنند و همچنین تعدادی از فیلدهایی را تشکیل میدهند که مکانیزمهای استاندارد رسیدگی به خطا را تشکیل میدهند.
باید توجه داشته باشید که وقتی BookNotFoundException ما نشان داده میشود، اسپرینگ به طور خودکار یک کد وضعیت HTTP 500 را بر میگرداند. اگر چه برخی از APIها یک کد وضعیت ۵۰۰ یا سایر کدهای عمومی را بر میگردانند، همانطور که با APIهای فیسبوک و توییتر خواهیم دید، برای همه خطاها به خاطر سادگی، بهتر است در صورت امکان از خاصترین کد خطا استفاده کنید.
پاسخهای دقیقتر
همانطور که در مثال اسپرینگ بالا مشاهده شد، گاهی اوقات یک کد وضعیت برای نشان دادن مشخصات خطا کافی نیست. در صورت نیاز، میتوانیم از متن پاسخ برای ارائه اطلاعات اضافی به مشتری استفاده کنیم.
هنگام ارائه پاسخهای دقیق، باید شامل موارد زیر باشیم:
- خطا: یک شناسه منحصر به فرد برای خطا
- پیام: یک پیام کوتاه قابل خواندن برای انسان
- جزئیات: توضیح طولانیتر از خطا
به عنوان مثال، اگر مشتری درخواستی با اعتبارنامه درست ارسال کند، میتوانیم پاسخ ۴۰۱ را با این بدنه ارسال کنیم:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ |
{ "error": "auth-0001", "message": "Incorrect username and password", "detail": "Ensure that the username and password included in the request are correct" } |
فیلد خطا نباید با کد پاسخ مطابقت داشته باشد. در عوض، باید یک کد خطا منحصر به فرد برای برنامه ما باشد. به طور کلی، هیچ قراردادی برای فیلد خطا وجود ندارد، انتظار داشته باشید که منحصر به فرد باشد.
معمولا این فیلد فقط شامل حروف اعداد و کاراکترهای ارتباطی مانند خط تیره یا زیر خط است. برای مثال ۰۰۰۱، auth-0001 و رمز عبور نادرست کاربر، نمونههای متعارفی از کدهای خطا هستند.
بخش پیام بدنه معمولا در رابطهای کاربر قابل ارائه در نظر گرفته میشود. بنابراین اگر از بینالمللی شدن حمایت میکنیم، باید این عنوان را هم ترجمه کنیم. بنابراین اگر مشتری درخواستی را با هدر Accept-Language با زبان فرانسوی ارسال کند، مقدار عنوان هم باید به فرانسوی ترجمه شود.
بخش جزئیات برای استفاده توسعهدهندگان مشتریان و نه کاربر نهایی در نظر گرفته شده است، بنابراین ترجمه لازم نیست.
علاوهبراین، ما همچنین میتوانیم یک URL -مانند فیلد راهنما- ارائه کنیم که مشتریان میتوانند برای کشف اطلاعات بیشتر آن را دنبال کنند:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ |
{ "error": "auth-0001", "message": "Incorrect username and password", "detail": "Ensure that the username and password included in the request are correct", "help": "https://example.com/help/error/auth-0001" } |
گاهی اوقات، ممکن است بخواهیم بیش از یک خطا را برای یک درخواست گزارش کنیم.
در این مورد باید خطاها را در یک لیست برگردانیم:
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ ۱۴ ۱۵ ۱۶ ۱۷ ۱۸ ۱۹ ۲۰ ۲۱ |
{ "errors": [ { "error": "auth-0001", "message": "Incorrect username and password", "detail": "Ensure that the username and password included in the request are correct", "help": "https://example.com/help/error/auth-0001" }, ... ] } |
و هنگامی که یک خطا رخ میدهد، ما با یک لیست حاوی یک عنصر پاسخ میدهیم.
توجه داشته باشید که پاسخگویی با چندین خطا ممکن است برای برنامههای ساده بسیار پیچیده باشد. در بسیاری از موارد، پاسخ دادن با اولین یا مهمترین خطا کافی است.
در بدنههای پاسخگویی استاندارد در حالی که اکثر APIهای REST از قراردادهای مشابه پیروی میکنند، ویژگیها و مشخصات متفاوتی دارند، مانند نام فیلدها، اطلاعات موجود در بدنه پاسخ و … این تفاوتها رسیدگی یکنواخت خطاها را برای کتابخانهها و چارچوبها دشوار میکند.
در تلاشی برای استانداردسازی مدیریت خطای REST API، نهاد IETF ایده RFC 7807 را ابداع کرد که یک طرح کلی مدیریت خطا ایجاد میکند.
این طرح از پنج بخش تشکیل شده است:
نوع: یک شناسه URI که خطا را دستهبندی میکند.
عنوان: یک پیام کوتاه و قابل خواندن برای انسان در مورد خطا
وضعیت: کد پاسخ HTTP (اختیاری)
جزئیات: توضیحی قابل خواندن برای انسان از خطا
نمونه: یک URI که وقوع خاص خطا را شناسایی میکند.
به جای استفاده از بدنه پاسخ خطای سفارشی خود، میتوانیم بدنه خود را تبدیل کنیم.
۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹ ۱۰ ۱۱ ۱۲ ۱۳ |
{ "type": "/errors/incorrect-user-pass", "title": "Incorrect username or password.", "status": ۴۰۱, "detail": "Authentication failed due to incorrect username or password.", "instance": "/login/log/abc123" } |
توجه داشته باشید که فیلد type نوع خطا را دستهبندی میکند، در حالی که نمونه یک رخداد خاص از خطا را به ترتیب مشابه کلاسها و اشیا شناسایی میکند.
با استفاده از URIها، کلاینتها میتوانند این مسیرها را برای یافتن اطلاعات بیشتر در مورد خطا دنبال کنند، به همان روشی که پیوندهای HATEOAS میتوانند برای پیمایش یک REST API استفاده شوند.
پایبندی به RFC ۷۸۰۷ اختیاری است، اما اگر یکنواختی و همسانی برای شما مهم باشد، سودمند است.