fastify-nextjs
fastify-nextjs copied to clipboard
In Jest tests, when closing fastify/nextjs (app.close()), nextjs does not close
Prerequisites
- [X] I have written a descriptive issue title
- [X] I have searched existing issues to ensure the bug has not already been reported
Fastify version
10.0.1
Plugin version
No response
Node.js version
18.18.0
Operating system
Windows
Operating system version (i.e. 20.04, 11.3, 10)
10 pro
Description
I ask for help and want to apologize if this is my mistake and not yours. Perhaps I'm doing something wrong. In Jest tests, when closing fastify/nextjs (app.close()), nextjs does not close, but continues to work. It happened a couple of times that I didn’t stop the test and after 3-5 minutes I received logs about page compilation from nextjs.
In dev and prod mode everything works correctly and without errors.
Another very important point is that my CI freezes after passing the tests. Even if I use beforeAll and afterAll in the test, then after successful completion I get a warning : "Jest did not exit one second after the test run has completed.
'This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles
to troubleshoot this issue." . I think this is due to the fact that Nextjs continues to work.
my test
import { randomUUID } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { createApp } from './app.js'
import { server, rest } from './test-utils/mswServer.js'
import { fetchRemote } from './utils/fetchRemote.js'
let app: ReturnType<typeof createApp>
beforeEach(async () => {
app = createApp({
logLevel: 'silent',
formats: ['pdf', 'docx'],
dbServer: 'http://............/db/next',
fileServer: 'http://............/file',
})
await app.listen({ port: 8800 })
})
afterEach(async () => {
await app.close()
})
it('/returns Swagger documentation', async () => {
const response = await fetchRemote('http://localhost:8800')
expect(response.headers.get('Content-Type')).toEqual(
expect.stringContaining('text/html'),
)
})
it('/extensions returns a list of supported extensions', async () => {
const response = await fetchRemote('http://localhost:8800/extensions')
const json = await response.json()
expect(json).toEqual(['pdf', 'docx'])
})
describe('/file/:fileId/:ext', () => {
it('in case the file is pdf', async () => {
const fileId = randomUUID()
server.use(
rest.get('http://file-server/:fileId', (req, res, ctx) => {
expect(req.params.fileId).toBe(fileId)
return res(ctx.text('response-body'))
}),
)
const response = await fetchRemote(`http://localhost:8800/file/${fileId}/pdf`)
expect(response.headers.get('Content-Type')).toBe('application/pdf')
const text = await response.text()
expect(text).toEqual('response-body')
})
it('in case the file is NOT pdf', async () => {
const fileId = randomUUID()
server.use(
rest.get('http://file-server/:fileId', (req, res, ctx) => {
expect(req.params.fileId).toBe(fileId)
return res(ctx.set('Content-Length', '13'), ctx.text('response-body'))
}),
rest.post('http://pdf-convert/forms/libreoffice/convert', (req, res, ctx) => {
expect(req.body).toContain('1,3')
return res(ctx.text('converted-body'))
}),
)
const response = await fetchRemote(
`http://localhost:8800/file/${fileId}/docx?pageRanges=1,3`,
)
expect(response.headers.get('Content-Type')).toBe('application/pdf')
const text = await response.text()
expect(text).toEqual('converted-body')
})
it('uses cache when requesting again', async () => {
let numCalls = 0
const fileId = randomUUID()
server.use(
rest.get('http://file-server/:fileId', (req, res, ctx) => {
expect(req.params.fileId).toBe(fileId)
numCalls++
return res(ctx.set('Content-Length', '13'), ctx.text('response-body'))
}),
rest.post('http://pdf-convert/forms/libreoffice/convert', (req, res, ctx) => {
numCalls++
return res(ctx.text('converted-body'))
}),
)
const firstResponse = await fetchRemote(`http://localhost:8800/file/${fileId}/docx`)
expect(firstResponse.headers.get('Content-Type')).toBe('application/pdf')
const firstText = await firstResponse.text()
const secondResponse = await fetchRemote(`http://localhost:8800/file/${fileId}/docx`)
expect(secondResponse.ok).toBe(true)
expect(secondResponse.headers.get('Content-Type')).toBe('application/pdf')
const secondText = await secondResponse.text()
expect(secondText).toBe(firstText)
expect(numCalls).toBe(2)
})
it('throws an error if the format is not supported', async () => {
await expect(() =>
fetchRemote('http://localhost:8800/file/abc/xls'),
).rejects.toThrowError(/Bad Request/)
})
})
my app
import { IncomingMessage, Server, ServerResponse } from 'node:http'
import path from 'node:path'
import proxy from '@fastify/http-proxy'
import fastifyNext from '@fastify/nextjs'
import fastifyStatic from '@fastify/static'
import swagger from '@fastify/swagger'
import swaggerUi from '@fastify/swagger-ui'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import Fastify, { FastifyInstance } from 'fastify'
import pino, { P } from 'pino'
import { routes } from './routes.js'
const PUBLIC_PATH = path.join(process.cwd(), 'public')
export function createApp({
logLevel,
basePath = '',
formats,
dbServer,
fileServer,
}: {
logLevel?: P.LevelWithSilent
basePath?: string
formats: string[]
dbServer: string
fileServer: string
}): FastifyInstance<Server, IncomingMessage, ServerResponse, P.Logger> {
const fastify = Fastify({
trustProxy: true,
requestIdHeader: 'x-request-id',
requestIdLogLabel: 'requestId',
logger: pino({
base: null,
timestamp: false,
level: logLevel ?? (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
transport:
process.env.NODE_ENV !== 'production'
? {
target: 'pino-pretty',
options: { colorize: true },
}
: undefined,
}),
}).withTypeProvider<TypeBoxTypeProvider>()
fastify
.register(fastifyNext, { dev: process.env.NODE_ENV !== 'production' })
.after(() => {
fastify.next('/web/viewer-new')
})
fastify.register(fastifyStatic, { root: PUBLIC_PATH, wildcard: false })
fastify.register(proxy, {
upstream: dbServer,
prefix: '/api/proxy/db/next',
httpMethods: ['POST'],
})
fastify.register(proxy, {
upstream: fileServer,
prefix: '/api/proxy/file',
httpMethods: ['GET', 'POST'],
})
fastify.register(swagger, {
mode: 'dynamic',
openapi: {
info: {
title: 'pdf-viewer-server',
description: 'Server for displaying office documents as HTML',
version: '0.1.0',
},
servers: [{ url: basePath !== '' ? basePath : '/' }],
},
})
fastify.register(swaggerUi, {
routePrefix: '/documentation',
})
fastify.register(routes, { basePath, formats })
return fastify
}
my index
import { createApp } from './app.js'
const {
OFFICE_FORMATS = 'txt,rtf,doc,docx,odt,xls,xlsx',
BASE_PATH,
PORT = '8800',
NODE_ENV,
DB_SERVER_URL,
FILE_SERVER_URL,
} = process.env
const formatsSet = new Set(['pdf', ...OFFICE_FORMATS.split(',')])
const fastify = createApp({
basePath: BASE_PATH,
formats: [...formatsSet],
dbServer: DB_SERVER_URL!,
fileServer: FILE_SERVER_URL,
})
const ALL_AVAILABLE_IPV4_INTERFACES = '0.0.0.0'
await fastify.listen({
port: Number(PORT),
host: ALL_AVAILABLE_IPV4_INTERFACES,
})
fastify.log.info(`Listening on port ${PORT}. Mode: ${NODE_ENV}`)
Steps to Reproduce
If in the beforeEach and afterEach test:
- Run the test npm run test:coverage
- The first two tests passed successfully
- Other tests fail with errors
thrown: "Exceeded timeout of 5000 ms for a hook.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
19 | })
20 |
> 21 | afterEach(async () => {
| ^
22 | await app.close()
23 | })
24 |
at afterEach (src/app.test.ts:21:1)
thrown: "Exceeded timeout of 5000 ms for a hook.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
8 | let app: ReturnType<typeof createApp>
9 |
> 10 | beforeEach(async () => {
| ^
11 | app = createApp({
12 | logLevel: 'silent',
13 | formats: ['pdf', 'docx'],
at beforeEach (src/app.test.ts:10:1)
TypeError: fastify.next is not a function
47 | .register(fastifyNext, { dev: process.env.NODE_ENV !== 'production' })
48 | .after(() => {
> 49 | fastify.next('/web/viewer-new')
| ^
50 | })
51 |
52 | fastify.register(fastifyStatic, { root: PUBLIC_PATH, wildcard: false })
at next (src/app.ts:49:12)
at Object._encapsulateThreeParam (node_modules/avvio/boot.js:544:13)
at Boot.timeoutCall (node_modules/avvio/boot.js:458:5)
at Boot.callWithCbOrNextTick (node_modules/avvio/boot.js:440:19)
at Boot._after (node_modules/avvio/boot.js:280:26)
at Plugin.Object.<anonymous>.Plugin.exec (node_modules/avvio/plugin.js:130:19)
at Boot.loadPlugin (node_modules/avvio/plugin.js:272:10)
If in the beforeAll and afterAll test:
- Run the test npm run test:coverage
- All tests pass
- I get a warning warning: "Jest did not exit one second after the test run has completed.
'This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles
to troubleshoot this issue."
But in this case CI does not work.
Expected Behavior
No response