REST Web service: HTTP Status Code và xử lý ngoại lệ RESTful web service với Jersey 2.x

Xử lý ngoại lệ (exception handling) là một phần không thể thiếu trong bất kỳ ứng dụng nào, ứng dụng RESTful web service cũng vậy. Có 2 nguyên nhân dẫn đến ứng dụng REST throw ra một ngoại lệ là lỗi gì đó trên server hoặc client đã gửi yêu cầu không hợp lệ. Trong cả hai trường hợp, chúng ta cần gửi lại phản hồi các thông tin hữu ích cho phía Client hoặc developer biết thông tin để dễ dàng gỡ lỗi khi cần thiết.

Trong bài này, chúng ta sẽ cùng tìm hiểu HTTP Status Code và cách xử lý ngoại lệ RESTful web service với Jersey.

1. Giới thiệu HTTP Status Code

HTTP status code (mã trạng thái) là mã code server trả về sau mỗi lần gửi request. Tất cả các request mà server nhận được đều sẽ được trả về 1 response với 1 mã code tương ứng. Mã code này được dùng để mô tả trạng thái của quá trình server xử lý một yêu cầu (request) cho trước gửi từ client tới server dưới giao thức HTTP.

Các bạn có thể mở browser -> mở cửa sổ Firebug -> mở một trang bất kỳ -> mở tab Network -> chọn lên một resource bất kỳ, bạn sẽ thấy được thông tin status code như sau:

Như hình trên, Status code là 200, nghĩa là server đã nhận được request và xử lý thành công.

1.1. Phân loại HTTP Status Code

RFC 2616 định nghĩa chuẩn status code có thể được sử dụng để trả kết quả request về Client. HTTP status code gồm 3 chữ số, được chia thành 5 loại khác nhau:

  • 1xx – Information (Thông tin): Các status code loại này dùng để thông báo với client rằng server đã nhận được request. Các status code 1xx ít được sử dụng.
  • 2xx – Success (Thành công): Các status code loại này có ý nghĩa rằng request được server nhận, hiểu và xử lý thành công.
  • 3xx – Redirection (Chuyển hướng): Các status code loại này có ý nghĩa rằng server sẽ chuyển tiếp request hiện tại sang một request khác và client cần thực hiện việc gửi request tiếp theo đó để có thể hoàn tất. Thông thường khi trình duyệt nhận được status code loại này nó sẽ tự động thực hiện việc gửi request tiếp theo để lấy về kết quả.
  • 4xx – Client Error (Lỗi Client): Các status code loại này có ý nghĩa rằng đã có lỗi từ phía client trong khi gửi request. Ví dụ như sai URL, sai HTTP Method, không có quyền truy cập vào resource, …
  • 5xx – Server Error (Lỗi Server): Các status code loại này có ý nghĩa rằng đã có lỗi từ phía server trong khi xử lý request. Ví dụ như lỗi database, server bị hết bộ nhớ, …

1.2. Một số các HTTP status code thông dụng trong REST

2xx (Sucess – Thành công):

  • 200 – OK: Request đã được tiếp nhận và xử lý thành công. Status code này thường được trả về khi xử lý thành công cho HTTP method GET/ POST.
  • 201 – Created: Request đã được tiếp nhận và xử lý thành công và 1 tài nguyên mới được tạo trên server. Status code này thường được trả về khi xử lý thành công cho HTTP method PUT.
  • 204 – No content: Request được xử lý thành công nhưng server không trả về dữ liệu nào. Status code này thường được trả về khi xử lý thành công cho HTTP method DELETE.

4xx (Client Error – Lỗi Client):

  • 400 – Bad Request: Server không thể xử lý hoặc sẽ không xử lý các Request lỗi của phía client (ví dụ Request có cú pháp sai)
  • 401 – Unauthorized: Cần chứng thực để truy cập.
  • 402 – Payment Required: Mã này chưa được định nghĩa.
  • 403 – Forbidden: Truy cập bị từ chối (ví dụ ip bị chặn)
  • 404 – Not Found: Trang được yêu cầu không tồn tại tại thời điểm hiện tại, tuy nhiên có thể tồn tại trong tương lai.
  • 405 – Method Not Allowed: Trang được yêu cầu không hỗ trợ method của request (ví dụ chỉ xử lý method POST, không xử lý method GET)
  • 406 – Not Acceptable: Server chỉ có thể tạo một Response mà không được chấp nhận bởi Client.
  • 407 – Proxy Authentication Required: Bạn phải xác nhận với một proxy server trước khi request này được phục vụ.
  • 408 – Request Timeout: Request tốn thời gian dài hơn thời gian Server được chuẩn bị để đợi.
  • 409 – Conflict: Request không thể được hoàn thành bởi vì sự xung đột
  • 410 – Gone: Giống 404 nhưng tài nguyên/trang cũng không tồn tại trong tương lai
  • 411 – Length Required: Chưa định nghĩa trường “Content-Length” trong header của request gửi đi.
  • 412 – Precondition Failed: Server sẽ không đáp ứng một trong những điều kiện tiên quyết của Client trong Request.
  • 413 – Payload Too Large: Server sẽ không chấp nhận yêu cầu, bởi vì đối tượng yêu cầu là quá rộng.
  • 414 – URI Too Long: URI được cung cấp là quá dài để Server xử lý.
  • 415 – Unsupported Media Type: Server sẽ không chấp nhận Request, bởi vì kiểu phương tiện không được hỗ trợ.

5xx (Server error – Lỗi server) :

  • 500 – Internal Server Error: Một thông báo chung chung, được đưa ra khi Server bị lỗi bất ngờ (chủ yếu do lỗi lập trình, kết nối database)
  • 501 – Not Implemented: Server không hỗ trợ xử lý request này.
  • 502 – Bad Gateway: Server đã hoạt động như một gateway hoặc proxy và nhận được một Response không hợp lệ từ máy chủ nguồn.
  • 503 – Service Unavailable: Server hiện tại không có sẵn (Quá tải hoặc được down để bảo trì). Nói chung đây chỉ là trạng thái tạm thời.
  • 504 – Gateway Timeout: Server đã hoạt động như một gateway hoặc proxy và không nhận được một Response từ máy chủ nguồn.
  • 505 – HTTP Version Not Supported: Server không hỗ trợ phiên bản “giao thức HTTP”.

Các bạn có thể tham khảo thêm các HTTP status code khác theo link sau:

2. Xử lý ngoại lệ RESTful web service với Jersey

Các ngoại lệ được throw trong ứng dụng có thể được xử lý bởi Jersey tại thời điểm run-time nếu nó được đăng ký một exception mapper. Exception mapper dùng để chuyển đổi một exception sang một HTTP Response.

JAX-RS cũng cung cấp một javax.ws.rs.WebApplicationException. Nó có thể throw một status code và sẽ được xử lý tự động bởi JAX-RS mà không cần đăng ký một exception mapper.

Ví dụ:

2.1. Tạo Jersey project

Chúng ta sẽ sử dụng lại project ở bài viết: Upload và Download file trong Jersey 2.x.

2.2. Tạo các custom exception và mapper

Chúng ta có thể sử dụng trực tiếp WebApplicationException với Status code. Trong ví dụ này, chúng ta sẽ tạo thêm một custom exception cho Status.NOT_FOUND với WebApplicationException.

CustomNotFoundWithWebApplicationException.java

package com.maixuanviet.exception;
 
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
 
public class CustomNotFoundWithWebApplicationException extends WebApplicationException {
 
    private static final long serialVersionUID = 3861196832088411918L;
 
    /**
     * Create a HTTP 404 (Not Found) exception.
     */
    public CustomNotFoundWithWebApplicationException() {
        super(Status.NOT_FOUND);
    }
 
    /**
     * Create a HTTP 404 (Not Found) exception.
     * 
     * @param message the String that is the entity of the 404 response.
     */
    public CustomNotFoundWithWebApplicationException(String message) {
        super(Response.status(Status.NOT_FOUND).entity(message).type("text/plain").build());
    }
}

Tạo một custom exception RecordNotFoundException được sử dụng khi một record không được tìm thấy từ database:

RecordNotFoundException.java

package com.maixuanviet.exception;
 
public class RecordNotFoundException extends RuntimeException {
 
    private static final long serialVersionUID = 3271661431899334345L;
 
    public RecordNotFoundException(String errorMessage) {
        super(errorMessage);
    }
}

Tạo model class để trả kết quả về Client:

ErrorResponse.java

package com.maixuanviet.model;
 
import javax.xml.bind.annotation.XmlRootElement;
 
import lombok.Data;
 
@XmlRootElement
@Data
public class ErrorResponse {
 
    private Integer status;
    private String errorMessage;
}

Tạo một ExceptionMapper để xử lý Response khi RecordNotFoundException được throw:

RecordNotFoundExceptionMapper.java

package com.maixuanviet.exception.mapper;
 
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
 
import com.maixuanviet.exception.RecordNotFoundException;
import com.maixuanviet.model.ErrorResponse;
 
@Provider
public class RecordNotFoundExceptionMapper implements ExceptionMapper<RecordNotFoundException> {
 
    @Override
    public Response toResponse(RecordNotFoundException ex) {
        ErrorResponse response = new ErrorResponse();
        response.setStatus(Status.NOT_FOUND.getStatusCode()); // 404, "Not Found"
        response.setErrorMessage(ex.getMessage());
 
        return Response.status(response.getStatus())
                .entity(response)
                .type(MediaType.APPLICATION_JSON) // "application/json"
                .build();
    }
}

Tạo một ExceptionMapper để xử lý Response khi bất kỳ ngoại lệ nào được throw nhưng chưa được xử lý:

GenericExceptionMapper.java

package com.maixuanviet.exception.mapper;
 
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
 
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
 
    @Override
    public Response toResponse(Throwable ex) {
        return Response.status(getStatusType(ex)) 
                .entity(ex.getMessage())
                .type(MediaType.TEXT_PLAIN) // "text/plain"
                .build();
    }
     
    private Response.StatusType getStatusType(Throwable ex) {
        if (ex instanceof WebApplicationException) {
            return((WebApplicationException)ex).getResponse().getStatusInfo();
        } else {
            // 500, "Internal Server Error"
            return Response.Status.INTERNAL_SERVER_ERROR;
        }
    }
}

2.3. Tạo ứng dụng REST web service

UserCrudWithExceptionHandlingService.java

package com.maixuanviet.api;
 
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
 
import com.maixuanviet.exception.CustomNotFoundWithWebApplicationException;
import com.maixuanviet.exception.RecordNotFoundException;
import com.maixuanviet.model.User;
 
//URI:
//http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
//http://localhost:8080/RestfulWebServiceExample/rest/v2/users
@Path("/v2/users")
public class UserCrudWithExceptionHandlingService {
 
    @GET
    @Path("/download/{type}")
    public Response downloadFile(@PathParam("type") String fileType) {
        if (!fileType.equalsIgnoreCase("docx") && !fileType.equalsIgnoreCase("jpg")) {
            throw new WebApplicationException(Status.PRECONDITION_FAILED);
        }
        return Response.ok("File downloaded").build();
    }
 
    @GET
    @Path("/download-v2/{type}")
    public Response downloadFileV2(@PathParam("type") String fileType) {
        if (!fileType.equalsIgnoreCase("docx") && !fileType.equalsIgnoreCase("jpg")) {
            throw new CustomNotFoundWithWebApplicationException(
                    "Our system is only supported for the following file type: docx, jpg");
        }
        return Response.ok("File downloaded").build();
    }
 
    @GET
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    public Response getUserById(@PathParam("id") int id) {
        if (id < 0) {
            throw new RecordNotFoundException("Cannot find user with the given id: " + id);
        }
        User user = new User(1, "maixuanviet");
        return Response.ok(user).build();
    }
 
    @DELETE
    @Path("/{id}")
    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
    public boolean deleteUserById(@PathParam("id") int id) {
        throw new UnsupportedOperationException("The delete action is not ready yet!");
    }
}
&#91;/code&#93;
<!-- /wp:shortcode -->

<!-- wp:paragraph -->
<p>Trong ví dụ trên, tôi cung throw các loại exception khác nhau:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Phương thức downloadFile() throw new WebApplicationException(Status.PRECONDITION_FAILED) : khi client gọi web service này với type khác docx và jpg sẽ nhận được một status code 412. Sử dụng&nbsp;<strong>exception handling mặc định</strong>&nbsp;<strong>WebApplicationException</strong>.</li><li>Phương thức downloadFileV2() throw new CustomNotFoundWithWebApplicationException() : khi client gọi web service này với type khác docx và jpg sẽ nhận được một status code 404. Đây là một&nbsp;<strong>custom exception của WebApplicationException</strong>.</li><li>Phương thức getUserById() throw new RecordNotFoundException() : phương thức này sẽ được xử lý bởi&nbsp;<strong>RecordNotFoundExceptionMapper</strong>.</li><li>Phương thức deleteUserById() throw new UnsupportedOperationException() : bất kỳ exception nào không được xử lý bởi Mapper hoặc ExceptionMapper cu thể thì sẽ được xử lý bởi&nbsp;<strong>GenericExceptionMapper</strong>.</li></ul>
<!-- /wp:list -->

<!-- wp:paragraph -->
<p>Cần chỉnh sửa lại một chút ở file config để Jersey có thể scan các Exception Handler:</p>
<!-- /wp:paragraph -->

<!-- wp:shortcode -->

package com.maixuanviet.config;
 
import java.util.logging.Level;
import java.util.logging.Logger;
 
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.logging.LoggingFeature;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
//Deployment of a JAX-RS application using @ApplicationPath with Servlet 3.0
//Descriptor-less deployment
import org.glassfish.jersey.server.ResourceConfig;
 
public class JerseyServletContainerConfig extends ResourceConfig {
    public JerseyServletContainerConfig() {
        // if there are more than two packages then separate them with semicolon
        packages("com.maixuanviet.api, com.maixuanviet.exception");
        register(new LoggingFeature(Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), Level.INFO,
                LoggingFeature.Verbosity.PAYLOAD_TEXT, 10000));
        register(JacksonFeature.class); 
        register(MultiPartFeature.class);   
    }
}

Lưu ý: các bạn có thể sửa lại packages(“com.gpcoder”) -> có nghĩa là sẽ scan toàn bộ package của ứng dụng.

2.4. Test ứng dụng

Các bạn hãy chạy thử các trường hợp trên để xem kết quả:

Test @GET http://localhost:8080/RestfulWebServiceExample/rest/v2/users/download/mp3

Test @GET http://localhost:8080/RestfulWebServiceExample/rest/v2/users/download-v2/mp3

Test @GET http://localhost:8080/RestfulWebServiceExample/rest/v2/users/-1

Test @DELETE http://localhost:8080/RestfulWebServiceExample/rest/v2/users/-1