app.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. 'use strict'
  2. /* eslint-disable no-console */
  3. // Metrics must be initialized before importing anything else
  4. require('@overleaf/metrics/initialize')
  5. const config = require('config')
  6. const Events = require('node:events')
  7. const BPromise = require('bluebird')
  8. const express = require('express')
  9. const helmet = require('helmet')
  10. const HTTPStatus = require('http-status')
  11. const logger = require('@overleaf/logger')
  12. const Metrics = require('@overleaf/metrics')
  13. const bodyParser = require('body-parser')
  14. const swaggerTools = require('swagger-tools')
  15. const swaggerDoc = require('./api/swagger')
  16. const security = require('./api/app/security')
  17. const healthChecks = require('./api/controllers/health_checks')
  18. const { mongodb, loadGlobalBlobs } = require('./storage')
  19. const path = require('node:path')
  20. Events.setMaxListeners(20)
  21. const app = express()
  22. module.exports = app
  23. logger.initialize('history-v1')
  24. Metrics.open_sockets.monitor()
  25. Metrics.injectMetricsRoute(app)
  26. app.use(Metrics.http.monitor(logger))
  27. Metrics.leaked_sockets.monitor(logger)
  28. // We may have fairly large JSON bodies when receiving large Changes. Clients
  29. // may have to handle 413 status codes and try creating files instead of sending
  30. // text content in changes.
  31. app.use(bodyParser.json({ limit: '6MB' }))
  32. app.use(
  33. bodyParser.urlencoded({
  34. extended: false,
  35. })
  36. )
  37. security.setupSSL(app)
  38. security.setupBasicHttpAuthForSwaggerDocs(app)
  39. const HTTP_REQUEST_TIMEOUT = parseInt(config.get('httpRequestTimeout'), 10)
  40. app.use(function (req, res, next) {
  41. res.setTimeout(HTTP_REQUEST_TIMEOUT)
  42. next()
  43. })
  44. app.get('/', function (req, res) {
  45. res.send('')
  46. })
  47. app.get('/status', healthChecks.status)
  48. app.get('/health_check', healthChecks.healthCheck)
  49. function setupSwagger() {
  50. return new BPromise(function (resolve) {
  51. swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) {
  52. app.use(middleware.swaggerMetadata())
  53. app.use(middleware.swaggerSecurity(security.getSwaggerHandlers()))
  54. app.use(middleware.swaggerValidator())
  55. app.use(
  56. middleware.swaggerRouter({
  57. controllers: path.join(__dirname, 'api/controllers'),
  58. useStubs: app.get('env') === 'development',
  59. })
  60. )
  61. app.use(middleware.swaggerUi())
  62. resolve()
  63. })
  64. })
  65. }
  66. function setupErrorHandling() {
  67. app.use(function (req, res, next) {
  68. const err = new Error('Not Found')
  69. err.status = HTTPStatus.NOT_FOUND
  70. return next(err)
  71. })
  72. // Handle Swagger errors.
  73. app.use(function (err, req, res, next) {
  74. const projectId = req.swagger?.params?.project_id?.value
  75. if (res.headersSent) {
  76. return next(err)
  77. }
  78. if (err.code === 'SCHEMA_VALIDATION_FAILED') {
  79. logger.error({ err, projectId }, err.message)
  80. return res.status(HTTPStatus.UNPROCESSABLE_ENTITY).json(err.results)
  81. }
  82. if (err.code === 'INVALID_TYPE' || err.code === 'PATTERN') {
  83. logger.error({ err, projectId }, err.message)
  84. return res.status(HTTPStatus.UNPROCESSABLE_ENTITY).json({
  85. message: 'invalid type: ' + err.paramName,
  86. })
  87. }
  88. if (err.code === 'ENUM_MISMATCH') {
  89. return res.status(HTTPStatus.UNPROCESSABLE_ENTITY).json({
  90. message: 'invalid enum value: ' + err.paramName,
  91. })
  92. }
  93. if (err.code === 'REQUIRED') {
  94. return res.status(HTTPStatus.UNPROCESSABLE_ENTITY).json({
  95. message: err.message,
  96. })
  97. }
  98. next(err)
  99. })
  100. app.use(function (err, req, res, next) {
  101. const projectId = req.swagger?.params?.project_id?.value
  102. logger.error({ err, projectId }, err.message)
  103. if (res.headersSent) {
  104. return next(err)
  105. }
  106. // Handle errors that specify a statusCode. Some come from our code. Some
  107. // bubble up from AWS SDK, but they sometimes have the statusCode set to
  108. // 200, notably some InternalErrors and TimeoutErrors, so we have to guard
  109. // against that. We also check `status`, but `statusCode` is preferred.
  110. const statusCode = err.statusCode || err.status
  111. if (statusCode && statusCode >= 400 && statusCode < 600) {
  112. res.status(statusCode)
  113. } else {
  114. res.status(HTTPStatus.INTERNAL_SERVER_ERROR)
  115. }
  116. const sendErrorToClient = app.get('env') === 'development'
  117. res.json({
  118. message: err.message,
  119. error: sendErrorToClient ? err : {},
  120. })
  121. })
  122. }
  123. app.setup = async function appSetup() {
  124. await mongodb.client.connect()
  125. logger.info('Connected to MongoDB')
  126. await loadGlobalBlobs()
  127. logger.info('Global blobs loaded')
  128. app.use(helmet())
  129. await setupSwagger()
  130. setupErrorHandling()
  131. }
  132. async function startApp() {
  133. await app.setup()
  134. const port = parseInt(process.env.PORT, 10) || 3100
  135. app.listen(port, err => {
  136. if (err) {
  137. console.error(err)
  138. process.exit(1)
  139. }
  140. Metrics.event_loop.monitor(logger)
  141. Metrics.memory.monitor(logger)
  142. })
  143. }
  144. // Run this if we're called directly
  145. if (!module.parent) {
  146. startApp().catch(err => {
  147. console.error(err)
  148. process.exit(1)
  149. })
  150. }