fastify 4.27.0 → 4.28.1
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/docs/Reference/Reply.md +6 -6
- package/docs/Reference/Warnings.md +2 -0
- package/fastify.js +1 -1
- package/lib/error-serializer.js +75 -18
- package/lib/reply.js +10 -4
- package/lib/route.js +5 -1
- package/lib/server.js +18 -8
- package/lib/warnings.js +6 -0
- package/package.json +1 -1
- package/test/close-pipelining.test.js +11 -4
- package/test/has-route.test.js +17 -1
- package/test/internals/reply.test.js +33 -3
- package/test/listen.5.test.js +99 -0
- package/test/types/import.js +2 -2
- package/test/types/reply.test-d.ts +1 -1
- package/test/types/type-provider.test-d.ts +2 -2
- package/types/reply.d.ts +4 -2
- package/types/route.d.ts +26 -26
- package/types/utils.d.ts +5 -0
- package/EXPENSE_POLICY.md +0 -105
package/docs/Reference/Reply.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
- [.trailer(key, function)](#trailerkey-function)
|
|
18
18
|
- [.hasTrailer(key)](#hastrailerkey)
|
|
19
19
|
- [.removeTrailer(key)](#removetrailerkey)
|
|
20
|
-
- [.redirect([code ,]
|
|
20
|
+
- [.redirect(dest, [code ,])](#redirectdest--code)
|
|
21
21
|
- [.callNotFound()](#callnotfound)
|
|
22
22
|
- [.getResponseTime()](#getresponsetime)
|
|
23
23
|
- [.type(contentType)](#typecontenttype)
|
|
@@ -62,8 +62,8 @@ since the request was received by Fastify.
|
|
|
62
62
|
- `.hasTrailer(key)` - Determine if a trailer has been set.
|
|
63
63
|
- `.removeTrailer(key)` - Remove the value of a previously set trailer.
|
|
64
64
|
- `.type(value)` - Sets the header `Content-Type`.
|
|
65
|
-
- `.redirect([code,]
|
|
66
|
-
optional (
|
|
65
|
+
- `.redirect(dest, [code,])` - Redirect to the specified URL, the status code is
|
|
66
|
+
optional (defaults to `302`).
|
|
67
67
|
- `.callNotFound()` - Invokes the custom not found handler.
|
|
68
68
|
- `.serialize(payload)` - Serializes the specified payload using the default
|
|
69
69
|
JSON serializer or using the custom serializer (if one is set) and returns the
|
|
@@ -299,7 +299,7 @@ reply.getTrailer('server-timing') // undefined
|
|
|
299
299
|
```
|
|
300
300
|
|
|
301
301
|
|
|
302
|
-
### .redirect([code ,]
|
|
302
|
+
### .redirect(dest, [code ,])
|
|
303
303
|
<a id="redirect"></a>
|
|
304
304
|
|
|
305
305
|
Redirects a request to the specified URL, the status code is optional, default
|
|
@@ -320,7 +320,7 @@ reply.redirect('/home')
|
|
|
320
320
|
Example (no `reply.code()` call) sets status code to `303` and redirects to
|
|
321
321
|
`/home`
|
|
322
322
|
```js
|
|
323
|
-
reply.redirect(
|
|
323
|
+
reply.redirect('/home', 303)
|
|
324
324
|
```
|
|
325
325
|
|
|
326
326
|
Example (`reply.code()` call) sets status code to `303` and redirects to `/home`
|
|
@@ -330,7 +330,7 @@ reply.code(303).redirect('/home')
|
|
|
330
330
|
|
|
331
331
|
Example (`reply.code()` call) sets status code to `302` and redirects to `/home`
|
|
332
332
|
```js
|
|
333
|
-
reply.code(303).redirect(
|
|
333
|
+
reply.code(303).redirect('/home', 302)
|
|
334
334
|
```
|
|
335
335
|
|
|
336
336
|
### .callNotFound()
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- [FSTDEP018](#FSTDEP018)
|
|
25
25
|
- [FSTDEP019](#FSTDEP019)
|
|
26
26
|
- [FSTDEP020](#FSTDEP020)
|
|
27
|
+
- [FSTDEP021](#FSTDEP021)
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
## Warnings
|
|
@@ -90,3 +91,4 @@ Deprecation codes are further supported by the Node.js CLI options:
|
|
|
90
91
|
| <a id="FSTDEP018">FSTDEP018</a> | You are accessing the deprecated `request.routerMethod` property. | Use `request.routeOptions.method`. | [#4470](https://github.com/fastify/fastify/pull/4470) |
|
|
91
92
|
| <a id="FSTDEP019">FSTDEP019</a> | You are accessing the deprecated `reply.context` property. | Use `reply.routeOptions.config` or `reply.routeOptions.schema`. | [#5032](https://github.com/fastify/fastify/pull/5032) [#5084](https://github.com/fastify/fastify/pull/5084) |
|
|
92
93
|
| <a id="FSTDEP020">FSTDEP020</a> | You are using the deprecated `reply.getReponseTime()` method. | Use the `reply.elapsedTime` property instead. | [#5263](https://github.com/fastify/fastify/pull/5263) |
|
|
94
|
+
| <a id="FSTDEP021">FSTDEP021</a> | The `reply.redirect()` method has a new signature: `reply.redirect(url: string, code?: number)`. It will be enforced in `fastify@v5`'. | [#5483](https://github.com/fastify/fastify/pull/5483) |
|
package/fastify.js
CHANGED
package/lib/error-serializer.js
CHANGED
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
/* istanbul ignore file */
|
|
3
3
|
|
|
4
4
|
'use strict'
|
|
5
|
-
const { dependencies } = require('fast-json-stringify/lib/standalone')
|
|
6
|
-
|
|
7
|
-
const { Serializer, Validator } = dependencies
|
|
8
5
|
|
|
6
|
+
const Serializer = require('fast-json-stringify/lib/serializer')
|
|
9
7
|
const serializerState = {"mode":"standalone"}
|
|
10
8
|
const serializer = Serializer.restoreFromState(serializerState)
|
|
11
9
|
|
|
@@ -15,6 +13,18 @@
|
|
|
15
13
|
module.exports = function anonymous(validator,serializer
|
|
16
14
|
) {
|
|
17
15
|
|
|
16
|
+
const JSON_STR_BEGIN_OBJECT = '{'
|
|
17
|
+
const JSON_STR_END_OBJECT = '}'
|
|
18
|
+
const JSON_STR_BEGIN_ARRAY = '['
|
|
19
|
+
const JSON_STR_END_ARRAY = ']'
|
|
20
|
+
const JSON_STR_COMMA = ','
|
|
21
|
+
const JSON_STR_COLONS = ':'
|
|
22
|
+
const JSON_STR_QUOTE = '"'
|
|
23
|
+
const JSON_STR_EMPTY_OBJECT = JSON_STR_BEGIN_OBJECT + JSON_STR_END_OBJECT
|
|
24
|
+
const JSON_STR_EMPTY_ARRAY = JSON_STR_BEGIN_ARRAY + JSON_STR_END_ARRAY
|
|
25
|
+
const JSON_STR_EMPTY_STRING = JSON_STR_QUOTE + JSON_STR_QUOTE
|
|
26
|
+
const JSON_STR_NULL = 'null'
|
|
27
|
+
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
// #
|
|
@@ -23,36 +33,83 @@
|
|
|
23
33
|
? input.toJSON()
|
|
24
34
|
: input
|
|
25
35
|
|
|
26
|
-
if (obj === null) return
|
|
36
|
+
if (obj === null) return JSON_STR_EMPTY_OBJECT
|
|
27
37
|
|
|
28
|
-
let
|
|
38
|
+
let value
|
|
39
|
+
let json = JSON_STR_BEGIN_OBJECT
|
|
29
40
|
let addComma = false
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
value = obj["statusCode"]
|
|
43
|
+
if (value !== undefined) {
|
|
44
|
+
!addComma && (addComma = true) || (json += JSON_STR_COMMA)
|
|
33
45
|
json += "\"statusCode\":"
|
|
34
|
-
json += serializer.asNumber(
|
|
46
|
+
json += serializer.asNumber(value)
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
value = obj["code"]
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
!addComma && (addComma = true) || (json += JSON_STR_COMMA)
|
|
39
52
|
json += "\"code\":"
|
|
40
|
-
|
|
53
|
+
|
|
54
|
+
if (typeof value !== 'string') {
|
|
55
|
+
if (value === null) {
|
|
56
|
+
json += JSON_STR_EMPTY_STRING
|
|
57
|
+
} else if (value instanceof Date) {
|
|
58
|
+
json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
|
|
59
|
+
} else if (value instanceof RegExp) {
|
|
60
|
+
json += serializer.asString(value.source)
|
|
61
|
+
} else {
|
|
62
|
+
json += serializer.asString(value.toString())
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
json += serializer.asString(value)
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
}
|
|
42
69
|
|
|
43
|
-
|
|
44
|
-
|
|
70
|
+
value = obj["error"]
|
|
71
|
+
if (value !== undefined) {
|
|
72
|
+
!addComma && (addComma = true) || (json += JSON_STR_COMMA)
|
|
45
73
|
json += "\"error\":"
|
|
46
|
-
|
|
74
|
+
|
|
75
|
+
if (typeof value !== 'string') {
|
|
76
|
+
if (value === null) {
|
|
77
|
+
json += JSON_STR_EMPTY_STRING
|
|
78
|
+
} else if (value instanceof Date) {
|
|
79
|
+
json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
|
|
80
|
+
} else if (value instanceof RegExp) {
|
|
81
|
+
json += serializer.asString(value.source)
|
|
82
|
+
} else {
|
|
83
|
+
json += serializer.asString(value.toString())
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
json += serializer.asString(value)
|
|
87
|
+
}
|
|
88
|
+
|
|
47
89
|
}
|
|
48
90
|
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
value = obj["message"]
|
|
92
|
+
if (value !== undefined) {
|
|
93
|
+
!addComma && (addComma = true) || (json += JSON_STR_COMMA)
|
|
51
94
|
json += "\"message\":"
|
|
52
|
-
|
|
95
|
+
|
|
96
|
+
if (typeof value !== 'string') {
|
|
97
|
+
if (value === null) {
|
|
98
|
+
json += JSON_STR_EMPTY_STRING
|
|
99
|
+
} else if (value instanceof Date) {
|
|
100
|
+
json += JSON_STR_QUOTE + value.toISOString() + JSON_STR_QUOTE
|
|
101
|
+
} else if (value instanceof RegExp) {
|
|
102
|
+
json += serializer.asString(value.source)
|
|
103
|
+
} else {
|
|
104
|
+
json += serializer.asString(value.toString())
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
json += serializer.asString(value)
|
|
108
|
+
}
|
|
109
|
+
|
|
53
110
|
}
|
|
54
111
|
|
|
55
|
-
return json +
|
|
112
|
+
return json + JSON_STR_END_OBJECT
|
|
56
113
|
|
|
57
114
|
}
|
|
58
115
|
|
package/lib/reply.js
CHANGED
|
@@ -55,7 +55,7 @@ const {
|
|
|
55
55
|
FST_ERR_MISSING_SERIALIZATION_FN,
|
|
56
56
|
FST_ERR_MISSING_CONTENTTYPE_SERIALIZATION_FN
|
|
57
57
|
} = require('./errors')
|
|
58
|
-
const { FSTDEP010, FSTDEP013, FSTDEP019, FSTDEP020 } = require('./warnings')
|
|
58
|
+
const { FSTDEP010, FSTDEP013, FSTDEP019, FSTDEP020, FSTDEP021 } = require('./warnings')
|
|
59
59
|
|
|
60
60
|
const toString = Object.prototype.toString
|
|
61
61
|
|
|
@@ -457,9 +457,15 @@ Reply.prototype.type = function (type) {
|
|
|
457
457
|
return this
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
-
Reply.prototype.redirect = function (
|
|
461
|
-
if (typeof
|
|
462
|
-
|
|
460
|
+
Reply.prototype.redirect = function (url, code) {
|
|
461
|
+
if (typeof url === 'number') {
|
|
462
|
+
FSTDEP021()
|
|
463
|
+
const temp = code
|
|
464
|
+
code = url
|
|
465
|
+
url = temp
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!code) {
|
|
463
469
|
code = this[kReplyHasStatusCode] ? this.raw.statusCode : 302
|
|
464
470
|
}
|
|
465
471
|
|
package/lib/route.js
CHANGED
|
@@ -168,7 +168,11 @@ function buildRouting (options) {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function hasRoute ({ options }) {
|
|
171
|
-
|
|
171
|
+
const normalizedMethod = options.method?.toUpperCase() ?? ''
|
|
172
|
+
return findRoute({
|
|
173
|
+
...options,
|
|
174
|
+
method: normalizedMethod
|
|
175
|
+
}) !== null
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
function findRoute (options) {
|
package/lib/server.js
CHANGED
|
@@ -220,6 +220,7 @@ function multipleBindings (mainServer, httpHandler, serverOpts, listenOptions, o
|
|
|
220
220
|
function listenCallback (server, listenOptions) {
|
|
221
221
|
const wrap = (err) => {
|
|
222
222
|
server.removeListener('error', wrap)
|
|
223
|
+
server.removeListener('listening', wrap)
|
|
223
224
|
if (!err) {
|
|
224
225
|
const address = logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText)
|
|
225
226
|
listenOptions.cb(null, address)
|
|
@@ -240,7 +241,8 @@ function listenCallback (server, listenOptions) {
|
|
|
240
241
|
|
|
241
242
|
server.once('error', wrap)
|
|
242
243
|
if (!this[kState].closing) {
|
|
243
|
-
server.
|
|
244
|
+
server.once('listening', wrap)
|
|
245
|
+
server.listen(listenOptions)
|
|
244
246
|
this[kState].listening = true
|
|
245
247
|
}
|
|
246
248
|
}
|
|
@@ -255,25 +257,33 @@ function listenPromise (server, listenOptions) {
|
|
|
255
257
|
|
|
256
258
|
return this.ready().then(() => {
|
|
257
259
|
let errEventHandler
|
|
260
|
+
let listeningEventHandler
|
|
261
|
+
function cleanup () {
|
|
262
|
+
server.removeListener('error', errEventHandler)
|
|
263
|
+
server.removeListener('listening', listeningEventHandler)
|
|
264
|
+
}
|
|
258
265
|
const errEvent = new Promise((resolve, reject) => {
|
|
259
266
|
errEventHandler = (err) => {
|
|
267
|
+
cleanup()
|
|
260
268
|
this[kState].listening = false
|
|
261
269
|
reject(err)
|
|
262
270
|
}
|
|
263
271
|
server.once('error', errEventHandler)
|
|
264
272
|
})
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
const listeningEvent = new Promise((resolve, reject) => {
|
|
274
|
+
listeningEventHandler = () => {
|
|
275
|
+
cleanup()
|
|
276
|
+
this[kState].listening = true
|
|
268
277
|
resolve(logServerAddress.call(this, server, listenOptions.listenTextResolver || defaultResolveServerListeningText))
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
this[kState].listening = true
|
|
278
|
+
}
|
|
279
|
+
server.once('listening', listeningEventHandler)
|
|
272
280
|
})
|
|
273
281
|
|
|
282
|
+
server.listen(listenOptions)
|
|
283
|
+
|
|
274
284
|
return Promise.race([
|
|
275
285
|
errEvent, // e.g invalid port range error is always emitted before the server listening
|
|
276
|
-
|
|
286
|
+
listeningEvent
|
|
277
287
|
])
|
|
278
288
|
})
|
|
279
289
|
}
|
package/lib/warnings.js
CHANGED
|
@@ -82,6 +82,11 @@ const FSTDEP020 = createDeprecation({
|
|
|
82
82
|
message: 'You are using the deprecated "reply.getResponseTime()" method. Use the "reply.elapsedTime" property instead. Method "reply.getResponseTime()" will be removed in `fastify@5`.'
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
const FSTDEP021 = createDeprecation({
|
|
86
|
+
code: 'FSTDEP021',
|
|
87
|
+
message: 'The `reply.redirect()` method has a new signature: `reply.redirect(url: string, code?: number)`. It will be enforced in `fastify@v5`'
|
|
88
|
+
})
|
|
89
|
+
|
|
85
90
|
const FSTWRN001 = createWarning({
|
|
86
91
|
name: 'FastifyWarning',
|
|
87
92
|
code: 'FSTWRN001',
|
|
@@ -113,6 +118,7 @@ module.exports = {
|
|
|
113
118
|
FSTDEP018,
|
|
114
119
|
FSTDEP019,
|
|
115
120
|
FSTDEP020,
|
|
121
|
+
FSTDEP021,
|
|
116
122
|
FSTWRN001,
|
|
117
123
|
FSTWRN002
|
|
118
124
|
}
|
package/package.json
CHANGED
|
@@ -36,8 +36,15 @@ test('Should return 503 while closing - pipelining', async t => {
|
|
|
36
36
|
await instance.close()
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// default enable of idle closing idle connection is accidentally backported to 18.19.0 and fixed in 18.20.3
|
|
40
|
+
// Refs: https://github.com/nodejs/node/releases/tag/v18.20.3
|
|
41
|
+
const isNodeDefaultClosingIdleConnection =
|
|
42
|
+
(
|
|
43
|
+
semver.gte(process.version, '18.19.0') &&
|
|
44
|
+
semver.lt(process.version, '18.20.3')
|
|
45
|
+
) ||
|
|
46
|
+
semver.gte(process.version, '19.0.0')
|
|
47
|
+
test('Should not return 503 while closing - pipelining - return503OnClosing: false, skip when Node default closing idle connection', { skip: isNodeDefaultClosingIdleConnection }, async t => {
|
|
41
48
|
const fastify = Fastify({
|
|
42
49
|
return503OnClosing: false,
|
|
43
50
|
forceCloseConnections: false
|
|
@@ -67,8 +74,8 @@ test('Should not return 503 while closing - pipelining - return503OnClosing: fal
|
|
|
67
74
|
await instance.close()
|
|
68
75
|
})
|
|
69
76
|
|
|
70
|
-
test('Should close the socket abruptly - pipelining - return503OnClosing: false, skip Node
|
|
71
|
-
// Since Node
|
|
77
|
+
test('Should close the socket abruptly - pipelining - return503OnClosing: false, skip when Node not default closing idle connection', { skip: !isNodeDefaultClosingIdleConnection }, async t => {
|
|
78
|
+
// Since Node v19, we will always invoke server.closeIdleConnections()
|
|
72
79
|
// therefore our socket will be closed
|
|
73
80
|
const fastify = Fastify({
|
|
74
81
|
return503OnClosing: false,
|
package/test/has-route.test.js
CHANGED
|
@@ -5,7 +5,7 @@ const test = t.test
|
|
|
5
5
|
const Fastify = require('../fastify')
|
|
6
6
|
|
|
7
7
|
test('hasRoute', t => {
|
|
8
|
-
t.plan(
|
|
8
|
+
t.plan(5)
|
|
9
9
|
const test = t.test
|
|
10
10
|
const fastify = Fastify()
|
|
11
11
|
|
|
@@ -74,4 +74,20 @@ test('hasRoute', t => {
|
|
|
74
74
|
url: '/example/12345.png'
|
|
75
75
|
}), true)
|
|
76
76
|
})
|
|
77
|
+
|
|
78
|
+
test('hasRoute - finds a route even if method is not uppercased', t => {
|
|
79
|
+
t.plan(1)
|
|
80
|
+
fastify.route({
|
|
81
|
+
method: 'GET',
|
|
82
|
+
url: '/equal',
|
|
83
|
+
handler: function (req, reply) {
|
|
84
|
+
reply.send({ hello: 'world' })
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
t.equal(fastify.hasRoute({
|
|
89
|
+
method: 'get',
|
|
90
|
+
url: '/equal'
|
|
91
|
+
}), true)
|
|
92
|
+
})
|
|
77
93
|
})
|
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
} = require('../../lib/symbols')
|
|
20
20
|
const fs = require('node:fs')
|
|
21
21
|
const path = require('node:path')
|
|
22
|
-
const { FSTDEP010, FSTDEP019, FSTDEP020 } = require('../../lib/warnings')
|
|
22
|
+
const { FSTDEP010, FSTDEP019, FSTDEP020, FSTDEP021 } = require('../../lib/warnings')
|
|
23
23
|
|
|
24
24
|
const agent = new http.Agent({ keepAlive: false })
|
|
25
25
|
|
|
@@ -250,7 +250,7 @@ test('within an instance', t => {
|
|
|
250
250
|
})
|
|
251
251
|
|
|
252
252
|
fastify.get('/redirect-code', function (req, reply) {
|
|
253
|
-
reply.redirect(
|
|
253
|
+
reply.redirect('/', 301)
|
|
254
254
|
})
|
|
255
255
|
|
|
256
256
|
fastify.get('/redirect-code-before-call', function (req, reply) {
|
|
@@ -258,7 +258,7 @@ test('within an instance', t => {
|
|
|
258
258
|
})
|
|
259
259
|
|
|
260
260
|
fastify.get('/redirect-code-before-call-overwrite', function (req, reply) {
|
|
261
|
-
reply.code(307).redirect(
|
|
261
|
+
reply.code(307).redirect('/', 302)
|
|
262
262
|
})
|
|
263
263
|
|
|
264
264
|
fastify.get('/custom-serializer', function (req, reply) {
|
|
@@ -2094,6 +2094,36 @@ test('redirect to an invalid URL should not crash the server', async t => {
|
|
|
2094
2094
|
await fastify.close()
|
|
2095
2095
|
})
|
|
2096
2096
|
|
|
2097
|
+
test('redirect with deprecated signature should warn', t => {
|
|
2098
|
+
t.plan(4)
|
|
2099
|
+
|
|
2100
|
+
process.removeAllListeners('warning')
|
|
2101
|
+
process.on('warning', onWarning)
|
|
2102
|
+
function onWarning (warning) {
|
|
2103
|
+
t.equal(warning.name, 'DeprecationWarning')
|
|
2104
|
+
t.equal(warning.code, FSTDEP021.code)
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
const fastify = Fastify()
|
|
2108
|
+
|
|
2109
|
+
fastify.get('/', (req, reply) => {
|
|
2110
|
+
reply.redirect(302, '/new')
|
|
2111
|
+
})
|
|
2112
|
+
|
|
2113
|
+
fastify.get('/new', (req, reply) => {
|
|
2114
|
+
reply.send('new')
|
|
2115
|
+
})
|
|
2116
|
+
|
|
2117
|
+
fastify.inject({ method: 'GET', url: '/' }, (err, res) => {
|
|
2118
|
+
t.error(err)
|
|
2119
|
+
t.pass()
|
|
2120
|
+
|
|
2121
|
+
process.removeListener('warning', onWarning)
|
|
2122
|
+
})
|
|
2123
|
+
|
|
2124
|
+
FSTDEP021.emitted = false
|
|
2125
|
+
})
|
|
2126
|
+
|
|
2097
2127
|
test('invalid response headers should not crash the server', async t => {
|
|
2098
2128
|
const fastify = Fastify()
|
|
2099
2129
|
fastify.route({
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { test } = require('tap')
|
|
4
|
+
const net = require('node:net')
|
|
5
|
+
const Fastify = require('../fastify')
|
|
6
|
+
const { once } = require('node:events')
|
|
7
|
+
|
|
8
|
+
function createDeferredPromise () {
|
|
9
|
+
const promise = {}
|
|
10
|
+
promise.promise = new Promise((resolve) => {
|
|
11
|
+
promise.resolve = resolve
|
|
12
|
+
})
|
|
13
|
+
return promise
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('same port conflict and success should not fire callback multiple times - callback', async (t) => {
|
|
17
|
+
t.plan(7)
|
|
18
|
+
const server = net.createServer()
|
|
19
|
+
server.listen({ port: 0, host: '127.0.0.1' })
|
|
20
|
+
await once(server, 'listening')
|
|
21
|
+
const option = { port: server.address().port, host: server.address().address }
|
|
22
|
+
let count = 0
|
|
23
|
+
const fastify = Fastify()
|
|
24
|
+
const promise = createDeferredPromise()
|
|
25
|
+
function callback (err) {
|
|
26
|
+
switch (count) {
|
|
27
|
+
case 6: {
|
|
28
|
+
// success in here
|
|
29
|
+
t.error(err)
|
|
30
|
+
fastify.close((err) => {
|
|
31
|
+
t.error(err)
|
|
32
|
+
promise.resolve()
|
|
33
|
+
})
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
case 5: {
|
|
37
|
+
server.close()
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
fastify.listen(option, callback)
|
|
40
|
+
}, 100)
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
default: {
|
|
44
|
+
// expect error
|
|
45
|
+
t.equal(err.code, 'EADDRINUSE')
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
fastify.listen(option, callback)
|
|
48
|
+
}, 100)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
count++
|
|
52
|
+
}
|
|
53
|
+
fastify.listen(option, callback)
|
|
54
|
+
await promise.promise
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('same port conflict and success should not fire callback multiple times - promise', async (t) => {
|
|
58
|
+
t.plan(5)
|
|
59
|
+
const server = net.createServer()
|
|
60
|
+
server.listen({ port: 0, host: '127.0.0.1' })
|
|
61
|
+
await once(server, 'listening')
|
|
62
|
+
const option = { port: server.address().port, host: server.address().address }
|
|
63
|
+
const fastify = Fastify()
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await fastify.listen(option)
|
|
67
|
+
} catch (err) {
|
|
68
|
+
t.equal(err.code, 'EADDRINUSE')
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await fastify.listen(option)
|
|
72
|
+
} catch (err) {
|
|
73
|
+
t.equal(err.code, 'EADDRINUSE')
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
await fastify.listen(option)
|
|
77
|
+
} catch (err) {
|
|
78
|
+
t.equal(err.code, 'EADDRINUSE')
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
await fastify.listen(option)
|
|
82
|
+
} catch (err) {
|
|
83
|
+
t.equal(err.code, 'EADDRINUSE')
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
await fastify.listen(option)
|
|
87
|
+
} catch (err) {
|
|
88
|
+
t.equal(err.code, 'EADDRINUSE')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
server.close()
|
|
92
|
+
|
|
93
|
+
await once(server, 'close')
|
|
94
|
+
|
|
95
|
+
// when ever we can listen, and close properly
|
|
96
|
+
// which means there is no problem on the callback
|
|
97
|
+
await fastify.listen()
|
|
98
|
+
await fastify.close()
|
|
99
|
+
})
|
package/test/types/import.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports,
|
|
1
|
+
'use strict'
|
|
2
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
@@ -29,7 +29,7 @@ const getHandler: RouteHandlerMethod = function (_request, reply) {
|
|
|
29
29
|
expectAssignable<() => { [key: string]: number | string | string[] | undefined }>(reply.getHeaders)
|
|
30
30
|
expectAssignable<(key: string) => FastifyReply>(reply.removeHeader)
|
|
31
31
|
expectAssignable<(key: string) => boolean>(reply.hasHeader)
|
|
32
|
-
expectType<{(statusCode: number, url: string): FastifyReply;
|
|
32
|
+
expectType<{(statusCode: number, url: string): FastifyReply;(url: string, statusCode?: number): FastifyReply;}>(reply.redirect)
|
|
33
33
|
expectType<() => FastifyReply>(reply.hijack)
|
|
34
34
|
expectType<() => void>(reply.callNotFound)
|
|
35
35
|
// Test reply.getResponseTime() deprecation
|
|
@@ -1052,7 +1052,7 @@ expectAssignable(server.withTypeProvider<InlineHandlerProvider>().get(
|
|
|
1052
1052
|
// Handlers: Auxiliary
|
|
1053
1053
|
// -------------------------------------------------------------------
|
|
1054
1054
|
|
|
1055
|
-
interface AuxiliaryHandlerProvider extends FastifyTypeProvider { output: '
|
|
1055
|
+
interface AuxiliaryHandlerProvider extends FastifyTypeProvider { output: this['input'] }
|
|
1056
1056
|
|
|
1057
1057
|
// Auxiliary handlers are likely shared for multiple routes and thus should infer as unknown due to potential varying parameters
|
|
1058
1058
|
function auxiliaryHandler (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction): void {
|
|
@@ -1063,7 +1063,7 @@ expectAssignable(server.withTypeProvider<AuxiliaryHandlerProvider>().get(
|
|
|
1063
1063
|
'/',
|
|
1064
1064
|
{
|
|
1065
1065
|
onRequest: auxiliaryHandler,
|
|
1066
|
-
schema: { body:
|
|
1066
|
+
schema: { body: 'handler-auxiliary' }
|
|
1067
1067
|
},
|
|
1068
1068
|
(req) => {
|
|
1069
1069
|
expectType<'handler-auxiliary'>(req.body)
|
package/types/reply.d.ts
CHANGED
|
@@ -57,9 +57,11 @@ export interface FastifyReply<
|
|
|
57
57
|
getHeaders(): Record<HttpHeader, number | string | string[] | undefined>;
|
|
58
58
|
removeHeader(key: HttpHeader): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
|
|
59
59
|
hasHeader(key: HttpHeader): boolean;
|
|
60
|
-
|
|
60
|
+
/**
|
|
61
|
+
* @deprecated The `reply.redirect()` method has a new signature: `reply.reply.redirect(url: string, code?: number)`. It will be enforced in `fastify@v5`'.
|
|
62
|
+
*/
|
|
61
63
|
redirect(statusCode: number, url: string): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
|
|
62
|
-
redirect(url: string): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
|
|
64
|
+
redirect(url: string, statusCode?: number): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
|
|
63
65
|
hijack(): FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider>;
|
|
64
66
|
callNotFound(): void;
|
|
65
67
|
/**
|
package/types/route.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
FastifyTypeProviderDefault,
|
|
13
13
|
ResolveFastifyReplyReturnType
|
|
14
14
|
} from './type-provider'
|
|
15
|
-
import { ContextConfigDefault, HTTPMethods, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerBase, RawServerDefault } from './utils'
|
|
15
|
+
import { ContextConfigDefault, HTTPMethods, NoInferCompat, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerBase, RawServerDefault } from './utils'
|
|
16
16
|
|
|
17
17
|
export interface FastifyRouteConfig {
|
|
18
18
|
url: string;
|
|
@@ -48,8 +48,8 @@ export interface RouteShorthandOptions<
|
|
|
48
48
|
attachValidation?: boolean;
|
|
49
49
|
exposeHeadRoute?: boolean;
|
|
50
50
|
|
|
51
|
-
validatorCompiler?: FastifySchemaCompiler<SchemaCompiler
|
|
52
|
-
serializerCompiler?: FastifySerializerCompiler<SchemaCompiler
|
|
51
|
+
validatorCompiler?: FastifySchemaCompiler<NoInferCompat<SchemaCompiler>>;
|
|
52
|
+
serializerCompiler?: FastifySerializerCompiler<NoInferCompat<SchemaCompiler>>;
|
|
53
53
|
bodyLimit?: number;
|
|
54
54
|
logLevel?: LogLevel;
|
|
55
55
|
config?: Omit<FastifyRequestContext<ContextConfig>['config'], 'url' | 'method'>;
|
|
@@ -59,33 +59,33 @@ export interface RouteShorthandOptions<
|
|
|
59
59
|
errorHandler?: (
|
|
60
60
|
this: FastifyInstance<RawServer, RawRequest, RawReply, Logger, TypeProvider>,
|
|
61
61
|
error: FastifyError,
|
|
62
|
-
request: FastifyRequest<RouteGeneric, RawServer, RawRequest, SchemaCompiler
|
|
63
|
-
reply: FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
62
|
+
request: FastifyRequest<RouteGeneric, RawServer, RawRequest, NoInferCompat<SchemaCompiler>, TypeProvider, ContextConfig, Logger>,
|
|
63
|
+
reply: FastifyReply<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider>
|
|
64
64
|
) => void;
|
|
65
65
|
childLoggerFactory?: FastifyChildLoggerFactory<RawServer, RawRequest, RawReply, Logger, TypeProvider>;
|
|
66
66
|
schemaErrorFormatter?: SchemaErrorFormatter;
|
|
67
67
|
|
|
68
68
|
// hooks
|
|
69
|
-
onRequest?: onRequestMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
70
|
-
| onRequestMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
71
|
-
preParsing?: preParsingMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
72
|
-
| preParsingMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
73
|
-
preValidation?: preValidationMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
74
|
-
| preValidationMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
75
|
-
preHandler?: preHandlerMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
76
|
-
| preHandlerMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
77
|
-
preSerialization?: preSerializationMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
78
|
-
| preSerializationMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
79
|
-
onSend?: onSendMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
80
|
-
| onSendMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
81
|
-
onResponse?: onResponseMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
82
|
-
| onResponseMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
83
|
-
onTimeout?: onTimeoutMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
84
|
-
| onTimeoutMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
85
|
-
onError?: onErrorMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, FastifyError, SchemaCompiler
|
|
86
|
-
| onErrorMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, FastifyError, SchemaCompiler
|
|
87
|
-
onRequestAbort?: onRequestAbortMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
88
|
-
| onRequestAbortMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
69
|
+
onRequest?: onRequestMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
70
|
+
| onRequestMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
71
|
+
preParsing?: preParsingMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
72
|
+
| preParsingMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
73
|
+
preValidation?: preValidationMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
74
|
+
| preValidationMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
75
|
+
preHandler?: preHandlerMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
76
|
+
| preHandlerMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
77
|
+
preSerialization?: preSerializationMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
78
|
+
| preSerializationMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
79
|
+
onSend?: onSendMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
80
|
+
| onSendMetaHookHandler<unknown, RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
81
|
+
onResponse?: onResponseMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
82
|
+
| onResponseMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
83
|
+
onTimeout?: onTimeoutMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
84
|
+
| onTimeoutMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
85
|
+
onError?: onErrorMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, FastifyError, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
86
|
+
| onErrorMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, FastifyError, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
87
|
+
onRequestAbort?: onRequestAbortMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>
|
|
88
|
+
| onRequestAbortMetaHookHandler<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>[];
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* Route handler method declaration.
|
|
@@ -162,7 +162,7 @@ export interface RouteOptions<
|
|
|
162
162
|
> extends RouteShorthandOptions<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler, TypeProvider, Logger> {
|
|
163
163
|
method: HTTPMethods | HTTPMethods[];
|
|
164
164
|
url: string;
|
|
165
|
-
handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, SchemaCompiler
|
|
165
|
+
handler: RouteHandlerMethod<RawServer, RawRequest, RawReply, RouteGeneric, ContextConfig, NoInferCompat<SchemaCompiler>, TypeProvider, Logger>;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
export type RouteHandler<
|
package/types/utils.d.ts
CHANGED
|
@@ -88,3 +88,8 @@ type OmitIndexSignature<T> = {
|
|
|
88
88
|
* Use this type only for input values, not for output values.
|
|
89
89
|
*/
|
|
90
90
|
export type HttpHeader = keyof OmitIndexSignature<http.OutgoingHttpHeaders> | (string & Record<never, never>);
|
|
91
|
+
|
|
92
|
+
// cheat for similar (same?) behavior as NoInfer but for TS <5.4
|
|
93
|
+
export type NoInferCompat<SC> = {
|
|
94
|
+
[K in keyof SC]: SC[K]
|
|
95
|
+
};
|
package/EXPENSE_POLICY.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# Expense Policy
|
|
2
|
-
|
|
3
|
-
Fastify collaborators accept donations through the [Open Collective](https://opencollective.com/fastify/)
|
|
4
|
-
platform and [GitHub Sponsors](https://github.com/sponsors/fastify)
|
|
5
|
-
to enhance the project and support the community.
|
|
6
|
-
|
|
7
|
-
This Collective is run by and for the benefit of the independent contributors to
|
|
8
|
-
the Fastify open source software project.
|
|
9
|
-
This Collective is not endorsed or administered by OpenJS Foundation, Inc.
|
|
10
|
-
(the “OpenJS Foundation”). The OpenJS Foundation does not receive or have
|
|
11
|
-
control over any funds contributed. The OpenJS Foundation does not direct or
|
|
12
|
-
otherwise supervise the actions of any contributor to the Fastify project,
|
|
13
|
-
and all donations made will be expended for the private benefit of or otherwise
|
|
14
|
-
to reimburse individuals that do not have an employer/employee, contractor, or
|
|
15
|
-
other agency relationship with the OpenJS Foundation.
|
|
16
|
-
The Fastify marks used herein are used under license from the OpenJS Foundation
|
|
17
|
-
for the benefit of the open source software community.
|
|
18
|
-
|
|
19
|
-
The admins of the Fastify Collective are the [lead maintainers](./GOVERNANCE.md)
|
|
20
|
-
of the project.
|
|
21
|
-
|
|
22
|
-
This document outlines the process for requesting reimbursement or an invoice
|
|
23
|
-
for expenses.
|
|
24
|
-
|
|
25
|
-
## Reimbursement
|
|
26
|
-
|
|
27
|
-
Reimbursement is applicable for expenses already paid, such as:
|
|
28
|
-
|
|
29
|
-
- Stickers
|
|
30
|
-
- Gadgets
|
|
31
|
-
- Hosting
|
|
32
|
-
|
|
33
|
-
**Before making any purchases**, initiate a [new discussion](https://github.com/orgs/fastify/discussions)
|
|
34
|
-
in the `fastify` organization with the following information:
|
|
35
|
-
|
|
36
|
-
- What is needed
|
|
37
|
-
- Why it is needed
|
|
38
|
-
- Cost
|
|
39
|
-
- Deadline
|
|
40
|
-
|
|
41
|
-
Once the discussion is approved by a lead maintainer and with no unresolved objections,
|
|
42
|
-
the purchase can proceed, and an expense can be submitted to the [Open Collective][submit].
|
|
43
|
-
This process takes a minimum of 3 business days from the request to allow time for
|
|
44
|
-
discussion approval.
|
|
45
|
-
|
|
46
|
-
The discussion helps prevent misunderstandings and ensures the expense is not rejected.
|
|
47
|
-
As a project under the OpenJS Foundation, Fastify benefits from the Foundation's
|
|
48
|
-
resources, including servers, domains, and [travel funds](https://github.com/openjs-foundation/community-fund/tree/main/programs/travel-fund).
|
|
49
|
-
|
|
50
|
-
Always seek approval first.
|
|
51
|
-
|
|
52
|
-
## Invoice
|
|
53
|
-
|
|
54
|
-
Invoices are for services provided to the Fastify project, such as PR reviews,
|
|
55
|
-
documentation, etc.
|
|
56
|
-
A VAT number is not required to submit an invoice.
|
|
57
|
-
Refer to the [Open Collective documentation][openc_docs] for details.
|
|
58
|
-
|
|
59
|
-
### Adding a bounty to an issue
|
|
60
|
-
|
|
61
|
-
Issues become eligible for a bounty when the core team adds the `bounty` label,
|
|
62
|
-
with the amount determined by the core team based on `estimated hours * rate`
|
|
63
|
-
(suggested $50 per hour).
|
|
64
|
-
|
|
65
|
-
> Example: If the estimated time to fix the issue is 2 hours,
|
|
66
|
-
> the bounty will be $100.
|
|
67
|
-
|
|
68
|
-
To add a bounty:
|
|
69
|
-
|
|
70
|
-
- Apply the `bounty` label to the issue
|
|
71
|
-
- Comment on the issue with the bounty amount
|
|
72
|
-
- Edit the first comment of the issue using this template:
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
## 💰 Bounty
|
|
76
|
-
|
|
77
|
-
This issue has a bounty of [$AMOUNT](LINK TO THE BOUNTY COMMENT).
|
|
78
|
-
_Read more about [the bounty program](./EXPENSE_POLICY.md)_
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
For discussions on bounties or determining amounts, open a [new discussion](https://github.com/orgs/fastify/discussions/new?category=bounty).
|
|
82
|
-
|
|
83
|
-
### Outstanding contributions
|
|
84
|
-
|
|
85
|
-
The lead team can decide to add a bounty to an issue or PR not labeled as `bounty`
|
|
86
|
-
if the contribution is outstanding.
|
|
87
|
-
|
|
88
|
-
### Claiming a bounty
|
|
89
|
-
|
|
90
|
-
To claim a bounty:
|
|
91
|
-
|
|
92
|
-
- Submit a PR that fixes the issue
|
|
93
|
-
- If multiple submissions exist, a core member will choose the best solution
|
|
94
|
-
- Once merged, the PR author can claim the bounty by:
|
|
95
|
-
- Submitting an expense to the [Open Collective][submit] with the PR link
|
|
96
|
-
- Adding a comment on the PR with a link to their Open Collective expense to
|
|
97
|
-
ensure the claimant is the issue resolver
|
|
98
|
-
- The expense will be validated by a lead maintainer and then the payment will be
|
|
99
|
-
processed by Open Collective
|
|
100
|
-
|
|
101
|
-
If the Open Collective budget is insufficient, the expense will be rejected.
|
|
102
|
-
Unclaimed bounties are available for other issues.
|
|
103
|
-
|
|
104
|
-
[submit]: https://opencollective.com/fastify/expenses/new
|
|
105
|
-
[openc_docs]: https://docs.oscollective.org/how-it-works/basics/invoice-and-reimbursement-examples
|