Expressのミドルウェアのnext()について色々勘違いしていたせいで若干戸惑ったので、顛末を残しておきます。
前提
Node.js 14.15.3
Express 4.17.1
起こったこと
Expressでリクエストから渡されたトークンを処理するミドルウェアを実装していました。
//get jwt token from authorization header
//set token to token field of request
const tokenExtractor = (request, response, next) => {
const authorization = request.get('authorization')
if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
request.token = authorization.substr(7)
next()
}
request.token = null
next()
}
後続の処理でrequest.tokenみたいにトークンを扱えるようにするミドルウェアです。
後には以下のような処理が続きます。
/**
* store new blog if a valid token is sent with the request
*/
blogsRouter.post('/', async (request, response) => {
const body = request.body
const token = request.token
const decodedToken = jwt.verify(token, process.env.SECRET)
if (!token || !decodedToken.id) {
return response.status(404).json({error: 'token missing or invalid'})
}
const user = await User.findById(decodedToken.id)
const blog = new Blog({
title: body.title,
author: body.author,
url: body.url,
likes: Number(body.likes),
user: user._id
})
const savedBlog = await blog.save()
console.log(savedBlog)
//save user-blog relation
user.blogs = user.blogs.concat(savedBlog)
await user.save()
return response.json(savedBlog)
})
トークンがマッチしてたらBlogを新規登録できるという処理です。
で、ここのjwt.verify()の呼び出しで、トークンがある場合でも第一引数にnullが渡されていることが発覚しました。
つまりrequest.tokenにnullがセットされているということを意味します。
request.tokenがnullということは、ミドルウェア内の
if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
request.token = authorization.substr(7)
next()
}
request.token = null
next()
ここが怪しそうです。
でもAuthorizationヘッダーにトークンがある場合はifの条件に引っかかって、if内のnext()で次の処理に移るのでは?
しかしいくら確認してもrequest.tokenはnullのままです。
公式ドキュメントを見てみる
上記の next() の呼び出しに注意してください。この関数を呼び出すと、アプリケーション内の次のミドルウェア関数が呼び出されます。
next()は次の処理を呼び出すコールバックで、現在の処理を抜けるわけではないっぽいです。
next()を呼ぶと、現在の関数を抜けて次の処理に移るのかなと勘違いしてしまっていたので、妙な動きをしてしまっていたわけです。
以下のようにelseを追加して修正すると、しっかりトークンの値を取得することができました。
//get jwt token from authorization header
//set token to token field of request
const tokenExtractor = (request, response, next) => {
const authorization = request.get('authorization')
if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
request.token = authorization.substr(7)
next()
}else{
request.token = null
next()
}
}
余談
解決済みですが原因がわからないものなので、余談とします。
今回の件ではトークンの値が取れなかったこと以外に、もう一つエラーが発生していました。以下のようなものです。
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:558:11)
at ServerResponse.header (/Users/xxx/projects/bloglist/node_modules/express/lib/response.js:771:10)
at ServerResponse.send (/Users/xxx/projects/bloglist/node_modules/express/lib/response.js:170:12)
at ServerResponse.json (/Users/xxx/projects/bloglist/node_modules/express/lib/response.js:267:15)
at /Users/xxx/projects/bloglist/controllers/blogs.js:47:18
at processTicksAndRejections (internal/process/task_queues.js:93:5)
レスポンス返した後にレスポンスヘッダーに値をセットしようとして発生したエラーっぽいです。つまりレスポンスを返してからさらに何かしらの処理を行おうとしたというわけです。
で、ミドルウェアの話に戻るのですが、このエラーはミドルウェア修正後は発生しなくなりました。なのでなんとなく関係ありそうな気がします。
修正前のミドルウェアはこういうものでした。
//get jwt token from authorization header
//set token to token field of request
const tokenExtractor = (request, response, next) => {
const authorization = request.get('authorization')
if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
request.token = authorization.substr(7)
next()
}
request.token = null
next()
}
if内でまず1つ目のnext()が呼ばれます。後続の処理は上にあるような「トークンが正しいものならBlogを新規作成する」というものですが、この処理の中ですでにレスポンスを返しています。
で、その後ifを抜けて次のnext()が呼ばれます。
ここでレスポンスオブジェクトに対して最終的にヘッダーに値を付与するような処理が走るのだと思いますが...ちょっとまだ理解が浅いせいでどういう流れでそうなるのかが追えませんでした。
もしどなたか詳しくて優しい方がいらっしゃれば、コメントで教えていただけるとありがたいです。