💖💖💖HTTP、脚本化HTTP 与 nestjs 异常处理

1,196 阅读6分钟

合抱之木,生于毫末; 九层之台,起于垒土; 千里之行,始于足下。 --- 《道德经》 第六十四章。

HTTP 简易介绍

http 对于一个前端是软实力,其实如果你想往后端靠近,扎实的 HTTP 基础会很好的帮助你。

  1. 特点简介
  2. 消息结构
  3. 请求方法
  4. 响应状态
  5. 状态码
  6. content-type 内容类型

全名

  • HyperText Transfer Protocol
  • 超文本传输协议

基于

  • TCP/IP

传输数据类型

  • html
  • 文件
  • 查询结果

特点

  1. 无状态
  2. 媒体独立(只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送)
  3. 无连接,一次链接之处理一个请求。也能实现长链接

消息结构

客户端: 请求消息结构

请求行(request line)、请求头部(header)、空行和请求数据四个部分组成

  • 请求行
  • 请求体
  • 空行
  • 请求数据

服务端:响应应消息结果

状态行、消息报头、空行和响应正文。

  • 状态行
  • 消息报头
  • 空行
  • 响应正文

请求方法

HTTP1.0 包含以下三种

  • GET --- 请求指定的页面信息,并返回实体主体。
  • POST --- 一般用于创新新的数据、修改已有的数据
  • HEAD --- 用于获取报头

HTTP1.1 新增六种

  • OPTIONS --- 允许客户端查看服务器的性能。
  • PUT --- 从客户端向服务器传送的数据取代指定的文档的内容。
  • PATCH --- 是对 PUT 方法的补充,用来对已知资源进行局部更新 。
  • DELETE --- 请求服务器删除指定的页面。
  • TRACE --- 回显服务器收到的请求,主要用于测试或诊断。
  • CONNECT --- HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。

所以在 1.1 以及之前,包含 9 中方法;

响应头

  • Allow
  • Content-Encoding
  • Content-Length
  • Content-Type
  • Date
  • Expires
  • Last-Modified
  • Location
  • Refresh
  • Server
  • Set-Cookie
  • WWW-Authenticate

状态码

对于状态码,我们时常不能记住他们具体的用处,有些具有多年工作经验的从业者,也不见得能记住状态码。

下面是最常用的状态码:成功、失败(失败的种类比较多了,所以可以理解为,整个状态码,就是对于 失败的识别,然后然用户知道,发生了什么失败!)

  • 200 - 请求成功
  • 301 - 资源(网页等)被永久转移到其它URL
  • 404 - 请求的资源(网页等)不存在
  • 500 - 内部服务器错误

分类

HTTP状态码分类

  • 1 --- 信息,服务器收到请求,需要请求者继续执行操作
  • 2 --- 成功,操作被成功接收并处理
  • 3 --- 重定向,需要进一步的操作以完成请求
  • 4 --- 客户端错误,请求包含语法错误或无法完成请求
  • 5 --- 服务器错误,服务器在处理请求的过程中发生了错误

脚本化 HTTP

浏览器在 XMLHttpRequest 对象上定义了 HTTP 的API。浏览器中通过 XMLHttpRequest 完成 HTTP 的请求与响应工作。

使用 XMLHTTPRequest 创建一个请求实例

const request = new XMLHttpRequest();

其实 XMLHTTPRequest 是有兼容性问题的。在早起的 IE 浏览器宏是并不支持这个对象的。而是用的 ActiveXObject 对象创建。所以,为了兼容会自己封装一个具有兼容性的 XMLHttpRequest 对象,方便使用。

请求流程

  • open 打开一个请求
  • 设置请求头
  • send 发送一个请求

监听请求状态

request.onReadyStateChange = function() {}

响应

  • 获取响应内容
    • state
    • readyState
    • responseText
    • getResponseHeader
    • responseXML

nestjs 中 http 状态码

nestjs 使用枚举类型 HttpStatus 定义了 http 状态码

export enum HttpStatus {
  CONTINUE = 100,
  SWITCHING_PROTOCOLS = 101,
  PROCESSING = 102,
  OK = 200,
  CREATED = 201,
  ACCEPTED = 202,
  NON_AUTHORITATIVE_INFORMATION = 203,
  NO_CONTENT = 204,
  RESET_CONTENT = 205,
  PARTIAL_CONTENT = 206,
  AMBIGUOUS = 300,
  MOVED_PERMANENTLY = 301,
  FOUND = 302,
  SEE_OTHER = 303,
  NOT_MODIFIED = 304,
  TEMPORARY_REDIRECT = 307,
  PERMANENT_REDIRECT = 308,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401,
  PAYMENT_REQUIRED = 402,
  FORBIDDEN = 403,
  NOT_FOUND = 404,
  METHOD_NOT_ALLOWED = 405,
  NOT_ACCEPTABLE = 406,
  PROXY_AUTHENTICATION_REQUIRED = 407,
  REQUEST_TIMEOUT = 408,
  CONFLICT = 409,
  GONE = 410,
  LENGTH_REQUIRED = 411,
  PRECONDITION_FAILED = 412,
  PAYLOAD_TOO_LARGE = 413,
  URI_TOO_LONG = 414,
  UNSUPPORTED_MEDIA_TYPE = 415,
  REQUESTED_RANGE_NOT_SATISFIABLE = 416,
  EXPECTATION_FAILED = 417,
  I_AM_A_TEAPOT = 418,
  UNPROCESSABLE_ENTITY = 422,
  FAILED_DEPENDENCY = 424,
  TOO_MANY_REQUESTS = 429,
  INTERNAL_SERVER_ERROR = 500,
  NOT_IMPLEMENTED = 501,
  BAD_GATEWAY = 502,
  SERVICE_UNAVAILABLE = 503,
  GATEWAY_TIMEOUT = 504,
  HTTP_VERSION_NOT_SUPPORTED = 505,
}

http 状态码广泛用于异常

总结

面试的过程中,关于 http 状态码,记忆模糊,其实 http 也是有规律的,不同的开头的状态码,被分到了不同的类型中。

错误类接口和错误

// 错误接口
interface Error {
    name: string;
    message: string;
    stack?: string;
}

// 错误构造器接口
interface ErrorConstructor {
    new(message?: string): Error;
    (message?: string): Error;
    readonly prototype: Error;
}
// 错误类
declare var Error: ErrorConstructor;

// Http 异常类
export class HttpException extends Error {
  public readonly message: any;
 
  constructor(
    private readonly response: string | object,
    private readonly status: number,
  ) {
    super();
    this.message = response;
  }
	
  // 获取响应对象
  public getResponse(): string | object {
    return this.response;
  }

// 获取状态码
  public getStatus(): number {
    return this.status;
  }
// 转变成字符串
  public toString(): string {
    const message = this.getErrorString(this.message);
    return `Error: ${message}`;
  }
// 获取错误字符串
  private getErrorString(target: string | object): string {
    return isString(target) ? target : JSON.stringify(target);
  }
	// 静态方法创建 body
  public static createBody(
    message: object | string,
    error?: string,
    statusCode?: number,
  ) {
    if (!message) {
      return { statusCode, error };
    }
    return isObject(message) && !Array.isArray(message)
      ? message
      : { statusCode, error, message };
  }
}

// HttpStatus.BAD_REQUEST 是 400
export class BadRequestException extends HttpException {
  constructor(message?: string | object | any, error = 'Bad Request') {
    super(
      HttpException.createBody(message, error, HttpStatus.BAD_REQUEST),
      HttpStatus.BAD_REQUEST,
    );
  }
}

 // ConflictException 是 409
 export class ConflictException extends HttpException {
 constructor(message?: string | object | any, error = 'Conflict') {
    super(
      HttpException.createBody(message, error, HttpStatus.CONFLICT),
      HttpStatus.CONFLICT,
    );
  }
}

// ForbiddenException 是 403
export class ForbiddenException extends HttpException {
  constructor(message?: string | object | any, error = 'Forbidden') {
    super(
      HttpException.createBody(message, error, HttpStatus.FORBIDDEN),
      HttpStatus.FORBIDDEN,
    );
  }
}

// GatewayTimeoutException 504
export class GatewayTimeoutException extends HttpException {
  constructor(message?: string | object | any, error = 'Gateway Timeout') {
    super(
      HttpException.createBody(message, error, HttpStatus.GATEWAY_TIMEOUT),
      HttpStatus.GATEWAY_TIMEOUT,
    );
  }
}
// GoneException 401
export class GoneException extends HttpException {
  constructor(message?: string | object | any, error = 'Gone') {
    super(
      HttpException.createBody(message, error, HttpStatus.GONE),
      HttpStatus.GONE,
    );
  }
}

// 505
export class HttpVersionNotSupportedException extends HttpException {
  constructor(
    message?: string | object | any,
    error = 'HTTP Version Not Supported',
  ) {
    super(
      HttpException.createBody(
        message,
        error,
        HttpStatus.HTTP_VERSION_NOT_SUPPORTED,
      ),
      HttpStatus.HTTP_VERSION_NOT_SUPPORTED,
    );
  }
}

// 418
export class ImATeapotException extends HttpException {

  constructor(message?: string | object | any, error = `I'm a teapot`) {
    super(
      HttpException.createBody(message, error, HttpStatus.I_AM_A_TEAPOT),
      HttpStatus.I_AM_A_TEAPOT,
    );
  }
}

// 500
export class InternalServerErrorException extends HttpException {
  constructor(
    message?: string | object | any,
    error = 'Internal Server Error',
  ) {
    super(
      HttpException.createBody(
        message,
        error,
        HttpStatus.INTERNAL_SERVER_ERROR,
      ),
      HttpStatus.INTERNAL_SERVER_ERROR,
    );
  }
}

// 405
export class MethodNotAllowedException extends HttpException {
  constructor(message?: string | object | any, error = 'Method Not Allowed') {
    super(
      HttpException.createBody(message, error, HttpStatus.METHOD_NOT_ALLOWED),
      HttpStatus.METHOD_NOT_ALLOWED,
    );
  }
}

// 406
export class NotAcceptableException extends HttpException {
  constructor(message?: string | object | any, error = 'Not Acceptable') {
    super(
      HttpException.createBody(message, error, HttpStatus.NOT_ACCEPTABLE),
      HttpStatus.NOT_ACCEPTABLE,
    );
  }
}

// 404
export class NotFoundException extends HttpException {
  constructor(message?: string | object | any, error = 'Not Found') {
    super(
      HttpException.createBody(message, error, HttpStatus.NOT_FOUND),
      HttpStatus.NOT_FOUND,
    );
  }
}

// 501
export class NotImplementedException extends HttpException {
  
  constructor(message?: string | object | any, error = 'Not Implemented') {
    super(
      HttpException.createBody(message, error, HttpStatus.NOT_IMPLEMENTED),
      HttpStatus.NOT_IMPLEMENTED,
    );
  }
}

// 413
export class PayloadTooLargeException extends HttpException {
  constructor(message?: string | object | any, error = 'Payload Too Large') {
    super(
      HttpException.createBody(message, error, HttpStatus.PAYLOAD_TOO_LARGE),
      HttpStatus.PAYLOAD_TOO_LARGE,
    );
  }
}

//  408
export class RequestTimeoutException extends HttpException {
  constructor(message?: string | object | any, error = 'Request Timeout') {
    super(
      HttpException.createBody(message, error, HttpStatus.REQUEST_TIMEOUT),
      HttpStatus.REQUEST_TIMEOUT,
    );
  }
}

// 503
export class ServiceUnavailableException extends HttpException {
  constructor(message?: string | object | any, error = 'Service Unavailable') {
    super(
      HttpException.createBody(message, error, HttpStatus.SERVICE_UNAVAILABLE),
      HttpStatus.SERVICE_UNAVAILABLE,
    );
  }
}

// 401
export class UnauthorizedException extends HttpException {
  constructor(message?: string | object | any, error = 'Unauthorized') {
    super(
      HttpException.createBody(message, error, HttpStatus.UNAUTHORIZED),
      HttpStatus.UNAUTHORIZED,
    );
  }
}

// ...

在 nestjs 中抛出一个异常

标准异常

标准异常的格式如下,只有两个字段:状态码和错误提示消息

{
  "statusCode": 500,
  "message": "Internal server error"
}

下面我们可以控制器中,使用 throw 实例化HttpException 类型,输出标准的异常:

@Get()
async findAll() {
  throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}

第一个参数是 message, 第二个参数是状态码。

覆盖标准输出,自定义一些信息,只需要将第一个参数改成一个对象:

@Get()
async findAll() {
  throw new HttpException({
    status: HttpStatus.FORBIDDEN,
    error: 'This is a custom message',
  }, HttpStatus.FORBIDDEN);
}

当然我们可以自定义类,方便我们复用其他的项目:

export class ForbiddenException extends HttpException {
  constructor() {
    super('Forbidden', HttpStatus.FORBIDDEN);
  }
}

异常与过滤器层配合

异常过滤器,是使得有能力完全的控制异常。可以控制确切的控制流以及将响应的内容发送回客户端。

过滤可以作用于:

  • 路由函数
  • controller 类
  • 在全局中使用
  • 捕获全部的异常

todo

  • jsonp
  • 轮询
  • comet