Skip to content

feat: Added fromParameters decorator. #421

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 12, 2025
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
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ It also supports an additional `rewriteRequestHeaders(headers, request)` functio
opening the WebSocket connection. This function should return an object with the given headers.
The default implementation forwards the `cookie` header.

## `wsReconnect`
### `wsReconnect`

**Experimental.** (default: `disabled`)

Expand All @@ -247,7 +247,7 @@ To enable the feature, set the `wsReconnect` option to an object with the follow

See the example in [examples/reconnection](examples/reconnection).

## wsHooks
### `wsHooks`

On websocket events, the following hooks are available, note **the hooks are all synchronous**.
The `context` object is passed to all hooks and contains the `log` property.
Expand All @@ -259,6 +259,26 @@ The `context` object is passed to all hooks and contains the `log` property.
- `onReconnect`: A hook function that is called when the connection is reconnected `onReconnect(context, source, target)` (default: `undefined`). The function is called if reconnection feature is enabled.
- `onPong`: A hook function that is called when the target responds to the ping `onPong(context, source, target)` (default: `undefined`). The function is called if reconnection feature is enabled.

## Decorators

### `reply.fromParameters(url[, params[, prefix]])`

It can be used to get the final URL and options that `@fastify/http-proxy` would have used to invoke `reply.from`.

A typical use is to override the request URL:

```javascript
preHandler (request, reply, done) {
if (request.url !== '/original') {
done()
return
}

const { url, options } = reply.fromParameters('/updated', { ...request.params, serverId: 42 })
reply.from(url, options)
}
```

## Benchmarks

The following benchmarks were generated on a dedicated server with an Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz and 64GB of RAM:
Expand Down
30 changes: 22 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -592,16 +592,16 @@ async function fastifyHttpProxy (fastify, opts) {
return components
}

function handler (request, reply) {
const { path, queryParams } = extractUrlComponents(request.url)
function fromParameters (url, params = {}, prefix = '/') {
const { path, queryParams } = extractUrlComponents(url)
let dest = path

if (this.prefix.includes(':')) {
if (prefix.includes(':')) {
const requestedPathElements = path.split('/')
const prefixPathWithVariables = this.prefix.split('/').map((_, index) => requestedPathElements[index]).join('/')
const prefixPathWithVariables = prefix.split('/').map((_, index) => requestedPathElements[index]).join('/')

let rewritePrefixWithVariables = rewritePrefix
for (const [name, value] of Object.entries(request.params)) {
for (const [name, value] of Object.entries(params)) {
rewritePrefixWithVariables = rewritePrefixWithVariables.replace(`:${name}`, value)
}

Expand All @@ -610,20 +610,34 @@ async function fastifyHttpProxy (fastify, opts) {
dest += `?${qs.stringify(queryParams)}`
}
} else {
dest = dest.replace(this.prefix, rewritePrefix)
dest = dest.replace(prefix, rewritePrefix)
}

return { url: dest || '/', options: replyOpts }
}

function handler (request, reply) {
const { url, options } = fromParameters(request.url, request.params, this.prefix)

if (request.raw[kWs]) {
reply.hijack()
try {
wsProxy.handleUpgrade(request, dest || '/', noop)
wsProxy.handleUpgrade(request, url, noop)
} /* c8 ignore start */ catch (err) {
request.log.warn({ err }, 'websocket proxy error')
} /* c8 ignore stop */
return
}
reply.from(dest || '/', replyOpts)
reply.from(url, options)
}

fastify.decorateReply('fromParameters', fromParameters)
fastify.decorateReply('wsProxy', {
/* c8 ignore next 3 */
getter () {
return wsProxy
}
})
}

module.exports = fp(fastifyHttpProxy, {
Expand Down
37 changes: 37 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,43 @@ async function run () {
const queryParams = JSON.stringify(qs.parse('foo=bar&foo=baz&abc=qux'))
t.assert.strictEqual(firstProxyPrefix.body, `this is "variable-api" endpoint with id 123 and query params ${queryParams}`)
})

test('manual from call via fromParameters', async t => {
const server = Fastify()
server.register(proxy, {
upstream: `http://localhost:${origin.server.address().port}`,
preHandler (request, reply, done) {
if (request.url !== '/fake-a') {
done()
return
}

const { url, options } = reply.fromParameters('/a')
reply.from(url, options)
}
})

await server.listen({ port: 0 })
t.after(() => server.close())

{
const {
statusCode,
body
} = await got(`http://localhost:${server.server.address().port}/`)
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(body, 'this is root')
}

{
const {
statusCode,
body
} = await got(`http://localhost:${server.server.address().port}/fake-a`)
t.assert.strictEqual(statusCode, 200)
t.assert.strictEqual(body, 'this is a')
}
})
}

run()
Loading