serverest 3.2.1 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "serverest",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "description": "Servidor REST local de forma rápida e simples para estudo de testes de API",
5
5
  "author": "Paulo Gonçalves <author@serverest.dev> (https://www.linkedin.com/in/paulo-goncalves/)",
6
6
  "license": "GPL-3.0",
package/src/app.js CHANGED
@@ -50,6 +50,11 @@ if (process.env.BLOCKED_IPS) {
50
50
  }
51
51
 
52
52
  app.set('json spaces', 4)
53
+ // Confia em exatamente um proxy, que é o do Cloud Run. Sem isso req.ip resolve para o
54
+ // endereço do proxy e todos os clientes viram a mesma chave no rate limiter. Com 'true'
55
+ // o Express passaria a ler o primeiro endereço do X-Forwarded-For, que o próprio cliente
56
+ // pode forjar, então o número de saltos precisa ser explícito.
57
+ app.set('trust proxy', 1)
53
58
  app.use(express.json())
54
59
  app.use(express.urlencoded({ extended: false }))
55
60
  app.use(queryParser())
@@ -6,9 +6,20 @@ const {
6
6
  } = require('../utils/ambiente')
7
7
  const { LOAD_TEST_DETECTED } = require('../utils/constants')
8
8
 
9
- const rateLimiter = new RateLimiterMemory({
10
- points: 600, // requests
11
- duration: 30 // segundo por IP
9
+ // Limite por cliente. Depende do 'trust proxy' definido no app.js: sem ele req.ip resolve
10
+ // para o endereço do proxy do Cloud Run e este limite passaria a valer para todos somados.
11
+ const limitePorIp = new RateLimiterMemory({
12
+ points: 300, // requests
13
+ duration: 30 // segundos, por IP
14
+ })
15
+
16
+ // Limite agregado do container. Existe porque o limite por IP, sozinho, não impõe teto ao
17
+ // total: bastariam vários clientes simultâneos para saturar a instância, que roda com
18
+ // 256 MiB e max-instances 1. Fica acima do limite por IP para que um único cliente abusivo
19
+ // seja barrado pelo próprio limite antes de consumir a cota que protege os demais.
20
+ const limiteGlobal = new RateLimiterMemory({
21
+ points: 900, // requests
22
+ duration: 30 // segundos, somando todos os clientes
12
23
  })
13
24
 
14
25
  module.exports = async (req, res, next) => {
@@ -16,11 +27,17 @@ module.exports = async (req, res, next) => {
16
27
  return next()
17
28
  }
18
29
 
19
- await rateLimiter.consume(req.ip)
20
- .then(() => next())
21
- .catch(() => {
22
- return res.status(429).send({
23
- message: LOAD_TEST_DETECTED
24
- })
30
+ try {
31
+ // O global vem primeiro de propósito. Avaliar os dois em paralelo faria a requisição
32
+ // consumir a cota individual mesmo quando o bloqueio viesse do limite agregado, o que
33
+ // penalizaria clientes inocentes durante congestionamento e, pior, criaria uma chave
34
+ // por endereço no mapa mesmo para requisições já rejeitadas.
35
+ await limiteGlobal.consume('global')
36
+ await limitePorIp.consume(req.ip)
37
+ return next()
38
+ } catch (_) {
39
+ return res.status(429).send({
40
+ message: LOAD_TEST_DETECTED
25
41
  })
42
+ }
26
43
  }
package/src/server.js CHANGED
@@ -7,16 +7,57 @@ require('dotenv').config({ path: path.resolve(__dirname, '../.env'), quiet: true
7
7
 
8
8
  const colors = require('colors')
9
9
  const debug = require('debug')('nodestr:server')
10
+ const fs = require('fs')
10
11
  const http = require('http')
12
+ const util = require('util')
11
13
  const open = require('open').default
12
14
 
13
15
  const { version } = require('../package.json')
14
16
  const { formaDeExecucao, urlDocumentacao, aplicacaoExecutandoLocalmente } = require('./utils/ambiente')
15
17
  const { conf } = require('./utils/conf')
16
18
  const getRandomFinancialContributor = require('./utils/getRandomFinancialContributor')
19
+ const { log } = require('./utils/logger')
17
20
 
18
21
  const DEFAULT_PORT = 3000
19
22
 
23
+ // Erros fora do ciclo de uma requisição encerram o processo sem deixar registro da causa,
24
+ // o que no Cloud Run aparece apenas como um container reiniciando sem explicação.
25
+ /* istanbul ignore next */
26
+ process.on('uncaughtException', valor => {
27
+ // Node permite lançar qualquer valor, não só Error. Sem esta normalização, um throw de
28
+ // string ou null faria o próprio handler lançar ao acessar .code, perdendo o registro
29
+ // da causa, que é justamente o que estes handlers existem para garantir.
30
+ const erro = valor instanceof Error ? valor : new Error(util.inspect(valor))
31
+
32
+ // Responder a uma requisição já encerrada é inofensivo: a resposta chegou ao cliente e o
33
+ // processo segue íntegro. Derrubar o servidor por isso custaria uma indisponibilidade
34
+ // inteira para corrigir nada.
35
+ if (erro.code === 'ERR_HTTP_HEADERS_SENT') {
36
+ log({ level: 'error', message: `Tentativa de responder requisição já encerrada: ${erro.message}` })
37
+ return
38
+ }
39
+
40
+ encerrar(`Exceção não capturada: ${erro.message}. Stack: ${erro.stack}`)
41
+ })
42
+
43
+ /* istanbul ignore next */
44
+ process.on('unhandledRejection', motivo => {
45
+ encerrar(`Promise rejeitada sem tratamento: ${motivo instanceof Error ? motivo.stack : util.inspect(motivo)}`)
46
+ })
47
+
48
+ // Sai de imediato em vez de drenar as conexões abertas. Com max-instances 1 no Cloud Run
49
+ // não existe outra instância para atender quem chegar durante a drenagem, então esperar
50
+ // só adiaria a subida do container substituto e prolongaria a indisponibilidade.
51
+ /* istanbul ignore next */
52
+ function encerrar (mensagem) {
53
+ // console.log é assíncrono quando stdout é um pipe, caso do Cloud Run, e process.exit
54
+ // não drena writes pendentes. A escrita síncrona no stderr garante que a causa fique
55
+ // registrada mesmo que o log estruturado seja truncado pelo encerramento.
56
+ fs.writeSync(2, `${mensagem}\n`)
57
+ log({ level: 'error', message: mensagem })
58
+ process.exit(1)
59
+ }
60
+
20
61
  const yargs = require('yargs/yargs')
21
62
  const { hideBin } = require('yargs/helpers')
22
63