You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Heroku除登录端点外所有接口返回503,开发环境运行正常

Heroku部署后除登录端点外其他API请求超时(H12错误)

开发环境下博客、用户信息和登录页面功能完全正常,但部署到Heroku生产环境后,仅/api/login端点可用,/api/blogs、/api/users等所有GET/POST端点均无法正常工作,Heroku日志显示H12 - 请求超时错误,伴随503状态码。

相关代码

app.js

require('dotenv').config()
const config = require('./utils/config')
const express = require('express')
require('express-async-errors')
const app = express()
const mongoose = require('mongoose')
const cors = require('cors')
const morgan = require('morgan');
const middleware = require('./utils/middleware')
const blogsRouter = require('./controllers/blogs')
const usersRouter = require('./controllers/users')
const loginRouter = require('./controllers/login')

mongoose.connect(config.MONGODB_URI)
  .then(() => {
    console.log('connected to MongoDB')
  })
  .catch((error) => {
    console.log('error connection to MongoDB:', error.message)
  })

app.use(middleware.tokenExtractor)

app.use(cors())
app.use(express.static("build"))
app.use(express.json())

app.use('/api/blogs', blogsRouter)
app.use('/api/users', usersRouter)
app.use('/api/login', loginRouter)

app.use(middleware.errorHandler)
app.use(middleware.unknownEndpoint)

app.use(morgan(':method :url :body'));

module.exports = app

index.js

require('dotenv').config()
const app = require('./app')
const http = require('http')
const logger = require('./utils/logger')
const config = require('./utils/config')

const server = http.createServer(app)

const PORT = process.env.PORT
server.listen(PORT, () => {
  logger.info(`Server running on port ${config.PORT}`)
})

Controllers

blogs.js

const blogsRouter = require('express').Router()
const { getNextKeyDef } = require('@testing-library/user-event/dist/keyboard/getNextKeyDef')
const Blog = require('../models/blog')
const User = require('../models/user')
const jwt = require('jsonwebtoken')

const getTokenFrom = request => {
  const authorization = request.get('authorization')
  if (authorization && authorization.toLowerCase().startsWith('bearer ')) {
    return authorization.substring(7)
  }
  return null
}
 
blogsRouter.get('/', (request, response) => {
    Blog
      .find({}).populate('user', {username: 1, name: 1})
      .then(blogs => {
        response.json(blogs)
      })
  })
  
blogsRouter.get('/:id', async (request, response) => {
      const blog = await Blog.findById(request.params.id).populate('user', {username: 1, name: 1}, ('comments', {commenter: 1, comment: 1}))
      if (blog) {
        response.json(blog.toJSON())
      } else {
        response.status(404).end()
      }
  })


blogsRouter.post('/', async (request, response, next) => {
    const body = request.body

    const token = getTokenFrom(request)
    const decodedToken = jwt.verify(token, process.env.SECRET)
    if (!decodedToken.id) {
      return response.status(401).json({ error: 'token missing or invalid' })
    }
    const user = await User.findById(decodedToken.id)
  
  if (body.title === undefined) {
    return response.status(400).json({ error: 'content missing' })
  }

  if (body.author === undefined) {
    return response.status(400).json({ error: 'content missing' })
  }


  const newblog = new Blog({
    title: body.title,
    author: body.author,
    url: body.url,
    user: user._id  })

    const savedBlog = await newblog.save()
    user.blogs = user.blogs.concat(savedBlog._id)
    await user.save()

    response.json(savedBlog)

  })

  blogsRouter.post('/:id/comments', async (request, response, next) => {
  try {
    const body = request.body

  const blog = await Blog.findById(request.params.id)

  if (blog) {
    blog.comments.push(body.comment)
   const savedBlog  = await blog.save()
 response.status(200).json(savedBlog.toJSON());
  }

  } catch (exception) {
    next(exception)
  }})

blogsRouter.delete('/:id', async (request, response, next) => {
  const decodedToken = jwt.verify(request.token, process.env.SECRET)

  if (!request.token || !decodedToken.id) {
    return response.status(401).json({
      error: 'token missing or invalid'
    })
  }
  try {
    const blog = await Blog.findById(request.params.id)

    if (blog.user.toString() === decodedToken.id.toString()) {
      await blog.remove()
      response.status(204).end()
    } else {
      response.status(401).end()
    }
  } catch (exception) {
    next(exception)
  }
  })

blogsRouter.put('/:id', async (request, response, next) => {
    const body = request.body

    /* const decodedToken = jwt.verify(request.token, process.env.SECRET)

    if (!request.token || !decodedToken.id) {
      return response.status(401).json({
        error: 'token missing or invalid'
      })
    }

    const user = await User.findById(decodedToken.id) */
  
    const blog = {
      title: body.title,
      author: body.author,
      url: body.url,
      user: body.user._id,
      likes: body.likes
    }
  
    const savedBlog = await Blog.findByIdAndUpdate(request.params.id, blog, { new: true })
    response.json(savedBlog)
  })
  

  module.exports = blogsRouter

login.js

const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const loginRouter = require('express').Router()
const User = require('../models/user')

loginRouter.post('/', async (request, response) => {
  const { username, password } = request.body

  const user = await User.findOne({ username })
  const passwordCorrect = user === null
    ? false
    : await bcrypt.compare(password, user.passwordHash)

  if (!(user && passwordCorrect)) {
    return response.status(401).json({
      error: 'invalid username or password'
    })
  }

  const userForToken = {
    username: user.username,
    id: user._id,
  }

  const token = jwt.sign(userForToken, process.env.SECRET)

  response
    .status(200)
    .send({ token, username: user.username, name: user.name })
})

module.exports = loginRouter

users.js

const bcrypt = require('bcrypt')
const usersRouter = require('express').Router()
const User = require('../models/user')

usersRouter.get('/', async (request, response) => {
    const users = await User.find({}).populate('blogs', {title: 1, author: 1, url: 1})
    response.json(users)
  })

usersRouter.post('/', async (request, response) => {
  const { username, name, password } = request.body

  const existingUser = await User.findOne({ username })
  if (existingUser) {
    return response.status(400).json({
      error: 'username must be unique'
    })
  }

  if (!username) {
    return response.status(400).json({
      error: 'please add a username'
    })
  }

  
  if (!password) {
    return response.status(400).json({
      error: 'please add a valid password'
    })
  }



  const saltRounds = 10
  const passwordHash = await bcrypt.hash(password, saltRounds)

  const user = new User({
    username,
    name,
    passwordHash,
  })

  const savedUser = await user.save()

  response.status(201).json(savedUser)
})

module.exports = usersRouter

排查与解决方向

  • MongoDB连接校验:检查Heroku环境变量中MONGODB_URI是否正确,确认数据库(如MongoDB Atlas)的IP白名单允许Heroku服务器访问,登录端点能工作可能是因为未触发复杂数据库查询,而其他端点因数据库连接超时导致请求超过Heroku 30秒限制。
  • 调整中间件顺序:将morgan日志中间件移到express.json()之后、路由挂载之前,确保能记录所有请求日志,排查请求是否到达服务器。
  • 统一异步处理逻辑:将blogsRouter.get('/')这类使用.then()的路由改为async/await写法,确保数据库查询错误能被express-async-errors捕获,避免请求无响应超时。
  • 检查Token提取一致性:tokenExtractor中间件和部分路由自定义的getTokenFrom逻辑需保持一致,避免因Token提取失败导致后续逻辑卡住。
  • Heroku资源检查:免费额度dyno可能因资源不足超时,尝试重启dyno或查看Heroku metrics监控资源占用情况。

内容的提问来源于stack exchange,提问作者Julius Goddard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 21:01:04