@fastify/reply-from 9.6.0 → 9.8.0

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/README.md CHANGED
@@ -82,7 +82,7 @@ proxy.register(require('@fastify/reply-from'), {
82
82
 
83
83
  #### `undici`
84
84
 
85
- By default, [undici](https://github.com/mcollina/undici) will be used to perform the HTTP/1.1
85
+ By default, [undici](https://github.com/nodejs/undici) will be used to perform the HTTP/1.1
86
86
  requests. Enabling this flag should guarantee
87
87
  20-50% more throughput.
88
88
 
@@ -102,6 +102,18 @@ proxy.register(require('@fastify/reply-from'), {
102
102
  }
103
103
  })
104
104
  ```
105
+
106
+ You can also include a proxy for the undici client:
107
+
108
+ ```js
109
+ proxy.register(require('@fastify/reply-from'), {
110
+ base: 'http://localhost:3001/',
111
+ undici: {
112
+ proxy: 'http://my.proxy.server:8080',
113
+ }
114
+ })
115
+ ```
116
+
105
117
  See undici own options for more configurations.
106
118
 
107
119
  You can also pass the plugin a custom instance:
@@ -281,6 +293,7 @@ If a `handler` is passed to the `retryDelay` object the onus is on the client to
281
293
  - `res` is the raw response returned by the underlying agent (if available) __Note__: this object is not a Fastify response, but instead the low-level response from the agent. This property may be null if no response was obtained at all, like from a connection reset or timeout.
282
294
  - `attempt` in the object callback refers to the current retriesAttempt number. You are given the freedom to use this in concert with the retryCount property set to handle retries
283
295
  - `getDefaultRetry` refers to the default retry handler. If this callback returns not null and you wish to handle those case of errors simply invoke it as done below.
296
+ - `retriesCount` refers to the retriesCount property a client passes to reply-from. Note if the client does not explicitly set this value it will default to 0. The objective value here is to avoid hard-coding and seeing the retriesCount set. It is your perogative to ensure that you ensure the value here is as you wish (and not `0` if not intended to be as a result of a lack of not setting it).
284
297
 
285
298
  Given example
286
299
 
@@ -324,7 +337,7 @@ const customRetryLogic = ({req, res, err, getDefaultRetry}: RetryDetails) => {
324
337
  ### `reply.from(source, [opts])`
325
338
 
326
339
  The plugin decorates the
327
- [`Reply`](https://github.com/fastify/fastify/blob/master/docs/Reply.md)
340
+ [`Reply`](https://fastify.dev/docs/latest/Reference/Reply)
328
341
  instance with a `from` method, which will reply to the original request
329
342
  __from the desired source__. The options allows to override any part of
330
343
  the request or response being sent or received to/from the source.
@@ -427,7 +440,7 @@ server.register(fastifyHttpProxy, {
427
440
  });
428
441
  ```
429
442
 
430
- #### `queryString` or `queryString(search, reqUrl)`
443
+ #### `queryString` or `queryString(search, reqUrl, request)`
431
444
 
432
445
  Replaces the original querystring of the request with what is specified.
433
446
  This will be passed to
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const fp = require('fastify-plugin')
4
- const { lru } = require('tiny-lru')
4
+ const { LruMap } = require('toad-cache')
5
5
  const querystring = require('fast-querystring')
6
6
  const fastContentTypeParse = require('fast-content-type-parse')
7
7
  const Stream = require('node:stream')
@@ -32,7 +32,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
32
32
  const retryMethods = new Set(opts.retryMethods || [
33
33
  'GET', 'HEAD', 'OPTIONS', 'TRACE'])
34
34
 
35
- const cache = opts.disableCache ? undefined : lru(opts.cacheURLs || 100)
35
+ const cache = opts.disableCache ? undefined : new LruMap(opts.cacheURLs || 100)
36
36
  const base = opts.base
37
37
  const requestBuilt = buildRequest({
38
38
  http: opts.http,
@@ -80,7 +80,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
80
80
  const sourceHttp2 = req.httpVersionMajor === 2
81
81
  const headers = sourceHttp2 ? filterPseudoHeaders(req.headers) : { ...req.headers }
82
82
  headers.host = url.host
83
- const qs = getQueryString(url.search, req.url, opts)
83
+ const qs = getQueryString(url.search, req.url, opts, this.request)
84
84
  let body = ''
85
85
 
86
86
  if (opts.body !== undefined) {
@@ -163,7 +163,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
163
163
 
164
164
  if (retryDelay) {
165
165
  requestImpl = createRequestRetry(request, this, (req, res, err, retries) => {
166
- return retryDelay({ err, req, res, attempt: retries, getDefaultDelay })
166
+ return retryDelay({ err, req, res, attempt: retries, getDefaultDelay, retriesCount })
167
167
  })
168
168
  } else {
169
169
  requestImpl = createRequestRetry(request, this, getDefaultDelay)
@@ -227,9 +227,9 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
227
227
  name: '@fastify/reply-from'
228
228
  })
229
229
 
230
- function getQueryString (search, reqUrl, opts) {
230
+ function getQueryString (search, reqUrl, opts, request) {
231
231
  if (typeof opts.queryString === 'function') {
232
- return '?' + opts.queryString(search, reqUrl)
232
+ return '?' + opts.queryString(search, reqUrl, request)
233
233
  }
234
234
 
235
235
  if (opts.queryString) {
package/lib/request.js CHANGED
@@ -3,7 +3,7 @@ const http = require('node:http')
3
3
  const https = require('node:https')
4
4
  const querystring = require('node:querystring')
5
5
  const eos = require('end-of-stream')
6
- const pump = require('pump')
6
+ const { pipeline } = require('node:stream')
7
7
  const undici = require('undici')
8
8
  const { stripHttp1ConnectionHeaders } = require('./utils')
9
9
  const http2 = require('node:http2')
@@ -83,7 +83,11 @@ function buildRequest (opts) {
83
83
  } else if (isUndiciInstance(opts.undici)) {
84
84
  undiciInstance = opts.undici
85
85
  } else if (!globalAgent) {
86
- undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
86
+ if (undiciOpts.proxy) {
87
+ undiciAgent = new undici.ProxyAgent(getUndiciProxyOptions(opts.undici))
88
+ } else {
89
+ undiciAgent = new undici.Agent(getUndiciOptions(opts.undici))
90
+ }
87
91
  } else {
88
92
  undiciAgent = undici.getGlobalDispatcher()
89
93
  }
@@ -204,7 +208,8 @@ function buildRequest (opts) {
204
208
  ...stripHttp1ConnectionHeaders(opts.headers)
205
209
  }, http2Opts.requestOptions)
206
210
  const isGet = opts.method === 'GET' || opts.method === 'get'
207
- if (!isGet) {
211
+ const isDelete = opts.method === 'DELETE' || opts.method === 'delete'
212
+ if (!isGet && !isDelete) {
208
213
  end(req, opts.body, done)
209
214
  }
210
215
  req.setTimeout(http2Opts.requestTimeout, () => {
@@ -248,7 +253,7 @@ function end (req, body, cb) {
248
253
  if (!body || typeof body === 'string' || body instanceof Uint8Array) {
249
254
  req.end(body)
250
255
  } else if (body.pipe) {
251
- pump(body, req, err => {
256
+ pipeline(body, req, err => {
252
257
  if (err) cb(err)
253
258
  })
254
259
  } else {
@@ -303,6 +308,13 @@ function getAgentOptions (opts) {
303
308
  }
304
309
  }
305
310
 
311
+ function getUndiciProxyOptions ({ proxy, ...opts }) {
312
+ if (typeof proxy === 'string' || proxy instanceof URL) {
313
+ return getUndiciOptions({ uri: proxy, ...opts })
314
+ }
315
+ return getUndiciOptions({ ...proxy, ...opts })
316
+ }
317
+
306
318
  function getUndiciOptions (opts = {}) {
307
319
  const res = {
308
320
  pipelining: 1,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fastify/reply-from",
3
- "version": "9.6.0",
3
+ "version": "9.8.0",
4
4
  "description": "forward your HTTP request to another server, for fastify",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
7
7
  "types": "types/index.d.ts",
8
8
  "scripts": {
9
9
  "lint": "standard | snazzy",
10
- "lint:fix": "standard --fix",
10
+ "lint:fix": "standard --fix | snazzy",
11
11
  "test": "npm run test:unit && npm run test:typescript",
12
12
  "test:unit": "tap",
13
13
  "test:typescript": "tsd"
@@ -41,6 +41,7 @@
41
41
  "h2url": "^0.2.0",
42
42
  "msgpack5": "^6.0.1",
43
43
  "nock": "^13.2.6",
44
+ "proxy": "^2.1.1",
44
45
  "proxyquire": "^2.1.3",
45
46
  "semver": "^7.5.1",
46
47
  "simple-get": "^4.0.1",
@@ -48,7 +49,7 @@
48
49
  "split2": "^4.1.0",
49
50
  "standard": "^17.0.0",
50
51
  "tap": "^16.2.0",
51
- "tsd": "^0.30.1"
52
+ "tsd": "^0.31.0"
52
53
  },
53
54
  "dependencies": {
54
55
  "@fastify/error": "^3.0.0",
@@ -56,8 +57,7 @@
56
57
  "fast-content-type-parse": "^1.1.0",
57
58
  "fast-querystring": "^1.0.0",
58
59
  "fastify-plugin": "^4.0.0",
59
- "pump": "^3.0.0",
60
- "tiny-lru": "^11.0.0",
60
+ "toad-cache": "^3.7.0",
61
61
  "undici": "^5.19.1"
62
62
  },
63
63
  "pre-commit": [
@@ -0,0 +1,50 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const got = require('got')
7
+
8
+ test('http -> http2', async function (t) {
9
+ const instance = Fastify()
10
+
11
+ t.teardown(instance.close.bind(instance))
12
+
13
+ const target = Fastify({
14
+ http2: true
15
+ })
16
+
17
+ target.delete('/', (request, reply) => {
18
+ t.pass('request proxied')
19
+ reply.code(200).header('x-my-header', 'hello!').send({
20
+ hello: 'world'
21
+ })
22
+ })
23
+
24
+ instance.delete('/', (request, reply) => {
25
+ reply.from()
26
+ })
27
+
28
+ t.teardown(target.close.bind(target))
29
+
30
+ await target.listen({ port: 0 })
31
+
32
+ instance.register(From, {
33
+ base: `http://localhost:${target.server.address().port}`,
34
+ http2: true
35
+ })
36
+
37
+ await instance.listen({ port: 0 })
38
+
39
+ const { headers, body, statusCode } = await got(
40
+ `http://localhost:${instance.server.address().port}`,
41
+ {
42
+ method: 'DELETE',
43
+ responseType: 'json'
44
+ }
45
+ )
46
+ t.equal(statusCode, 200)
47
+ t.equal(headers['x-my-header'], 'hello!')
48
+ t.match(headers['content-type'], /application\/json/)
49
+ t.same(body, { hello: 'world' })
50
+ })
@@ -0,0 +1,56 @@
1
+ 'use strict'
2
+
3
+ const t = require('tap')
4
+ const Fastify = require('fastify')
5
+ const From = require('..')
6
+ const http = require('node:http')
7
+ const get = require('simple-get').concat
8
+ const querystring = require('node:querystring')
9
+
10
+ const instance = Fastify()
11
+
12
+ instance.addHook('preHandler', (request, reply, done) => {
13
+ request.addedVal = 'test'
14
+ done()
15
+ })
16
+
17
+ t.plan(10)
18
+ t.teardown(instance.close.bind(instance))
19
+
20
+ const target = http.createServer((req, res) => {
21
+ t.pass('request proxied')
22
+ t.equal(req.method, 'GET')
23
+ t.equal(req.url, '/world?q=test')
24
+ res.statusCode = 205
25
+ res.setHeader('Content-Type', 'text/plain')
26
+ res.setHeader('x-my-header', 'hello!')
27
+ res.end('hello world')
28
+ })
29
+
30
+ instance.get('/hello', (request, reply) => {
31
+ reply.from(`http://localhost:${target.address().port}/world?a=b`, {
32
+ queryString (search, reqUrl, request) {
33
+ return querystring.stringify({ q: request.addedVal })
34
+ }
35
+ })
36
+ })
37
+
38
+ t.teardown(target.close.bind(target))
39
+
40
+ target.listen({ port: 0 }, (err) => {
41
+ t.error(err)
42
+
43
+ instance.register(From)
44
+
45
+ instance.listen({ port: 0 }, (err) => {
46
+ t.error(err)
47
+
48
+ get(`http://localhost:${instance.server.address().port}/hello?a=b`, (err, res, data) => {
49
+ t.error(err)
50
+ t.equal(res.headers['content-type'], 'text/plain')
51
+ t.equal(res.headers['x-my-header'], 'hello!')
52
+ t.equal(res.statusCode, 205)
53
+ t.equal(data.toString(), 'hello world')
54
+ })
55
+ })
56
+ })
@@ -160,3 +160,27 @@ test('we can exceed our retryCount and introspect attempts independently', async
160
160
  t.equal(res.statusCode, 205)
161
161
  t.equal(res.body.toString(), 'Hello World 5!')
162
162
  })
163
+
164
+ test('we handle our retries based on the retryCount', async (t) => {
165
+ const attemptCounter = []
166
+ const customRetryLogic = ({ req, res, err, attempt, getDefaultDelay, retriesCount }) => {
167
+ if (retriesCount < attempt) {
168
+ return null
169
+ }
170
+
171
+ if (res && res.statusCode === 500 && req.method === 'GET') {
172
+ attemptCounter.push(attempt)
173
+ return 0.1
174
+ }
175
+ return null
176
+ }
177
+
178
+ const { instance } = await setupServer(t, { retryDelay: customRetryLogic, retriesCount: 2 }, 500)
179
+
180
+ const res = await got.get(`http://localhost:${instance.server.address().port}`, { retry: 5 })
181
+
182
+ t.match(attemptCounter, [0, 1])
183
+ t.equal(res.headers['content-type'], 'text/plain')
184
+ t.equal(res.statusCode, 205)
185
+ t.equal(res.body.toString(), 'Hello World 5!')
186
+ })
@@ -0,0 +1,79 @@
1
+ 'use strict'
2
+
3
+ const { test } = require('tap')
4
+ const { createServer } = require('node:http')
5
+ const Fastify = require('fastify')
6
+ const get = require('simple-get').concat
7
+ const { createProxy } = require('proxy')
8
+ const From = require('..')
9
+
10
+ const configFormat = {
11
+ string: (value) => value,
12
+ 'url instance': (value) => new URL(value),
13
+ object: (value) => ({ uri: value })
14
+ }
15
+
16
+ for (const [description, format] of Object.entries(configFormat)) {
17
+ test(`use undici ProxyAgent to connect through proxy - configured via ${description}`, async (t) => {
18
+ t.plan(5)
19
+ const target = await buildServer()
20
+ const proxy = await buildProxy()
21
+ t.teardown(target.close.bind(target))
22
+ t.teardown(proxy.close.bind(proxy))
23
+
24
+ const targetUrl = `http://localhost:${target.address().port}`
25
+ const proxyUrl = `http://localhost:${proxy.address().port}`
26
+
27
+ proxy.on('connect', () => {
28
+ t.ok(true, 'should connect to proxy')
29
+ })
30
+
31
+ target.on('request', (req, res) => {
32
+ res.setHeader('content-type', 'application/json')
33
+ res.end(JSON.stringify({ hello: 'world' }))
34
+ })
35
+
36
+ const instance = Fastify()
37
+ t.teardown(instance.close.bind(instance))
38
+
39
+ instance.register(From, {
40
+ base: targetUrl,
41
+ undici: {
42
+ proxy: format(proxyUrl)
43
+ }
44
+ })
45
+
46
+ instance.get('/', (request, reply) => {
47
+ reply.from()
48
+ })
49
+
50
+ const executionFlow = () => new Promise((resolve) => {
51
+ instance.listen({ port: 0 }, err => {
52
+ t.error(err)
53
+
54
+ get(`http://localhost:${instance.server.address().port}`, (err, res, data) => {
55
+ t.error(err)
56
+ t.same(res.statusCode, 200)
57
+ t.match(JSON.parse(data.toString()), { hello: 'world' })
58
+ resolve()
59
+ })
60
+ })
61
+ })
62
+
63
+ await executionFlow()
64
+ })
65
+ }
66
+
67
+ function buildServer () {
68
+ return new Promise((resolve) => {
69
+ const server = createServer()
70
+ server.listen(0, () => resolve(server))
71
+ })
72
+ }
73
+
74
+ function buildProxy () {
75
+ return new Promise((resolve) => {
76
+ const server = createProxy(createServer())
77
+ server.listen(0, () => resolve(server))
78
+ })
79
+ }
package/types/index.d.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  RequestOptions as SecureRequestOptions
29
29
  } from "https";
30
30
  import { Pool } from 'undici';
31
+ import { ProxyAgent } from 'undici';
31
32
 
32
33
  declare module "fastify" {
33
34
  interface FastifyReply {
@@ -40,13 +41,18 @@ declare module "fastify" {
40
41
 
41
42
  type FastifyReplyFrom = FastifyPluginCallback<fastifyReplyFrom.FastifyReplyFromOptions>
42
43
  declare namespace fastifyReplyFrom {
43
- type QueryStringFunction = (search: string | undefined, reqUrl: string) => string;
44
+ type QueryStringFunction = (
45
+ search: string | undefined,
46
+ reqUrl: string,
47
+ request: FastifyRequest<RequestGenericInterface, RawServerBase>
48
+ ) => string;
44
49
 
45
50
  export type RetryDetails = {
46
51
  err: Error;
47
52
  req: FastifyRequest<RequestGenericInterface, RawServerBase>;
48
53
  res: FastifyReply<RawServerBase>;
49
54
  attempt: number;
55
+ retriesCount: number;
50
56
  getDefaultDelay: () => number | null;
51
57
  }
52
58
  export interface FastifyReplyFromHooks {
@@ -98,7 +104,7 @@ declare namespace fastifyReplyFrom {
98
104
  disableCache?: boolean;
99
105
  http?: HttpOptions;
100
106
  http2?: Http2Options | boolean;
101
- undici?: Pool.Options;
107
+ undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options };
102
108
  contentTypesToEncode?: string[];
103
109
  retryMethods?: (HTTPMethods | 'TRACE')[];
104
110
  maxRetriesOn503?: number;
@@ -38,7 +38,8 @@ const fullOptions: FastifyReplyFromOptions = {
38
38
  disableCache: false,
39
39
  undici: {
40
40
  connections: 100,
41
- pipelining: 10
41
+ pipelining: 10,
42
+ proxy: 'http://example2.com:8080'
42
43
  },
43
44
  contentTypesToEncode: ['application/x-www-form-urlencoded'],
44
45
  retryMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'],
@@ -60,6 +61,10 @@ async function main() {
60
61
 
61
62
  server.register(replyFrom, fullOptions);
62
63
 
64
+ server.register(replyFrom, { undici: { proxy: new URL('http://example2.com:8080') } });
65
+
66
+ server.register(replyFrom, { undici: { proxy: { uri: 'http://example2.com:8080' } } });
67
+
63
68
  server.get("/v1", (request, reply) => {
64
69
  expectType<FastifyReply>(reply.from());
65
70
  });
@@ -91,7 +96,7 @@ async function main() {
91
96
  instance.get("/http2", (request, reply) => {
92
97
  reply.from("/", {
93
98
  method: "POST",
94
- retryDelay: ({err, req, res, attempt, getDefaultDelay}) => {
99
+ retryDelay: ({err, req, res, attempt, retriesCount, getDefaultDelay }) => {
95
100
  const defaultDelay = getDefaultDelay();
96
101
  if (defaultDelay) return defaultDelay;
97
102
 
@@ -111,7 +116,13 @@ async function main() {
111
116
  },
112
117
  onError(reply: FastifyReply<RawServerBase>, error) {
113
118
  return reply.send(error.error);
114
- }
119
+ },
120
+ queryString(search, reqUrl, request) {
121
+ expectType<string | undefined>(search);
122
+ expectType<string>(reqUrl);
123
+ expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(request);
124
+ return '';
125
+ },
115
126
  });
116
127
  });
117
128