Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/Lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ Incoming Request
└─▶ onResponse Hook
```

At any point before or during the `User Handler`, `reply.hijack()` can be called to prevent fastify from:
- Running all the following hooks and user handler
- Sending the response automatically

NB (*): If `reply.raw` is used to send a response back to the user, `onResponse` hooks will still be executed

## Reply Lifecycle

Whenever the user handles the request the result may be:
Expand All @@ -45,7 +51,7 @@ Whenever the user handles the request the result may be:
- in sync handler: it sends a payload
- in sync handler: it sends an `Error` instance

So, when the reply is being submitted, the data flow performed is the following:
If the reply was hijacked, we skip all the below steps. Otherwise, when it is being submitted, the data flow performed is the following:

```
★ schema validation Error
Expand All @@ -56,6 +62,7 @@ So, when the reply is being submitted, the data flow performed is the following:
│ ★ throw an Error
★ send or return │ │
│ │ │
│ ▼ │
reply sent ◀── JSON ─┴─ Error instance ──▶ setErrorHandler ◀─────┘
Expand Down
9 changes: 9 additions & 0 deletions docs/Reply.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
- [.raw](#raw)
- [.serializer(func)](#serializerfunc)
- [.sent](#sent)
- [.hijack](#hijack)
- [.send(data)](#senddata)
- [Objects](#objects)
- [Strings](#strings)
Expand Down Expand Up @@ -251,6 +252,14 @@ app.get('/', (req, reply) => {

If the handler rejects, the error will be logged.

<a name="hijack"></a>
### .hijack()
Sometimes you might need to halt the execution of the normal request lifecycle and handle sending the response manually.

To achieve this, fastify provides the method `reply.hijack()` that can be called during the request lifecycle (At any point before `reply.send()` is called), and allows you to prevent fastify from sending the response, and from running the remaining hooks (and user handler if the reply was hijacked before).

NB (*): If `reply.raw` is used to send a response back to the user, `onResponse` hooks will still be executed

<a name="send"></a>
### .send(data)
As the name suggests, `.send()` is the function that sends the payload to the end user.
Expand Down
28 changes: 28 additions & 0 deletions docs/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,34 @@ fastify.listen({
}, (err) => {})
```

<a name="getDefaultRoute"></a>
#### getDefaultRoute
Method to get the `defaultRoute` for the server:

```js
const defaultRoute = fastify.getDefaultRoute()
```

<a name="setDefaultRoute"></a>
#### setDefaultRoute
Method to set the `defaultRoute` for the server:

```js
const defaultRoute = function (req, res) {
res.end('hello world')
}

fastify.setDefaultRoute(defaultRoute)
```

<a name="routing"></a>
#### routing
Method to access the `lookup` method of the internal router and match the request to the appropriate handler:

```js
fastify.routing(req, res)
```

<a name="route"></a>
#### route
Method to add routes to the server, it also has shorthand functions, check [here](Routes.md).
Expand Down
4 changes: 4 additions & 0 deletions fastify.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ function fastify (options) {
[pluginUtils.registeredPlugins]: [],
[kPluginNameChain]: [],
[kAvvioBoot]: null,
// routing method
routing: httpHandler,
getDefaultRoute: router.getDefaultRoute.bind(router),
setDefaultRoute: router.setDefaultRoute.bind(router),
// routes shorthand methods
delete: function _delete (url, opts, handler) {
return router.prepareRoute.call(this, 'DELETE', url, opts, handler)
Expand Down
6 changes: 6 additions & 0 deletions lib/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ const codes = {
"'%s' is not a valid url component",
400
),
FST_ERR_DEFAULT_ROUTE_INVALID_TYPE: createError(
'FST_ERR_DEFAULT_ROUTE_INVALID_TYPE',
'The defaultRoute type should be a function',
500,
TypeError
),

/**
* again listen when close server
Expand Down
5 changes: 5 additions & 0 deletions lib/reply.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ Object.defineProperties(Reply.prototype, {
}
})

Reply.prototype.hijack = function () {
this[kReplySent] = true
return this
}

Reply.prototype.send = function (payload) {
if (this[kReplyIsRunningOnErrorHook] === true) {
throw new FST_ERR_SEND_INSIDE_ONERR()
Expand Down
16 changes: 14 additions & 2 deletions lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const {

const {
FST_ERR_SCH_VALIDATION_BUILD,
FST_ERR_SCH_SERIALIZATION_BUILD
FST_ERR_SCH_SERIALIZATION_BUILD,
FST_ERR_DEFAULT_ROUTE_INVALID_TYPE
} = require('./errors')

const {
Expand Down Expand Up @@ -87,6 +88,16 @@ function buildRouting (options) {
routing: router.lookup.bind(router), // router func to find the right handler to call
route, // configure a route in the fastify instance
prepareRoute,
getDefaultRoute: function () {
return router.defaultRoute
},
setDefaultRoute: function (defaultRoute) {
if (typeof defaultRoute !== 'function') {
throw new FST_ERR_DEFAULT_ROUTE_INVALID_TYPE()
}

router.defaultRoute = defaultRoute
},
routeHandler,
closeRoutes: () => { closing = true },
printRoutes: router.prettyPrint.bind(router)
Expand All @@ -112,6 +123,7 @@ function buildRouting (options) {
options = Object.assign({}, options, {
method,
url,
path: url,
handler: handler || (options && options.handler)
})

Expand Down Expand Up @@ -152,7 +164,7 @@ function buildRouting (options) {

this.after((notHandledErr, done) => {
const path = opts.url || opts.path
if (path === '/' && prefix.length > 0) {
if (path === '/' && prefix.length > 0 && opts.method !== 'HEAD') {
switch (opts.prefixTrailingSlash) {
case 'slash':
afterRouteAdded.call(this, { path }, notHandledErr, done)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "fastify",
"version": "3.10.1",
"version": "3.11.0",
"description": "Fast and low overhead web framework, for Node.js",
"main": "fastify.js",
"type": "commonjs",
Expand Down
43 changes: 43 additions & 0 deletions test/default-route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict'

const t = require('tap')
const test = t.test
const Fastify = require('..')

test('should fail if defaultRoute is not a function', t => {
t.plan(1)

const fastify = Fastify()
const defaultRoute = {}

fastify.get('/', () => {})

try {
fastify.setDefaultRoute(defaultRoute)
} catch (error) {
t.equal(error.code, 'FST_ERR_DEFAULT_ROUTE_INVALID_TYPE')
}
})

test('correctly sets, returns, and calls defaultRoute', t => {
t.plan(3)

const fastify = Fastify()
const defaultRoute = (req, res) => {
res.end('hello from defaultRoute')
}

fastify.setDefaultRoute(defaultRoute)
const returnedDefaultRoute = fastify.getDefaultRoute()
t.equal(returnedDefaultRoute, defaultRoute)

fastify.get('/', () => {})

fastify.inject({
method: 'GET',
url: '/random'
}, (err, res) => {
t.error(err)
t.equal(res.body, 'hello from defaultRoute')
})
})
53 changes: 53 additions & 0 deletions test/route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1060,3 +1060,56 @@ test('Set a custom HEAD route before GET one without disabling exposeHeadRoutes
t.strictEqual(res.body, '')
})
})

test('HEAD routes properly auto created for GET routes when prefixTrailingSlash: \'no-slash\'', t => {
t.plan(2)

const fastify = Fastify()

fastify.register(function routes (f, opts, next) {
f.route({
method: 'GET',
url: '/',
exposeHeadRoute: true,
prefixTrailingSlash: 'no-slash',
handler: (req, reply) => {
reply.send({ hello: 'world' })
}
})

next()
}, { prefix: '/prefix' })

fastify.inject({ url: '/prefix/prefix', method: 'HEAD' }, (err, res) => {
t.error(err)
t.strictEquals(res.statusCode, 404)
})
})

test('HEAD routes properly auto created for GET routes when prefixTrailingSlash: \'both\'', async t => {
t.plan(3)

const fastify = Fastify()

fastify.register(function routes (f, opts, next) {
f.route({
method: 'GET',
url: '/',
exposeHeadRoute: true,
prefixTrailingSlash: 'both',
handler: (req, reply) => {
reply.send({ hello: 'world' })
}
})

next()
}, { prefix: '/prefix' })

const doublePrefixReply = await fastify.inject({ url: '/prefix/prefix', method: 'HEAD' })
const trailingSlashReply = await fastify.inject({ url: '/prefix/', method: 'HEAD' })
const noneTrailingReply = await fastify.inject({ url: '/prefix', method: 'HEAD' })

t.equals(doublePrefixReply.statusCode, 404)
t.equals(trailingSlashReply.statusCode, 200)
t.equals(noneTrailingReply.statusCode, 200)
})
Loading