fastify / fastify/fastify-nextjs

In Jest tests, when closing fastify/nextjs (app.close()), nextjs does not close

Open
#855 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
558
Forks
62
Avg merge
2d 4h
Merged PRs (30d)
6

Description

### 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

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 {
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()
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:
1. Run the test npm run test:coverage
2. The first two tests passed successfully
3. 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
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..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:
1. Run the test npm run test:coverage
2. All tests pass
3. 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_

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.