Middleware
Middleware é uma série de funções executadas durante uma solicitação HTTP antes que a solicitação chegue ao manipulador de rotas. Cada função na cadeia pode encerrar a solicitação ou encaminhá-la para o próximo middleware.
Um aplicativo AdonisJS típico usa middleware para analisar o corpo da solicitação, gerenciar sessões de usuários, autenticar solicitações, servir ativos estáticos, etc.
Você também pode criar middleware personalizado para executar tarefas adicionais durante uma solicitação HTTP.
Pilhas de middleware
Para dar a você melhor controle sobre a execução do pipeline de middleware, o AdonisJS dividiu a pilha de middleware nos três grupos a seguir.
Pilha de middleware do servidor
O middleware do servidor é executado em todas as solicitações HTTP, mesmo que você não tenha definido nenhuma rota para a URL da solicitação atual.
Eles são ótimos para adicionar funcionalidades adicionais ao seu aplicativo que não dependem do sistema de roteamento do framework. Por exemplo, o middleware Static Assets é registrado como middleware de servidor.
Você pode registrar o middleware de servidor usando o método server.use
dentro do arquivo start/kernel.ts
.
import server from '@adonisjs/core/services/server'
server.use([
() => import('@adonisjs/static/static_middleware')
])
Pilha de middleware de roteador
O middleware de roteador também é conhecido como middleware global. Ele é executado em cada solicitação HTTP que tem uma rota correspondente.
O Bodyparser, auth e o middleware de sessão são registrados na pilha de middleware de roteador.
Você pode registrar o middleware de roteador usando o método router.use
dentro do arquivo start/kernel.ts
.
import router from '@adonisjs/core/services/router'
router.use([
() => import('@adonisjs/core/bodyparser_middleware')
])
Coleção de middleware nomeado
O middleware nomeado é uma coleção de middleware que não são executados a menos que explicitamente atribuídos a uma rota ou grupo.
Em vez de definir o middleware como um retorno de chamada em linha dentro do arquivo de rotas, recomendamos que você crie classes de middleware dedicadas, armazene-as dentro da coleção de middleware nomeada e, em seguida, atribua-as às rotas.
Você pode definir o middleware nomeado usando o método router.named
dentro do arquivo start/kernel.ts
. Certifique-se de exportar a coleção nomeada para poder usá-la dentro do arquivo de rotas.
import router from '@adonisjs/core/services/router'
router.named({
auth: () => import('#middleware/auth_middleware')
})
Criando middleware
O middleware é armazenado dentro do diretório ./app/middleware
, e você pode criar um novo arquivo de middleware executando o comando ace make:middleware
.
node ace make:middleware user_location
O comando acima criará o arquivo user_location_middleware.ts
no diretório middleware.
Um middleware é representado como uma classe com o método handle
. Durante a execução, o AdonisJS chamará automaticamente esse método e dará a ele o HttpContext como o primeiro argumento.
// app/middleware/user_location_middleware.ts
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
}
}
Dentro do método handle
, um middleware tem que decidir se continua com a solicitação, finaliza a solicitação enviando uma resposta ou gera uma exceção para abortar a solicitação.
Abortar solicitação
Se um middleware gerar uma exceção, todos os middlewares futuros e o manipulador de rotas não serão executados, e a exceção será dada ao manipulador de exceções global.
import { Exception } from '@adonisjs/core/exceptions'
import { NextFn } from '@adonisjs/core/types/http'
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
throw new Exception('Aborting request')
}
}
Continuar com a solicitação
Você deve chamar o método next
para continuar com a solicitação. Caso contrário, o restante das ações dentro da pilha de middleware não serão executadas.
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// Chame a função `next` para continuar
await next()
}
}
Enviar uma resposta e não chamar o método next
Finalmente, você pode encerrar a solicitação enviando a resposta. Nesse caso, não chame o método next
.
export default class UserLocationMiddleware {
async handle(ctx: HttpContext, next: NextFn) {
// envie a resposta + não chame next
ctx.response.send('Ending request')
}
}
Atribuindo middleware a rotas e grupos de rotas
A coleção de middleware nomeada não é usada por padrão, e você deve atribuí-la explicitamente a rotas ou grupos de rotas.
No exemplo a seguir, primeiro importamos a coleção middleware
e atribuímos o middleware userLocation
a uma rota.
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.get('posts', () => {})
.use(middleware.userLocation())
Vários middlewares podem ser aplicados como uma matriz ou chamando o método use
várias vezes.
router
.get('posts', () => {})
.use([
middleware.userLocation(),
middleware.auth()
])
Da mesma forma, você também pode atribuir middleware a um grupo de rotas. O middleware do grupo será aplicado a todas as rotas do grupo automaticamente.
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router.group(() => {
router.get('posts', () => {})
router.get('users', () => {})
router.get('payments', () => {})
}).use(middleware.userLocation())
Parâmetros do middleware
O middleware registrado na coleção de middleware nomeada pode aceitar um parâmetro adicional como parte dos argumentos do método handle
. Por exemplo, o middleware auth
aceita o guard de autenticação como uma opção de configuração.
type AuthGuards = 'web' | 'api'
export default class AuthMiddleware {
async handle(ctx, next, options: { guard: AuthGuards }) {
}
}
Ao atribuir o middleware à rota, você pode especificar o guard a ser usado.
import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router.get('payments', () => {}).use(
middleware.auth({ guard: 'web' })
)
Injeção de dependência
As classes de middleware são instanciadas usando o contêiner IoC; portanto, você pode dar dicas de tipo para dependências dentro do construtor de middleware, e o contêiner as injetará para você.
Dado que você tem uma classe GeoIpService
para procurar a localização do usuário a partir do IP da solicitação, você pode injetá-la no middleware usando o decorador @inject
.
// app/services/geoip_service.ts
export class GeoIpService {
async lookup(ipAddress: string) {
// localização de pesquisa e retorne
}
}
import { inject } from '@adonisjs/core'
import { GeoIpService } from '#services/geoip_service'
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
@inject()
export default class UserLocationMiddleware {
constructor(protected geoIpService: GeoIpService) {
}
async handle(ctx: HttpContext, next: NextFn) {
const ip = ctx.request.ip()
ctx.location = await this.geoIpService.lookup(ip)
}
}
Fluxo de execução do middleware
A camada de middleware do AdonisJS é construída sobre o padrão de design Chain of Responsibility. Um middleware tem duas fases de execução: a fase downstream e a fase upstream.
- A fase downstream é o bloco de código que você escreve antes de chamar o método
next
. Nesta fase, você manipula a solicitação. - A fase upstream é o bloco de código que você pode escrever após chamar o método
next
. Nesta fase, você pode inspecionar a resposta ou alterá-la completamente.
Middleware e tratamento de exceções
O AdonisJS captura automaticamente a exceção gerada pelo pipeline do middleware ou pelo manipulador de rotas e a converte em uma resposta HTTP usando o manipulador de exceções global.
Como resultado, você não precisa encapsular as chamadas de função next
dentro de uma instrução try/catch
. Além disso, o tratamento automático de exceções garante que a lógica upstream do middleware seja sempre executada após a chamada de função next
.
Mutando a resposta de um middleware
A fase upstream do middleware pode mutar o corpo da resposta, os cabeçalhos e o código de status. Isso descartará a resposta antiga definida pelo manipulador de rota ou qualquer outro middleware.
Antes de mutar a resposta, você deve garantir que está lidando com o tipo de resposta correto. A seguir está a lista de tipos de resposta na classe Response
.
- Resposta padrão refere-se ao envio de valores de dados usando o método
response.send
. Seu valor pode ser umArray
,Object
,String
,Boolean
ouBuffer
. - Resposta de streaming refere-se ao envio de um fluxo para o soquete de resposta usando o método
response.stream
. - Resposta de download de arquivo refere-se ao download de um arquivo usando o método
response.download
.
Você terá/não terá acesso a propriedades de resposta específicas com base no tipo de resposta.
Lidando com uma resposta padrão
Ao mutar uma resposta padrão, você pode acessá-la usando a propriedade response.content
. Certifique-se primeiro de verificar se o content
existe ou não.
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasContent) {
console.log(response.content)
console.log(typeof response.content)
response.send(newResponse)
}
}
}
Lidando com uma resposta de streaming
Os fluxos de resposta definidos usando o método response.stream
não são imediatamente canalizados para a resposta HTTP de saída. Em vez disso, o AdonisJS aguarda o manipulador de rota e o pipeline do middleware terminarem.
Como resultado, dentro de um middleware, você pode substituir o fluxo existente por um novo fluxo ou definir manipuladores de eventos para monitorar o fluxo.
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasStream) {
response.outgoingStream.on('data', (chunk) => {
console.log(chunk)
})
}
}
}
Lidando com downloads de arquivos
Os downloads de arquivos realizados usando os métodos response.download
e response.attachment
adiam o processo de download até que o manipulador de rotas e o pipeline do middleware terminem.
Como resultado, dentro de um middleware, você pode substituir o caminho para o arquivo ser baixado.
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
export default class {
async handle({ response }: HttpContext, next: NextFn) {
await next()
if (response.hasFileToStream) {
console.log(response.fileToStream.generateEtag)
console.log(response.fileToStream.path)
}
}
}
Testando classes de middleware
A criação de middleware como classes permite que você teste facilmente um middleware isoladamente (também conhecido como teste de unidade de um middleware). Existem algumas maneiras diferentes de testar middleware. Vamos explorar todas as opções disponíveis.
A opção mais simples é criar uma nova instância da classe de middleware e invocar o método handle
com o contexto HTTP e a função de retorno de chamada next
.
import testUtils from '@adonisjs/core/services/test_utils'
import GeoIpService from '#services/geoip_service'
import UserLocationMiddleware from '#middleware/user_location_middleware'
const middleware = new UserLocationMiddleware(
new GeoIpService()
)
const ctx = testUtils.createHttpContext()
await middleware.handle(ctx, () => {
console.log('Next function invoked')
})
O serviço testUtils
está disponível somente após o aplicativo AdonisJS ser inicializado. No entanto, se você estiver testando um middleware dentro de um pacote, você pode usar a classe HttpContextFactory
para criar uma instância de contexto HTTP fictícia sem inicializar um aplicativo.
Teste de middleware CORS para um exemplo do mundo real.
import {
RequestFactory,
ResponseFactory,
HttpContextFactory
} from '@adonisjs/core/factories/http'
const request = new RequestFactory().create()
const response = new ResponseFactory().create()
const ctx = new HttpContextFactory()
.merge({ request, response })
.create()
await middleware.handle(ctx, () => {
console.log('Next function invoked')
})
Usando o pipeline do servidor
Se seu middleware depende de outro middleware para ser executado primeiro, você pode compor um pipeline de middleware usando o método server.pipeline
.
- O método
server.pipeline
aceita uma matriz de classes de middleware. - A instância de classe é criada usando o contêiner IoC.
- O fluxo de execução é o mesmo que o fluxo de execução original do middleware durante uma solicitação HTTP.
import testUtils from '@adonisjs/core/services/test_utils'
import server from '@adonisjs/core/services/server'
import UserLocationMiddleware from '#middleware/user_location_middleware'
const pipeline = server.pipeline([
UserLocationMiddleware
])
const ctx = testUtils.createHttpContext()
await pipeline.run(ctx)
Você pode definir as funções finalHandler
e errorHandler
antes de chamar o método pipeline.run
.
- O manipulador final é executado após todo o middleware ter sido executado. O manipulador final não é executado quando qualquer middleware encerra a cadeia sem chamar o método
next
. - O manipulador de erros é executado se um middleware gera uma exceção. O fluxo upstream será iniciado após o manipulador de erros ser invocado.
const ctx = testUtils.createHttpContext()
await pipeline
.finalHandler(() => {
console.log('all middleware called next')
console.log('the upstream logic starts from here')
})
.errorHandler((error) => {
console.log('an exception was raised')
console.log('the upstream logic starts from here')
})
.run(ctx)
console.log('pipeline executed')
O serviço server
fica disponível após o aplicativo ser inicializado. No entanto, se você estiver criando um pacote, poderá usar o ServerFactory
para criar uma instância da classe Server sem inicializar o aplicativo.
import { ServerFactory } from '@adonisjs/core/factories/http'
const server = new ServerFactory().create()
const pipeline = server.pipeline([
UserLocationMiddleware
])