一个很少遇到的网络状态码 -- 499

5,336 阅读2分钟

一、问题背景

前段时间运维拦截到了一些 499 网络状态码报警,我对该问题进行了简单排查,本文对该状态码做简单的一个介绍。

二、问题探寻

2.1 问题探寻-过程

[499 - CLIENT CLOSED REQUEST] A non-standard status code introduced by nginx for the case when a client closes the connection while nginx is processing the request.

查阅文档得知,499 是 nginx 引入的非标准状态代码,用于在 nginx 服务器在处理请求时客户端关闭连接的情况。作为 4xx 开头状态码,表明的是这个请求的过程中是客户端发生了错误。

查阅代码得知,是在某个业务场景,用户点击的服务不同,需要调用接口查询价格,在查询价格的接口下存在这个错误报警。

再切换服务的时候,在上一个请求接口没有返回前,发起新的接口请求并取消上一个请求,这个逻辑是符合预期的。(当然防抖要加上)

尽管在客户端侧只能看到一个被 cancel 的请求,但是这个请求在 nginx 拦截到的状态码是 499。

既然是符合预期的场景,那么其实不该告警,于是联系运维去掉关于该状态码低频状态下的告警。

2.1 问题探寻-取消 ajax 请求

顺便我们看看怎么取消 ajax 请求

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// cancel the request
controller.abort()

自 v0.22 后 ajax 换成以上的方法,因此我也换成这种写法进行介绍。

把以上方法进行一些封装。

// cancel-request.js
import qs from 'qs'
export default class CancelRequest {
  constructor() {
    this.pendingRequest = new Map()
  }
  // 根据请求信息生成唯一标识 key
  geterateReqKey(config) {
    const { url, method, params, data } = config
    return [url, method, qs.stringify(params), qs.stringify(data)].join('&')
  }
  // 把当前请求信息添加到pendingRequest对象中
  addPendingRequest(config, CancelToken) {
  	if (!config.cancelDuplicated) return
    const requestKey = this.geterateReqKey(config)
    if (!config.signal) {
    	const controller = new AbortController()
    	
    	if (!this.pendingRequest.has(requestKey)) {
    		// 把请求取消方法作为 map 值存起来
    		this.pendingRequest.set(requestKey, controller)
    	}
    	config.signal = controller.signal
    }
  }
  // 检查是否存在重复请求,若存在则取消前一次请求
  removePendingRequest(config) {
  	if (!config.cancelDuplicated) return
    const requestKey = this.geterateReqKey(config)
    if (this.pendingRequest.has(requestKey)) {
      const controller = this.pendingRequest.get(requestKey)
      // 取消请求
      controller.abort()
      // 删除map中对应的属性
      this.removeRequestKey(config)
    }
  }
  // 从pendingRequest中删除对应的key
  removeRequestKey(config) {
    const requestKey = this.geterateReqKey(config)
    this.pendingRequest.delete(requestKey)
  }
}
// request.js
import axios from 'axios';
import CancelRequest from './cancel-request.js'
// 实例化
let cancelRequest = new CancelRequest()
const instance = axios.create({
  // ...
});

// 请求拦截器
instance.interceptors.request.use(config => {
  // 在请求开始之前检查先前的请求,如果是重复请求,删除之前的
  cancelRequest.removePendingRequest(config);
  // 如果不存在就将当前请求添加到pendingRequest
  cancelRequest.addPendingRequest(config);
    return config;
}, err => {
    Promise.reject(err);
});
// 响应拦截器
instance.interceptors.response.use(res => {
  // 移除成功请求记录
    cancelRequest.removeRequestKey(res.config)
    return res.data;
}, err => {
  // 失败时也需要移除
    cancelRequest.removeRequestKey(err.config || {} )
    Promise.reject(err);
});
export default instance;

我们将 cancelDuplicated 作为是否开启取消重复请求的开关,于是,在业务接口处,我们采用

import { request } from '@/utils/request'
export const exampleRequest = (params) => {
  return request('apicode', params,{
  	cancelDuplicated: true
  })
}

就可以开启该配置。