بناء واجهات REST مع Spring Boot

رموز حالة HTTP ودلالات REST

18 دقيقة الدرس 6 من 13

رموز حالة HTTP ودلالات REST

إعادة رمز حالة HTTP الصحيح ليست مسألة جمالية — بل هي آلية الإشارة الأساسية التي تمتلكها واجهة برمجية (API) مع كل عميل وخادم وكيل وذاكرة تخزين مؤقت وأداة مراقبة في السلسلة. يُخبر الرمز المختار بدقة المُستدعيَ ما إذا كان عليه إعادة المحاولة، أو تحليل الجسم، أو إعادة التوجيه، أو تسجيل تنبيه، أو لا يفعل شيئًا. الخطأ في اختيار الرموز يُقوّض الثقة في واجهتك البرمجية ويجبر العملاء على كتابة حلول وقائية. يربط هذا الدرس عائلات الحالة القياسية بعمليات REST الحقيقية ويُظهر كيف يُتيح لك Spring Boot التحكم فيها بدقة.

العائلات الخمس لرموز الحالة

تنقسم رموز حالة HTTP إلى خمس عائلات، لكل منها معنى مشترك:

  • 1xx — معلوماتية: الطلب قيد المعالجة. نادرة في REST APIs؛ نادرًا ما ستُرسلها بنفسك.
  • 2xx — النجاح: اكتملت العملية على النحو المطلوب. هنا تُركّز معظم جهد التصميم.
  • 3xx — إعادة التوجيه: يجب على العميل اتخاذ إجراء آخر، عادةً اتباع عنوان URL جديد.
  • 4xx — خطأ العميل: الطلب خاطئ. يجب على العميل تصحيحه قبل إعادة المحاولة.
  • 5xx — خطأ الخادم: فشل الخادم. يجوز للعميل إعادة المحاولة لاحقًا.

عائلة 2xx — مطابقة الرمز للعملية

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

  • 200 OK — نجحت عملية قراءة أو استبدال كامل وجسم الاستجابة يحمل النتيجة. استخدمه لـ GET وPUT (عند إعادة المورد المُحدَّث) وأفعال POST التي ليست إنشاء موارد.
  • 201 Created — تم إنشاء مورد جديد. استخدمه لـ POST (وأحيانًا PUT). يجب أن تتضمن الاستجابة ترويسة Location تُشير إلى عنوان URL للمورد الجديد.
  • 204 No Content — نجحت العملية لكن لا يوجد ما يُعاد. استخدمه لـ DELETE ولـ PUT/PATCH حين لا تحتاج إلى إعادة التمثيل المُحدَّث.
  • 202 Accepted — قُبل الطلب للمعالجة غير المتزامنة لكنه لم يكتمل بعد. يحتوي الجسم عادةً على معرّف مهمة أو وظيفة يمكن للعميل استطلاعه.
لماذا يهم 201 + Location: يمكن للعملاء الذين يتبعون الاستجابة التنقل فورًا إلى المورد المُنشأ حديثًا دون استعلام ثانٍ. تستخدم الأطر وبوابات API هذه الترويسة أيضًا لربط الموارد ذات الصلة تلقائيًا.

إعادة 201 وLocation في Spring Boot

استخدم ResponseEntity مع UriComponentsBuilder أو ServletUriComponentsBuilder لبناء عنوان URI للموقع بشكل نظيف:

import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; @RestController @RequestMapping("/api/v1/products") public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } @PostMapping public ResponseEntity<Product> create(@RequestBody @Valid ProductRequest req) { Product saved = productService.create(req); URI location = ServletUriComponentsBuilder .fromCurrentRequest() // الأساس = /api/v1/products .path("/{id}") .buildAndExpand(saved.getId()) .toUri(); // /api/v1/products/42 return ResponseEntity.created(location).body(saved); // 201 + ترويسة Location + الجسم } }

يُعدّ ResponseEntity.created(uri) دالة مصنع تضبط الحالة على 201 وترويسة Location في استدعاء واحد. إضافة .body(saved) اختيارية — يُغفلها بعض الفرق للإبقاء على الاستجابة موجزة.

إعادة 204 لعملية DELETE

@DeleteMapping("/{id}") public ResponseEntity<Void> delete(@PathVariable Long id) { productService.delete(id); return ResponseEntity.noContent().build(); // 204، لا جسم }

استخدام Void كنوع عام يوضح صراحةً أنه لا يُتوقع أي جسم. ResponseEntity.noContent().build() هي الدالة المصنع المعيارية.

عائلة 4xx — أخطاء العميل

تُخبر هذه الرموز العميل بأنه يجب تصحيح الطلب. لا تدمجها كلها في 400:

  • 400 Bad Request — جسم الطلب أو المعاملات غير صالحة تركيبيًا أو دلاليًا. استخدمه عند فشل التحقق من الصحة.
  • 401 Unauthorized — العميل غير مُوثَّق. الاسم مُضلّل؛ معناه الحقيقي "غير مُعرَّف الهوية". يضبط Spring Security هذا تلقائيًا للنقاط المحمية.
  • 403 Forbidden — العميل مُوثَّق لكن ليس لديه إذن لهذه العملية.
  • 404 Not Found — المورد المطلوب غير موجود. لا تستخدم 200 مع جسم فارغ عندما يكون السجل مفقودًا.
  • 405 Method Not Allowed — طريقة HTTP غير مدعومة لهذا المسار (يضبطه Spring تلقائيًا).
  • 409 Conflict — لا يمكن إتمام العملية بسبب تعارض مع الحالة الحالية للمورد، مثل بريد إلكتروني مكرر أثناء التسجيل.
  • 422 Unprocessable Entity — التركيب صحيح لكن قواعد العمل ترفض البيانات. تُفضّل كثير من الفرق هذا على 400 لأخطاء التحقق للتمييز بين فشل التحليل وفشل المنطق.
  • 429 Too Many Requests — تجاوز العميل حد معدل الطلبات.
استخدم 404 باتساق للموارد المفقودة. يمكن أن يكشف إرجاع رموز مختلفة بحسب ما إذا كان السطر مفقودًا أو يفتقر المستخدم إلى صلاحية الوصول عن المعرّفات الموجودة. بالنسبة للموارد الحساسة أمنيًا، يُعدّ إعادة 404 سواء كان السجل مفقودًا أو غير مُتاح خيارًا تصميميًا متعمدًا يمنع هجمات التعداد.

رمي الاستثناءات التي تُعيَّن إلى رموز الحالة

إزعاج كل دالة في المتحكم بقرارات رمز الحالة أمر مُزعج. النمط الأنظف هو رمي استثناء مخصص للنطاق والسماح لـ @ControllerAdvice المركزي بتعيينه:

// استثناء النطاق public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } } // في الخدمة أو المتحكم public Product findById(Long id) { return repository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found: " + id)); }
import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ProblemDetail handleNotFound(ResourceNotFoundException ex) { ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); pd.setTitle("Resource Not Found"); return pd; // يُسلسله Spring مع الحالة 404 } @ExceptionHandler(ConflictException.class) public ProblemDetail handleConflict(ConflictException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage()); } }

يُنفّذ ProblemDetail (المُقدَّم في Spring 6 / Spring Boot 3) معيار RFC 9457 (المعروف سابقًا بـ RFC 7807)، وهو صيغة تفاصيل المشكلة القياسية. يتلقى العملاء جسم JSON يحتوي على حقول type وtitle وstatus وdetail يمكنهم معالجتها برمجيًا.

استخدام @ResponseStatus على فئات الاستثناء

في الحالات البسيطة يمكنك تعليق الاستثناء مباشرةً بدلًا من كتابة @ExceptionHandler:

import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } }

سيضبط Spring رمز الحالة تلقائيًا عند انتشار هذا الاستثناء من المتحكم. المقايضة: لا يمكنك تخصيص جسم الاستجابة بهذه الطريقة، لذا فضّل @ExceptionHandler + ProblemDetail لأي واجهة برمجية في الإنتاج يحتاج عملاؤها إلى تفاصيل خطأ منظّمة.

عائلة 5xx — متى تستخدم 500 مقابل 503

تجنّب كتابة كود يرمي 500 عمدًا. دع الاستثناءات غير المعالجة تتدفق إلى معالج Spring الافتراضي الذي يُعيد 500. استخدم 503 Service Unavailable عندما يكون اعتماد خارجي (قاعدة بيانات، خدمة خارجية) متوقفًا مؤقتًا وتريد الإشارة إلى أن على العميل إعادة المحاولة — مع تضمين ترويسة Retry-After اختياريًا. تأتي 502 Bad Gateway و504 Gateway Timeout من طبقات البنية التحتية (الوكلاء العكسيون وموازنات الحمل)، وليس من كود تطبيقك.

لا تُعد 200 مطلقًا مع خطأ في الجسم. الأنماط مثل {"success": false, "error": "not found"} ملفوفة في استجابة 200 تُعطّل كل أداة مدركة لـ HTTP: ذاكرات التخزين المؤقت تحفظ الخطأ، والمراقبة تُصنّفه نجاحًا، ومنطق إعادة المحاولة يتجاهله. استخدم رمز الحالة الصحيح لتعمل طبقة البروتوكول لصالحك.

دلالات REST: القدرة على الاستيعاب والسلامة

لا تحكي رموز الحالة القصة كاملة. تُشكّل خاصيتان من HTTP الرموز المناسبة:

  • الأساليب الآمنة (GET، HEAD) يجب ألا تغيّر حالة الخادم. لا ينبغي أن تُعيد أبدًا 201 أو 204.
  • الأساليب القابلة للاستيعاب (Idempotent) (GET، PUT، DELETE، HEAD، OPTIONS) تُنتج النتيجة ذاتها بغض النظر عن عدد مرات الاستدعاء. يجب أن تُعيد عملية DELETE على مورد لم يعد موجودًا 404 في المرة الأولى و404 مجددًا عند التكرار — وليس رمزًا مختلفًا أو خطأ حول "تم الحذف بالفعل".
  • POST ليست آمنة ولا قابلة للاستيعاب. استدعاء POST /products مرتين يجب أن ينشئ منتجَين، ويُعيد الاستدعاء الثاني 201 مجددًا (أو 409 إذا كانت التكرارات محظورة).

جدول مرجعي سريع

  • GET /resources200 OK (قائمة)، 404 مستحيلة (القائمة فارغة = 200 + [])
  • GET /resources/{id}200 OK أو 404 Not Found
  • POST /resources201 Created + Location، أو 400/409 عند الخطأ
  • PUT /resources/{id}200 OK (مع جسم) أو 204 No Content (بدون جسم)؛ 404 إذا كان مفقودًا
  • PATCH /resources/{id}200 OK أو 204 No Content؛ 404 أو 422 عند الخطأ
  • DELETE /resources/{id}204 No Content؛ 404 إذا كان مفقودًا

الخلاصة

اختيار رمز الحالة الصحيح فعل تصميمي متعمد. أعد 201 مع ترويسة Location عند الإنشاء، و204 عند الحذف أو التحديث دون إعادة الجسم، و404 عند غياب المورد، والرمز المناسب من عائلة 4xx عندما يُرسل العميل بيانات خاطئة. مركز قرارات رموز الحالة في @RestControllerAdvice باستخدام ProblemDetail، ولا تسيء أبدًا استخدام 200 لإخفاء الأخطاء. بهذه العادات تتواصل واجهتك البرمجية بدقة عبر HTTP باللغة ذاتها التي يتحدثها كل وكيل وعميل وأداة مراقبة.