الفصل 3: إدارة الأخطاء HTTP
مقدمة
يجب على API REST الاحترافية إدارة الأخطاء بطريقة واضحة ومتسقة، من خلال إعادة الأكواد الصحيحة لحالة HTTP ورسائل خطأ واضحة.
ResponseEntity
ما هو ResponseEntity؟
ResponseEntity هي فئة في Spring تسمح بالتحكم الكامل في استجابة HTTP:
- كود حالة HTTP
- رؤوس HTTP
- جسم الاستجابة
الصياغة الأساسية
ResponseEntity<TypeRetour> methodName() {
return ResponseEntity
.status(HttpStatus.OK)
.body(data);
}
الأكواد الرئيسية لحالة HTTP
جدول ملخص
| الكود | الاسم | الاستخدام | المثال |
|---|---|---|---|
| 200 | OK | نجاح عام | GET ناجح |
| 201 | Created | تم إنشاء المورد | POST ناجح |
| 204 | No Content | نجاح بدون محتوى | DELETE ناجح |
| 400 | Bad Request | طلب غير صالح | فشل التحقق |
| 404 | Not Found | المورد غير موجود | GET معرف غير موجود |
| 500 | Internal Server Error | خطأ في الخادم | استثناء غير متوقع |
أمثلة عملية مع ResponseEntity
200 OK - استرجاع ناجح
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// مكافئ لـ:
return ResponseEntity.status(HttpStatus.OK).body(product);
201 Created - إنشاء ناجح
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// أو مع URI:
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
204 No Content - حذف ناجح
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
404 Not Found - المورد غير موجود
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
try {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
} catch (ProductNotFoundException e) {
return ResponseEntity.notFound().build();
}
}
إدارة الاستثناءات العامة مع @ControllerAdvice
استثناء مخصص
package com.example.demo.exception;
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
مدير عام
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<Map<String, Object>> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.NOT_FOUND.value());
body.put("error", "Not Found");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGlobalException(
Exception ex, WebRequest request) {
Map<String, Object> body = new HashMap<>();
body.put("timestamp", LocalDateTime.now());
body.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value());
body.put("error", "Internal Server Error");
body.put("message", ex.getMessage());
body.put("path", request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
}
}
الاستخدام في الخدمة
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
}
استجابة JSON
{
"timestamp": "2025-11-28T10:30:45",
"status": 404,
"error": "Not Found",
"message": "Product not found with id: 123",
"path": "/api/products/123"
}
فئة ErrorResponse مخصصة
package com.example.demo.exception;
import java.time.LocalDateTime;
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String path;
public ErrorResponse(int status, String error, String message, String path) {
this.timestamp = LocalDateTime.now();
this.status = status;
this.error = error;
this.message = message;
this.path = path;
}
// Getters و setters
public LocalDateTime getTimestamp() {
return timestamp;
}
public void setTimestamp(LocalDateTime timestamp) {
this.timestamp = timestamp;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
}
الاستخدام
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"Not Found",
ex.getMessage(),
request.getDescription(false).replace("uri=", "")
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}
الهيكل الكامل: Controller + Service + Exception
1. المتحكم
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
2. الخدمة
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
public Product createProduct(Product product) {
if (product.getPrice() < 0) {
throw new InvalidProductException("السعر لا يمكن أن يكون سالبًا");
}
return productRepository.save(product);
}
public Product updateProduct(Long id, Product productDetails) {
Product product = getProductById(id);
product.setName(productDetails.getName());
product.setPrice(productDetails.getPrice());
product.setQuantity(productDetails.getQuantity());
return productRepository.save(product);
}
public void deleteProduct(Long id) {
Product product = getProductById(id);
productRepository.delete(product);
}
}
3. استثناءات مخصصة
public class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(Long id) {
super("Product not found with id: " + id);
}
}
public class InvalidProductException extends RuntimeException {
public InvalidProductException(String message) {
super(message);
}
}
4. مدير عام
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
public ResponseEntity<ErrorResponse> handleProductNotFound(
ProductNotFoundException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(404, "Not Found", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(InvalidProductException.class)
public ResponseEntity<ErrorResponse> handleInvalidProduct(
InvalidProductException ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(400, "Bad Request", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGlobalException(
Exception ex, WebRequest request) {
ErrorResponse error = new ErrorResponse(500, "Internal Server Error", ex.getMessage(),
request.getDescription(false).replace("uri=", ""));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
أفضل الممارسات
DO (ما يجب فعله)
✅ استخدام ResponseEntity لجميع الاستجابات
✅ إعادة الأكواد الصحيحة لـ HTTP
✅ إنشاء استثناءات مخصصة
✅ استخدام @ControllerAdvice للإدارة العامة
✅ توفير رسائل خطأ واضحة
DON'T (ما يجب تجنبه)
❌ إعادة 200 OK دائمًا
❌ كشف آثار الأخطاء التقنية للعميل
❌ إدارة الاستثناءات في كل متحكم
❌ استخدام استثناء عام
ملخص الفصل
النقاط الرئيسية
- ResponseEntity للتحكم في استجابات HTTP
- الأكواد المناسبة للحالة (200، 201، 204، 400، 404، 500)
- @ControllerAdvice للإدارة العامة للأخطاء
- استثناءات مخصصة لوضوح الكود
- ErrorResponse لهيكل متسق