انتقل إلى المحتوى الرئيسي

الفصل 3: إدارة الأخطاء HTTP

مقدمة​

يجب على API REST الاحترافية إدارة الأخطاء بطريقة واضحة ومتسقة، من خلال إعادة الأكواد الصحيحة لحالة HTTP ورسائل خطأ واضحة.


ResponseEntity​

ما هو ResponseEntity؟​

ResponseEntity هي فئة في Spring تسمح بالتحكم الكامل في استجابة HTTP:

  • كود حالة HTTP
  • رؤوس HTTP
  • جسم الاستجابة

الصياغة الأساسية​

ResponseEntity<TypeRetour> methodName() {
return ResponseEntity
.status(HttpStatus.OK)
.body(data);
}

الأكواد الرئيسية لحالة HTTP​

جدول ملخص​

الكودالاسمالاستخدامالمثال
200OKنجاح عامGET ناجح
201Createdتم إنشاء الموردPOST ناجح
204No Contentنجاح بدون محتوىDELETE ناجح
400Bad Requestطلب غير صالحفشل التحقق
404Not Foundالمورد غير موجودGET معرف غير موجود
500Internal 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 لهيكل متسق

الفصل التالي: بنية 3 طبقات

العودة إلى الوحدة 3