iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 6
0
Modern Web

Nest.js framework 30天初探系列 第 6

Nestjs framework 30天初探:Day06 Exception Filters

Exception Filters

https://ithelp.ithome.com.tw/upload/images/20171208/20107195JpLYk0b6VB.png

在Nestjs世界裡,有一層Exception Layer,專門負責捕捉Exception,並適當的回應給Client。
預設情況下,所有未經定義的Exception(非HttpException或未繼承HttpException的class),都會被Global Exception Filter捕捉處理,並返回一段JSON訊息給Client。

{
    "statusCode": 500,
    "message": "Internal server error"
}
  1. 在UsersController新增getException(),來體驗一下HttpException,程式碼如下。
    src/modules/Users/users.controller.ts
import { Controller, Get, Post, Request, Response, Param, Next, HttpStatus, Body, HttpException } from '@nestjs/common';
import { CreateUserDTO } from './DTO/create-users.dto';
import { UsersService } from '../Users/Services/users.service';
import { ProductsService } from '../Products/Services/products.service';

@Controller()
export class UsersController {

    //依賴注入,建議要使用,這是低耦合作法
    //注入ProductsService
    constructor(private userService: UsersService, private productsService: ProductsService) { }

    @Get('users')
    //使用Express的參數
    async getAllUsers( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.userService.getAllUsers()
            .then((users) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(users);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Get('users/:id')
    //使用Express的參數
    //@Param('id')可以直接抓id參數
    async getUser( @Response() res, @Param('id') id) {
        //+id ,+符號可以直接把string 轉換成number
        await this.userService.getUser(+id)
            .then((user) => {
                res.status(HttpStatus.OK).json(user);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Post('users')
    async addUser( @Response() res, @Body() createUserDTO: CreateUserDTO) {
        //使用Rx.js,所以回傳可以做更多資料流的處理
        await this.userService.addUser(createUserDTO).subscribe((users) => {
            res.status(HttpStatus.OK).json(users);
        })
    }

    //測試ProductsService是否可以正常使用
    @Get('testProducts')
    //使用Express的參數
    async testGetAllProducts( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.productsService.getAllProducts()
            .then((products) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(products);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    //新增/getException使用HttpException
    @Get('getException')
    async getException(@Request() req, @Response() res, @Next() next){
        //直接拋錯,Nestjs有ExceptionsHandler 會主動捕捉錯誤。
        throw new Error('error');
    }
}
  1. 使用Postman,對http://localhost:3000/getException 進行GET請求,結果如下。
    console畫面
    https://ithelp.ithome.com.tw/upload/images/20171208/201071958s2sbwJP9m.png

說明:Nestjs有一層Exception layer,有一隻ExceptionsHandler會去執行捕捉錯誤。

https://ithelp.ithome.com.tw/upload/images/20171208/201071952bieHEYESP.png

說明:GET回傳結果為Nestjs預設的錯誤回應。

  1. 到目前為止,捕捉錯誤機制都還蠻OK的,但我想自己客製化訊息可以嗎? 當然可以,接下來修改UsersController,程式碼如下。
    src/modules/Users/users.controller.ts
import { Controller, Get, Post, Request, Response, Param, Next, HttpStatus, Body, HttpException } from '@nestjs/common';
import { CreateUserDTO } from './DTO/create-users.dto';
import { UsersService } from '../Users/Services/users.service';
import { ProductsService } from '../Products/Services/products.service';

@Controller()
export class UsersController {

    //依賴注入,建議要使用,這是低耦合作法
    //注入ProductsService
    constructor(private userService: UsersService, private productsService: ProductsService) { }

    @Get('users')
    //使用Express的參數
    async getAllUsers( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.userService.getAllUsers()
            .then((users) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(users);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Get('users/:id')
    //使用Express的參數
    //@Param('id')可以直接抓id參數
    async getUser( @Response() res, @Param('id') id) {
        //+id ,+符號可以直接把string 轉換成number
        await this.userService.getUser(+id)
            .then((user) => {
                res.status(HttpStatus.OK).json(user);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Post('users')
    async addUser( @Response() res, @Body() createUserDTO: CreateUserDTO) {
        //使用Rx.js,所以回傳可以做更多資料流的處理
        await this.userService.addUser(createUserDTO).subscribe((users) => {
            res.status(HttpStatus.OK).json(users);
        })
    }

    //測試ProductsService是否可以正常使用
    @Get('testProducts')
    //使用Express的參數
    async testGetAllProducts( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.productsService.getAllProducts()
            .then((products) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(products);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    //新增/getException使用HttpException
    @Get('getException')
    async getException(@Request() req, @Response() res, @Next() next){
        /*HttpException接受兩個參數,第一個是string或object,如果是object
        ,會override response第二個參數是HTTP status*/
        throw new HttpException('禁止訪問',HttpStatus.FORBIDDEN);
    }
}

說明:throw new HttpException(),使用nestjs的API,做更細緻的處理。

  1. 打開Postman,對http://localhost:3000/getException 做GET請求,結果如下。
    https://ithelp.ithome.com.tw/upload/images/20171208/20107195waJyHBauaW.png

說明:如此我們就客製化了回應的錯誤訊息和狀態碼。

  1. 不只可以客製化錯誤訊息和狀態碼,我們還可以寫自己的HttpException class。
    https://ithelp.ithome.com.tw/upload/images/20171208/2010719580ObZrMzSf.png
    切換到根目錄,下cmd指令。
cd src/modules/Shared & mkdir ExceptionFilters
  1. 在ExceptionFilters 資料夾新增forbidden.exception.ts 並寫以下程式碼。
    src/modules/Shared/ExceptionFilters
import { HttpException } from "@nestjs/core/exceptions/http-exception";
import { HttpStatus } from "@nestjs/common";

export class CustomForbiddenException extends HttpException {
    constructor() {
        super('禁止訪問', HttpStatus.FORBIDDEN);
    }
}

說明:ForbiddenException extends HttpException,所有特性都會跟HttpException一樣。

  1. 再改寫一下UsersController,改成使用ForbiddenException,程式碼如下。
    src/modules/Users/user.controller.ts
import { Controller, Get, Post, Request, Response, Param, Next, HttpStatus, Body, HttpException } from '@nestjs/common';
import { CreateUserDTO } from './DTO/create-users.dto';
import { UsersService } from '../Users/Services/users.service';
import { ProductsService } from '../Products/Services/products.service';
import { CustomForbiddenException } from '../Shared/ExceptionFilters/forbidden.exception';

@Controller()
export class UsersController {

    //依賴注入,建議要使用,這是低耦合作法
    //注入ProductsService
    constructor(private userService: UsersService, private productsService: ProductsService) { }

    @Get('users')
    //使用Express的參數
    async getAllUsers( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.userService.getAllUsers()
            .then((users) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(users);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Get('users/:id')
    //使用Express的參數
    //@Param('id')可以直接抓id參數
    async getUser( @Response() res, @Param('id') id) {
        //+id ,+符號可以直接把string 轉換成number
        await this.userService.getUser(+id)
            .then((user) => {
                res.status(HttpStatus.OK).json(user);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Post('users')
    async addUser( @Response() res, @Body() createUserDTO: CreateUserDTO) {
        //使用Rx.js,所以回傳可以做更多資料流的處理
        await this.userService.addUser(createUserDTO).subscribe((users) => {
            res.status(HttpStatus.OK).json(users);
        })
    }

    //測試ProductsService是否可以正常使用
    @Get('testProducts')
    //使用Express的參數
    async testGetAllProducts( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.productsService.getAllProducts()
            .then((products) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(products);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    //新增/getException使用HttpException
    @Get('getException')
    async getException(@Request() req, @Response() res, @Next() next){
        //使用CustomForbiddenException
        throw new CustomForbiddenException();
    }
}

說明:再使用Postmanhttp://localhost:3000/getException 做GET請求,結果會跟第4點一樣。

  1. 到目前為止我們都只是在做捕捉錯誤、回應錯誤訊息的動作,究竟Exception Filter要用在哪?答案是,捕捉到錯誤之後的處理動作就是Exception Filter所負責的。一般我們捕捉到錯誤,會寫log,甚至要能完全控制整個Exception layer做些動作,所以我們來寫一隻Exception Filter體驗一下它的魔力。

8.1 在src/modules/Shared/ExceptionFilters下新增http-exception.filter.ts ,並寫些程式碼如下。
src/modules/Shared/ExceptionFilters/http-exception.filter.ts

import { ExceptionFilter, Catch } from '@nestjs/common';
import { HttpException } from '@nestjs/core';

//記得使用Catch裝飾器,可以傳入無數個參數。
@Catch(HttpException)
//建議要implements ExceptionFilter,實作它的interface
export class HttpExceptionFilter implements ExceptionFilter {
    //傳入HttpException
    catch(exception: HttpException, response) {
        const status = exception.getStatus();
        const message = '我是Exception Log Message';

        //做log動作
        console.log(`exception status:`, status);
        console.log(`exception message:`, message);

        //調整response的json內容
        response.status(status).json({
            statusCode: status,
            message: message,
        });
    }
}

說明:@Catch()裝飾器會連接metadata到Exception Filter,告訴Nestjs,Filter會去尋找HttpException。

src/modules/Users/user.controller.ts

import { Controller, Get, Post, Request, Response, Param, Next, HttpStatus, Body, HttpException, UseFilters } from '@nestjs/common';
import { CreateUserDTO } from './DTO/create-users.dto';
import { UsersService } from '../Users/Services/users.service';
import { ProductsService } from '../Products/Services/products.service';
import { CustomForbiddenException } from '../Shared/ExceptionFilters/forbidden.exception';
import { HttpExceptionFilter } from '../Shared/ExceptionFilters/http-exception.filter';

@Controller()
export class UsersController {

    //依賴注入,建議要使用,這是低耦合作法
    //注入ProductsService
    constructor(private userService: UsersService, private productsService: ProductsService) { }

    @Get('users')
    //使用Express的參數
    async getAllUsers( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.userService.getAllUsers()
            .then((users) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(users);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Get('users/:id')
    //使用Express的參數
    //@Param('id')可以直接抓id參數
    async getUser( @Response() res, @Param('id') id) {
        //+id ,+符號可以直接把string 轉換成number
        await this.userService.getUser(+id)
            .then((user) => {
                res.status(HttpStatus.OK).json(user);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    @Post('users')
    async addUser( @Response() res, @Body() createUserDTO: CreateUserDTO) {
        //使用Rx.js,所以回傳可以做更多資料流的處理
        await this.userService.addUser(createUserDTO).subscribe((users) => {
            res.status(HttpStatus.OK).json(users);
        })
    }

    //測試ProductsService是否可以正常使用
    @Get('testProducts')
    //使用Express的參數
    async testGetAllProducts( @Request() req, @Response() res, @Next() next) {
        //Promise 有then catch方法可以調用
        await this.productsService.getAllProducts()
            .then((products) => {
                //多種Http的Status可以使用
                res.status(HttpStatus.OK).json(products);
            })
            .catch((error) => {
                console.error(error);
                res.status(HttpStatus.INTERNAL_SERVER_ERROR);
            })
    }

    //新增/getException使用HttpException
    @Get('getException')
    //使用HttpExceptionFilter
    @UseFilters(new HttpExceptionFilter())
    async getException(@Request() req, @Response() res, @Next() next){
        //使用CustomForbiddenException
        throw new CustomForbiddenException();
    }
}

說明:Controller呼叫一下@UseFilters()裝飾器,傳入HttpExceptionFilter,如此CustomForbiddenException的行為會被控制改寫。

  1. 完成上述動作,使用Postman,對http://localhost:3000/getException 進行GET請求,結果如下。
    9.1 console畫面如下
[Nest] 16148   - 2017-12-8 13:00:26   [NestFactory] Starting Nest application...
[Nest] 16148   - 2017-12-8 13:00:26   [InstanceLoader] ApplicationModule dependencies initialized +6ms
[Nest] 16148   - 2017-12-8 13:00:26   [InstanceLoader] UsersModule dependencies initialized +3ms
[Nest] 16148   - 2017-12-8 13:00:26   [InstanceLoader] ProductsModule dependencies initialized +1ms
[Nest] 16148   - 2017-12-8 13:00:26   [RoutesResolver] UsersController {/}: +54ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/users, GET} route +2ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/users/:id, GET} route +1ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/users, POST} route +1ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/testProducts, GET} route +1ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/getException, GET} route +2ms
[Nest] 16148   - 2017-12-8 13:00:26   [RoutesResolver] ProductsController {/products}: +3ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/, GET} route +3ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/:id, GET} route +12ms
[Nest] 16148   - 2017-12-8 13:00:26   [RouterExplorer] Mapped {/, POST} route +3ms
[Nest] 16148   - 2017-12-8 13:00:26   [NestApplication] Nest application successfully started +1ms
Application based on Express is listening on port 3000
來自根模組的參數
執行middleware...
我是簡單的Middleware
[Nest] 16148   - 2017-12-8 13:00:29   [HttpException] DEPRECATED! Since version [4.3.2] HttpException class was moved to the @nestjs/common package!
exception status: 403
exception message: 我是Exception Log Message

說明:console有打印log資訊,在這邊是以console.log()替代log操作。

9.2 Postman結果如下。
https://ithelp.ithome.com.tw/upload/images/20171208/20107195mOU82kOUrg.png

說明:json回應有被改寫,Exception Filter具有override功用。

  1. Exception Filter只能在HTTP 方法使用嗎? 當然不只,它可以在HTTP方法使用、Controller使用,甚至全域使用,程式碼如下。
    src/modules/Users/user.controller.ts
@Controller()
@UseFilters(new HttpExceptionFilter())
export class UsersController {...省略}

src/modules/app.module.ts

import { Module, RequestMethod, UseFilters } from '@nestjs/common';
import { UsersController } from './Users/users.controller';
import { UsersService } from './Users/Services/users.service';
import { UsersModule } from './Users/users.module';
import { LoggerMiddleware } from './Shared/Middlewares/logger.middleware';
import { SimpleMiddleware } from './Shared/Middlewares/simple.middleware';
import { NestModule, MiddlewaresConsumer } from '@nestjs/common/interfaces';
import { ProductsController } from './Products/products.controller';
import { HttpExceptionFilter } from './Shared/ExceptionFilters/http-exception.filter';

@Module({
  modules: [UsersModule]
})
//load HttpExceptionFilter
@UseFilters(new HttpExceptionFilter())
//NestModule本身是個Interface,建議要implements。
export class ApplicationModule implements NestModule {
  configure(consumer: MiddlewaresConsumer): void {
    //apply、forRoute方法允許傳入多個參數
    consumer.apply([LoggerMiddleware,SimpleMiddleware])
      //with方法可以傳入參數到middleware
      .with('來自根模組的參數')
      .forRoutes(
      //load進Controllers
      UsersController,
      ProductsController
      )
  }
}

大功告成,這樣對Exception Filter就有更多的了解,真是神好用的Filter。

程式碼在github


上一篇
Nestjs framework 30天初探:Day05 Middlewares
下一篇
Nestjs framework 30天初探:Day07 Pipes
系列文
Nest.js framework 30天初探30

尚未有邦友留言

立即登入留言