koa中当你使用不正确使用next时会造成的404 Not Found问题.【附带koa源码解释】

原创文章
声明:作者声明此文章为原创,未经作者同意,请勿转载,若转载,务必注明本站出处,本平台保留追究侵权法律责任的权利。
全栈老韩
全栈工程师,擅长iOS App开发、前端(vue、react、nuxt、小程序&Taro)开发、Flutter、React Native、后端(midwayjs、golang、express、koa)开发、docker容器、seo优化等。

因为koa是一个以中间件为基础的网络框架,所以经常有许多和中间件相关的操作,一个中间件处理完事务,就会交由另一个中间件继续处理。但是当处理next不当时,经常会有异常的问题出现,导致花费比较多的时间。

一、问题

404 Not Found

koa 404

如上图所示,是一个get接口请求得到404的截图。

二、代码审查

代码截图:
代码截图

user.ts 复制代码
const userRouter = new Router({
  prefix: "/user",
});

userRouter.get("/profile", ValidTokenMiddleware, async (ctx: any) => {
  const user_id = MiddlewareGetHeaderUserId(ctx);
  console.log("user id: ", user_id);
  const user = await UserModel.findById(user_id);
  console.log("user: ", user);
  ctx.body = new LTNetResponse(LTNetErrorCode.Success, user, "");
});

如上代码所示,接口/user/profile通过get请求,经过中间件ValidTokenMiddleware,然后交由async (ctx:any) => {}中间件处理请求结果。

从代码逻辑上是没有问题的,但是为什么会得到一个404 Not Found的结果呢。

首先,代码中的数据库处理是没有问题的,也是经得起验证的。
其次,直接去掉awaitasync相关的代码,直接ctx.body="123",是不会404 Not Found的。

所以,404 Not Found是和异步调用有关产生的问题,那么继续审查代码可以发现,其中的中间件ValidTokenMiddleware应该是导致这个问题的关键。

查看ValidTokenMiddleware中间件的内容:
中间件_1.png
中间件_2.png

其实能找到关键代码:

ValidTokenMiddleware_file.ts 复制代码
...
    const user_id_of_header = decoded.userId;
    if (userId != user_id_of_header + "") {
      ctx.body = new LTNetResponse(
        LTNetErrorCode.ParamError,
        null,
        String_Token_Error
      );
      return;
    }
    next();
...

我们可以看到next的中间件传递处理,但是这里没有使用await next()。问题找到了,应该在next()时使用await关键字。

三、正确认识next

正如koa的核心概念,koa使用的是一个以洋葱模型进行网络请求的入和出的数据流动逻辑。
我们看看koajs对next的解释:

koa example 复制代码
// Middleware normally takes two parameters (ctx, next), ctx is the context for one request,
// next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.

app.use((ctx, next) => {
  const start = Date.now();
  return next().then(() => {
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.

所以next是一个返回Promise对象的异步函数,koajs也在官网给出了next的代码示例:
koa cascading

When a middleware invokes next() the function suspends and passes control to the next middleware defined. After there are no more middleware to execute downstream, the stack will unwind and each middleware is resumed to perform its upstream behaviour.

对于中间件这样的一个栈结构,在某一个中间件(叫做中间件A)中,如果不用await修饰next(),那么下一个中间件(叫做中间件B)其中的async异步操作完成的时候,中间件B中的控制权无法返回到中间件A,导致response无法完成,也就是形成404 Not Found

四、代码解释

4.1 koa构造的时候,做了什么

koa app source codes
从截图中可以看到这里有一个中间件数组。
在koa初始化后,会使用use加入很多中间件:
koa use middleware
这里的use就是往中间件数组中加入函数的。

4.2 koa监听的时候做了什么

koa Application.js 复制代码
/**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

将一个callback传递给node.httpcallback后面说,对于createServer的定义是这样的:

复制代码
import http from 'node:http';

// Create a local server to receive data from
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    data: 'Hello World!',
  }));
});

所以现在记住,reqres都会被callback处理。

4.3 callback做了什么

首先看callback的源码:

复制代码
/**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      if (!this.ctxStorage) {
        return this.handleRequest(ctx, fn);
      }
      return this.ctxStorage.run(ctx, async() => {
        return await this.handleRequest(ctx, fn);
      });
    };

    return handleRequest;
  }

reqresnode.http原生请求中过来后,向compose传递了middleware数组得到一个fn函数,并且对于这次请求的req和res创建了一个上下文context,

compose的fn 复制代码
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

compose就是在中间件数组中,从序号0开始,依次读取中间件,通过i+1dispatch.bind操作读取数组中的下一个中间件,依次处理context,也就是说,next指的是下一个中间件【next返回值是Promise】,这样子形成了一个Promise的链式调用。

其实就一句话,一个中间件就是一个函数,第一个参数是context,第二个参数next是下一个中间件函数,context在中间件之间传递。

当中间件A中调用next(),不加await,那么中间件B的resolve状态将不会返回,中间件B中即使对ctx.body的修改将不会按照顺序链的调用方式,因此本篇文章示例代码中ctx.body将不会正确处理,导致404 Not Found.

暂无评论,快来发表第一条评论吧