statuses 是一个在操作 HTTP 状态码时会用到的一个小型工具库。著名 Node.js 后端 Web 框架 Koa/Express 的源码中都有用到它。
本文就来介绍。
基本使用
安装。
bash
$ npm install statuses
安装好后,我们来看看 statuses 的基本使用。
javascript
const status = require('statuses')
status()
status()
是一个函数,接收的参数类型可以是数字,也可以是字符串。
javascript
status(403) // => 'Forbidden'
status('Forbidden') // => 403
- 当接收的是一个数字时,会执行 HTTP 状态码到对应英文信息的转换
- 当接收的是一个字符串时,会执行英文信息到对应的 HTTP 状态码的转换
数字字符串也会作为数字处理。
bash
status('403') // => 'Forbidden'
另外,做英文信息到对应的 HTTP 状态码的转换时,不区分大小写。
bash
status('forbidden') // => 403
如果传入的是无效状态码就会报错。
javascript
status(306) // throws
status('foo') // throws
status.message[code]/.code[msg]
如果不想报错,可以使用 status.message[code]
、status.code[msg]
。
javascript
status.message[306] // undefined
status.code['foo'] // undefined
status()
函数内部其实就是依据传入的参数类型,访问对应的 status.message[code]
、status.code[msg]
的映射对象,不过加了一层抛错判断。
status.empty[code]
除了上述的转换方法,statuses 还提供了一些便捷映射对象。比如:status.empty
。status.empty
中存储了响应体为空时的一个状态码即可。方便你进行响应体为空判断。
javascript
status.empty[200] // => undefined
status.empty[204] // => true
status.empty[205] // => true
status.empty[304] // => true
这样的响应码有 3 个:204(No Content,比如 PUT 请求,Etag 头更新)、205(Reset Content,比如清空表单内容、重置 canvas 状态等)和 304(Not Modified,协商缓存使用)
除此之外,还有一个 status.redirect[code]
。
status.redirect[code]
status.redirect[code]
表示当前是否属于重定向请求。
javascript
status.redirect[200] // => undefined
status.redirect[301] // => true
重定向请求对应的状态码比较多,包括:300(Multiple Choices)、301(Moved Permanently)、302(Found)、303(See Other)、305(Use Proxy)、307(Temporary Redirect) 和 308(Permanent Redirect)。
需要注意的是,301、308 都表示永久重定向,资源永久移动了,浏览器会根据返回的 Location 头信息将地址重定向。一般来说,301 状态码用作 GET 或 HEAD 方法的响应,而对于 POST 则使用 308 Permanent Redirect,因为 308 状态码能够确保请求方法和消息主体不会发生改变。
另外,302 跟 307 语义相同,都表示临时重定向,浏览器会根据返回的 Location 头信息将地址重定向。不过,307 状态码能够确保请求方法和消息主体不会发生改变。
还有最后一个 status.retry[code]
。
status.retry[code]
这些状态码表示这些请求是需要重试的。
javascript
status.retry[501] // => undefined
status.retry[503] // => true
statuses 中定义的重试状态码有 3 个,包括:502(Bad Gateway)、503(Service Unavailable)和 504(Gateway Timeout)。
接下来来看一下 statuses 的实现过程。
分析源码
基于 v2.0.1 版本。
statuses 的所有操作都是围绕 http
模块提供的 STATUS_CODES
常量进行的。
status.message
首先,我们先将 STATUS_CODES
导出为 status.message
。
javascript
const codes = require('node:http').STATUS_CODES
module.exports = status
// status code to message map
status.message = codes
function status(code) {
// ...
}
注意:statuses 源代码中为了兼容("node": ">= 0.8"),
codes
是从codes.json
文件导入的(require('./codes.json')
)。http.STATUS_CODES
从 v0.1.22 就开始支持了,为避免实现冗余,我们就不再使用code.json
了。
status.code
然后,将 STATUS_CODES
处理成响应消息到 HTTP 状态码的映射对象 status.code
。
javascript
// status message (lower-case) to code map
status.code = function createMessageToStatusCodeMap(codes) {
const map = {}
Object.keys(codes).forEach(code => {
const message = codes[code]
const status = Number(code)
// populate map
map[message.toLowerCase()] = status
})
return map
}
注意,存储
message
时,为了方便后续比较,我们将英文信息都转成小写的了。
status()
现在 status.message
和 status.code
都有了,可以着手实现 status()
了。
javascript
module.exports = status
// ...
function status(code) {
if (typeof code === 'number') {
return status.message[code]
}
// ...
}
数字直接丢给 status.message
查询。剩下的情况,如果参数不是字符串类型就报错。
javascript
// ...
function status(code) {
if (typeof code === 'number') {
return status.message[code]
}
if (typeof code === 'string') {
throw new Error('code must be a number or string')
}
// ...
}
接下来再处理类似 '403'
这样的字符串数字。
javascript
// ...
function status(code) {
if (typeof code === 'number') {
return status.message[code]
}
if (typeof code === 'string') {
throw new Error('code must be a number or string')
}
// '403'
const n = parseInt(code, 10)
if (!isNaN(n)) {
return status.message[n]
}
// ...
}
我们使用了 parseInt()
处理数字字符串,也就是说,像 "403.21"
这样的数组字符串会作为 "403"
状态码处理。
最后,处理英文信息到对应的 HTTP 状态码的转换。
javascript
// ...
function status(code) {
if (typeof code === 'number') {
return status.message[code]
}
if (typeof code === 'string') {
throw new Error('code must be a number or string')
}
// '403'
const n = parseInt(code, 10)
if (!isNaN(n)) {
return status.message[n]
}
return status.code[msg.toLowerCase()]
}
注意,
msg
做了转小写的处理。
不过,status(code)
还需要对传入不合法的 HTTP 状态码/信息做抛错处理,因此,我们需要再额外抽象出两个方法,替换直接访问 status.message
/status.code
的方式。
javascript
// ...
function status(code) {
if (typeof code === 'number') {
return getStatusMessage(code)
}
if (typeof code === 'string') {
throw new Error('code must be a number or string')
}
// '403'
const n = parseInt(code, 10)
if (!isNaN(n)) {
return getStatusMessage(n)
}
return getStatusCode(msg)
}
/**
* Get the status code for given message.
* @private
*/
function getStatusCode (message) {
var msg = message.toLowerCase()
if (!Object.prototype.hasOwnProperty.call(status.code, msg)) {
throw new Error('invalid status message: "' + message + '"')
}
return status.code[msg]
}
/**
* Get the status message for given code.
* @private
*/
function getStatusMessage (code) {
if (!Object.prototype.hasOwnProperty.call(status.message, code)) {
throw new Error('invalid status code: ' + code)
}
return status.message[code]
}
status.redirect/.empty/.retry
最后还有 3 个预定义常量 status.redirect
/status.empty
/status.retry
。
javascript
// status codes for redirects
status.redirect = {
300: true,
301: true,
302: true,
303: true,
305: true,
307: true,
308: true
}
// status codes for empty bodies
status.empty = {
204: true,
205: true,
304: true
}
// status codes for when you should retry the request
status.retry = {
502: true,
503: true,
504: true
}
到这里,差不多就写完了。感受一下全部代码。
全部代码
javascript
const codes = require('node:http').STATUS_CODES
module.exports = status
// status code to message map
status.message = codes
// status message (lower-case) to code map
status.code = function createMessageToStatusCodeMap(codes) {
const map = {}
Object.keys(codes).forEach(code => {
const message = codes[code]
const status = Number(code)
// populate map
map(message.toLowerCase()) = status
})
return map
}
// status codes for redirects
status.redirect = {
300: true,
301: true,
302: true,
303: true,
305: true,
307: true,
308: true
}
// status codes for empty bodies
status.empty = {
204: true,
205: true,
304: true
}
// status codes for when you should retry the request
status.retry = {
502: true,
503: true,
504: true
}
function status(code) {
if (typeof code === 'number') {
return getStatusMessage(code)
}
if (typeof code === 'string') {
throw new Error('code must be a number or string')
}
// '403'
const n = parseInt(code, 10)
if (!isNaN(n)) {
return getStatusMessage(n)
}
return getStatusCode(msg)
}
function getStatusCode (message) {
var msg = message.toLowerCase()
if (!Object.prototype.hasOwnProperty.call(status.code, msg)) {
throw new Error('invalid status message: "' + message + '"')
}
return status.code[msg]
}
function getStatusMessage (code) {
if (!Object.prototype.hasOwnProperty.call(status.message, code)) {
throw new Error('invalid status code: ' + code)
}
return status.message[code]
}
一共 82 行。
总结
本文我们讲解了 HTTP 状态码的工具库的基本使用和代码实现。总的来说,statuses 库其实就是基于 require('node:http').STATUS_CODES
常量之上的一套操作封装,Web 框架 Koa、Express 或多或少的都在使用它。
当然,学习使用和实现的过程,也帮助我们更好地理解了常用 HTTP 状态码的含义和使用场景。想全面了解的同学,可以参考 MDN 上的文档《HTTP 响应状态码》进行学习。
好了,关于 statuses 的学习,我们暂时讲到这里了。希望对大家能有所帮助,感谢阅读,再见!
福利内容:真实框架中的使用
这一节展示 statuses 在 Express、Koa 框架中的使用。
Koa
jsx
/**
* Response helper.
*/
function respond(ctx) {
// ...
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
// ...
}
使用 statuses 进行空响应检查。如果是空响应状态码,就把响应体明确指定为空(null
)。
jsx
/**
* Context prototype.
*/
const proto = module.exports = {
/**
* Default error handling.
*
* @param {Error} err
* @api private
*/
onerror (err) {
// ...
// default to 500
if (typeof statusCode !== 'number' || !statuses[statusCode]) statusCode = 500
// respond
const code = statuses[statusCode]
const msg = err.expose ? err.message : code
this.status = err.status = statusCode
this.length = Buffer.byteLength(msg)
res.end(msg)
},
}
用于获取 5xx 状态码对应的报错信息。
jsx
/**
* Prototype.
*/
module.exports = {
/**
* Set response status code.
*
* @param {Number} code
* @api public
*/
set status (code) {
this.res.statusCode = code
if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
if (this.body && statuses.empty[code]) this.body = null
},
}
在显式设置 HTTP 状态码时,再一次执行空响应状态码的判断。
Express
jsx
/**
* Response prototype.
* @public
*/
var res = Object.create(http.ServerResponse.prototype)
/**
* Module exports.
* @public
*/
module.exports = res
/**
* Send given HTTP status code.
*
* Sets the response status to `statusCode` and the body of the
* response to the standard description from node's http.STATUS_CODES
* or the statusCode number if no description.
*
* Examples:
*
* res.sendStatus(200);
*
* @param {number} statusCode
* @public
*/
res.sendStatus = function sendStatus(statusCode) {
var body = statuses.message[statusCode] || String(statusCode)
this.statusCode = statusCode;
this.type('txt');
return this.send(body);
};
注意,Express 中使用的是 statuses.message[statusCode]
而非 statuses(statusCode)
方式获取状态码信息,这样即使 statusCode
不合法,也不会报错,而是返回它的字符串表示。
jsx
/**
* Redirect to the given `url` with optional response `status`
* defaulting to 302.
*
* The resulting `url` is determined by `res.location()`, so
* it will play nicely with mounted apps, relative paths,
* `"back"` etc.
*
* Examples:
*
* res.redirect('/foo/bar');
* res.redirect('http://example.com');
* res.redirect(301, 'http://example.com');
* res.redirect('../login'); // /blog/post/1 -> /blog/login
*
* @public
*/
res.redirect = function redirect(url) {
var address = url;
var body;
var status = 302;
// allow status / url
if (arguments.length === 2) {
if (typeof arguments[0] === 'number') {
status = arguments[0];
address = arguments[1];
} else {
deprecate('res.redirect(url, status): Use res.redirect(status, url) instead');
status = arguments[1];
}
}
// Set location header
address = this.location(address).get('Location');
// Support text/{plain,html} by default
this.format({
text: function(){
body = statuses.message[status] + '. Redirecting to ' + address
},
html: function(){
var u = escapeHtml(address);
body = '<p>' + statuses.message[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>'
},
default: function(){
body = '';
}
});
// Respond
this.statusCode = status;
this.set('Content-Length', Buffer.byteLength(body));
if (this.req.method === 'HEAD') {
this.end();
} else {
this.end(body);
}
};
用于重定向请求时的错误消息拼接,使用的依然是 statuses.message[status]
获取英文信息,避免抛错。