@fastify/reply-from 12.0.1 → 12.1.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.
@@ -9,5 +9,5 @@ updates:
9
9
  - package-ecosystem: "npm"
10
10
  directory: "/"
11
11
  schedule:
12
- interval: "weekly"
12
+ interval: "monthly"
13
13
  open-pull-requests-limit: 10
@@ -4,7 +4,6 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
- - master
8
7
  - next
9
8
  - 'v*'
10
9
  paths-ignore:
@@ -17,6 +16,9 @@ on:
17
16
 
18
17
  jobs:
19
18
  test:
19
+ permissions:
20
+ contents: write
21
+ pull-requests: write
20
22
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
21
23
  with:
22
24
  license-check: true
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fastify/reply-from
2
2
 
3
- [![CI](https://github.com/fastify/fastify-reply-from/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/fastify-reply-from/actions/workflows/ci.yml)
3
+ [![CI](https://github.com/fastify/fastify-reply-from/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/fastify-reply-from/actions/workflows/ci.yml)
4
4
  [![NPM version](https://img.shields.io/npm/v/@fastify/reply-from.svg?style=flat)](https://www.npmjs.com/package/@fastify/reply-from)
5
5
  [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
6
6
 
@@ -477,6 +477,10 @@ By default: 0 (disabled)
477
477
  Override the `'Content-Type'` header of the forwarded request, if we are
478
478
  already overriding the [`body`](#body).
479
479
 
480
+ #### `timeout`
481
+
482
+ Set a specific timeout for the request. Override options `http.requestOptions.timeout`, `http2.requestOptions.timeout`, `undici.headersTimeout` and `undici.bodyTimeout` from the plugin config.
483
+
480
484
  ### Combining with [@fastify/formbody](https://github.com/fastify/fastify-formbody)
481
485
 
482
486
  `formbody` expects the body to be returned as a string and not an object.
package/index.js CHANGED
@@ -55,6 +55,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
55
55
  opts = opts || {}
56
56
  const req = this.request.raw
57
57
  const method = opts.method || req.method
58
+ const timeout = opts.timeout
58
59
  const onResponse = opts.onResponse
59
60
  const rewriteHeaders = opts.rewriteHeaders || headersNoOp
60
61
  const rewriteRequestHeaders = opts.rewriteRequestHeaders || requestHeadersNoOp
@@ -169,7 +170,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
169
170
  requestImpl = createRequestRetry(request, this, getDefaultDelay)
170
171
  }
171
172
 
172
- requestImpl({ method, url, qs, headers: requestHeaders, body }, (err, res) => {
173
+ requestImpl({ method, url, qs, headers: requestHeaders, body, timeout }, (err, res) => {
173
174
  if (err) {
174
175
  this.request.log.warn(err, 'response errored')
175
176
  if (!this.sent) {
package/lib/request.js CHANGED
@@ -120,7 +120,8 @@ function buildRequest (opts) {
120
120
  hostname: opts.url.hostname,
121
121
  headers: opts.headers,
122
122
  agent: agents[opts.url.protocol.replace(/^unix:/, '')],
123
- ...httpOpts.requestOptions
123
+ ...httpOpts.requestOptions,
124
+ timeout: opts.timeout ?? httpOpts.requestOptions.timeout
124
125
  })
125
126
  req.on('error', done)
126
127
  req.on('response', res => {
@@ -146,8 +147,8 @@ function buildRequest (opts) {
146
147
  method: opts.method,
147
148
  headers: Object.assign({}, opts.headers),
148
149
  body: opts.body,
149
- headersTimeout: undiciOpts.headersTimeout,
150
- bodyTimeout: undiciOpts.bodyTimeout
150
+ headersTimeout: opts.timeout ?? undiciOpts.headersTimeout,
151
+ bodyTimeout: opts.timeout ?? undiciOpts.bodyTimeout
151
152
  }
152
153
 
153
154
  let pool
@@ -212,7 +213,7 @@ function buildRequest (opts) {
212
213
  if (!isGet && !isDelete) {
213
214
  end(req, opts.body, done)
214
215
  }
215
- req.setTimeout(http2Opts.requestTimeout, () => {
216
+ req.setTimeout(opts.timeout ?? http2Opts.requestTimeout, () => {
216
217
  const err = new Http2RequestTimeoutError()
217
218
  req.close(http2.constants.NGHTTP2_CANCEL)
218
219
  done(err)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fastify/reply-from",
3
- "version": "12.0.1",
3
+ "version": "12.1.0",
4
4
  "description": "forward your HTTP request to another server, for fastify",
5
5
  "main": "index.js",
6
6
  "type": "commonjs",
@@ -70,7 +70,7 @@
70
70
  "got": "^11.8.6",
71
71
  "h2url": "^0.2.0",
72
72
  "neostandard": "^0.12.0",
73
- "nock": "^13.5.4",
73
+ "nock": "^14.0.0",
74
74
  "proxy": "^2.1.1",
75
75
  "proxyquire": "^2.1.3",
76
76
  "simple-get": "^4.0.1",
@@ -6,19 +6,19 @@ const From = require('..')
6
6
  const got = require('got')
7
7
  const FakeTimers = require('@sinonjs/fake-timers')
8
8
 
9
- const clock = FakeTimers.createClock()
10
-
11
9
  test('http request timeout', async (t) => {
10
+ const clock = FakeTimers.createClock()
12
11
  const target = Fastify()
13
12
  t.teardown(target.close.bind(target))
14
13
 
15
14
  target.get('/', (_request, reply) => {
16
15
  t.pass('request arrives')
17
16
 
18
- clock.setTimeout(() => {
17
+ setTimeout(() => {
19
18
  reply.status(200).send('hello world')
20
- t.end()
21
19
  }, 200)
20
+
21
+ clock.tick(200)
22
22
  })
23
23
 
24
24
  await target.listen({ port: 0 })
@@ -45,7 +45,60 @@ test('http request timeout', async (t) => {
45
45
  error: 'Gateway Timeout',
46
46
  message: 'Gateway Timeout'
47
47
  })
48
+ return
49
+ }
50
+
51
+ t.fail()
52
+ })
53
+
54
+ test('http request with specific timeout', async (t) => {
55
+ const clock = FakeTimers.createClock()
56
+ const target = Fastify()
57
+ t.teardown(target.close.bind(target))
58
+
59
+ target.get('/', (_request, reply) => {
60
+ t.pass('request arrives')
61
+
62
+ setTimeout(() => {
63
+ reply.status(200).send('hello world')
64
+ }, 200)
65
+
48
66
  clock.tick(200)
67
+ })
68
+
69
+ await target.listen({ port: 0 })
70
+
71
+ const instance = Fastify()
72
+ t.teardown(instance.close.bind(instance))
73
+
74
+ instance.register(From, { http: { requestOptions: { timeout: 100 } } })
75
+
76
+ instance.get('/success', (_request, reply) => {
77
+ reply.from(`http://localhost:${target.server.address().port}/`, {
78
+ timeout: 300
79
+ })
80
+ })
81
+ instance.get('/fail', (_request, reply) => {
82
+ reply.from(`http://localhost:${target.server.address().port}/`, {
83
+ timeout: 50
84
+ })
85
+ })
86
+
87
+ await instance.listen({ port: 0 })
88
+ const { statusCode } = await got.get(`http://localhost:${instance.server.address().port}/success`, { retry: 0 })
89
+ t.equal(statusCode, 200)
90
+
91
+ try {
92
+ await got.get(`http://localhost:${instance.server.address().port}/fail`, { retry: 0 })
93
+ } catch (err) {
94
+ t.equal(err.response.statusCode, 504)
95
+ t.match(err.response.headers['content-type'], /application\/json/)
96
+ t.same(JSON.parse(err.response.body), {
97
+ statusCode: 504,
98
+ code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT',
99
+ error: 'Gateway Timeout',
100
+ message: 'Gateway Timeout'
101
+ })
49
102
  return
50
103
  }
51
104
 
@@ -4,6 +4,7 @@ const { test } = require('tap')
4
4
  const Fastify = require('fastify')
5
5
  const From = require('..')
6
6
  const got = require('got')
7
+ const FakeTimers = require('@sinonjs/fake-timers')
7
8
 
8
9
  test('http2 request timeout', async (t) => {
9
10
  const target = Fastify({ http2: true, sessionTimeout: 0 })
@@ -49,6 +50,63 @@ test('http2 request timeout', async (t) => {
49
50
  t.fail()
50
51
  })
51
52
 
53
+ test('http2 request with specific timeout', async (t) => {
54
+ const clock = FakeTimers.createClock()
55
+ const target = Fastify({ http2: true })
56
+ t.teardown(target.close.bind(target))
57
+
58
+ target.get('/', (_request, reply) => {
59
+ t.pass('request arrives')
60
+
61
+ setTimeout(() => {
62
+ reply.status(200).send('hello world')
63
+ }, 200)
64
+
65
+ clock.tick(200)
66
+ })
67
+
68
+ await target.listen({ port: 0 })
69
+
70
+ const instance = Fastify()
71
+ t.teardown(instance.close.bind(instance))
72
+
73
+ instance.register(From, {
74
+ base: `http://localhost:${target.server.address().port}`,
75
+ http2: { requestTimeout: 100, sessionTimeout: 6000 }
76
+ })
77
+
78
+ instance.get('/success', (_request, reply) => {
79
+ reply.from(`http://localhost:${target.server.address().port}/`, {
80
+ timeout: 300
81
+ })
82
+ })
83
+ instance.get('/fail', (_request, reply) => {
84
+ reply.from(`http://localhost:${target.server.address().port}/`, {
85
+ timeout: 50
86
+ })
87
+ })
88
+
89
+ await instance.listen({ port: 0 })
90
+ const { statusCode } = await got.get(`http://localhost:${instance.server.address().port}/success`, { retry: 0 })
91
+ t.equal(statusCode, 200)
92
+
93
+ try {
94
+ await got.get(`http://localhost:${instance.server.address().port}/fail`, { retry: 0 })
95
+ } catch (err) {
96
+ t.equal(err.response.statusCode, 504)
97
+ t.match(err.response.headers['content-type'], /application\/json/)
98
+ t.same(JSON.parse(err.response.body), {
99
+ statusCode: 504,
100
+ code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT',
101
+ error: 'Gateway Timeout',
102
+ message: 'Gateway Timeout'
103
+ })
104
+ return
105
+ }
106
+
107
+ t.fail()
108
+ })
109
+
52
110
  test('http2 session timeout', async (t) => {
53
111
  const target = Fastify({ http2: true, sessionTimeout: 0 })
54
112
  t.teardown(target.close.bind(target))
@@ -1,24 +1,24 @@
1
1
  'use strict'
2
2
 
3
- const t = require('tap')
3
+ const { test } = require('tap')
4
4
  const Fastify = require('fastify')
5
5
  const From = require('..')
6
6
  const got = require('got')
7
7
  const FakeTimers = require('@sinonjs/fake-timers')
8
8
 
9
- const clock = FakeTimers.createClock()
10
-
11
- t.test('undici request timeout', async (t) => {
9
+ test('undici request timeout', async (t) => {
10
+ const clock = FakeTimers.createClock()
12
11
  const target = Fastify()
13
12
  t.teardown(target.close.bind(target))
14
13
 
15
14
  target.get('/', (_request, reply) => {
16
15
  t.pass('request arrives')
17
16
 
18
- clock.setTimeout(() => {
17
+ setTimeout(() => {
19
18
  reply.status(200).send('hello world')
20
- t.end()
21
19
  }, 1000)
20
+
21
+ clock.tick(1000)
22
22
  })
23
23
 
24
24
  await target.listen({ port: 0 })
@@ -50,7 +50,66 @@ t.test('undici request timeout', async (t) => {
50
50
  error: 'Gateway Timeout',
51
51
  message: 'Gateway Timeout'
52
52
  })
53
+ return
54
+ }
55
+
56
+ t.fail()
57
+ })
58
+
59
+ test('undici request with specific timeout', async (t) => {
60
+ const clock = FakeTimers.createClock()
61
+ const target = Fastify()
62
+ t.teardown(target.close.bind(target))
63
+
64
+ target.get('/', (_request, reply) => {
65
+ t.pass('request arrives')
66
+
67
+ setTimeout(() => {
68
+ reply.status(200).send('hello world')
69
+ }, 1000)
70
+
53
71
  clock.tick(1000)
72
+ })
73
+
74
+ await target.listen({ port: 0 })
75
+
76
+ const instance = Fastify()
77
+ t.teardown(instance.close.bind(instance))
78
+
79
+ instance.register(From, {
80
+ base: `http://localhost:${target.server.address().port}`,
81
+ undici: {
82
+ headersTimeout: 100,
83
+ }
84
+ })
85
+
86
+ instance.get('/success', (_request, reply) => {
87
+ reply.from('/', {
88
+ timeout: 1000
89
+ })
90
+ })
91
+ instance.get('/fail', (_request, reply) => {
92
+ reply.from('/', {
93
+ timeout: 50
94
+ })
95
+ })
96
+
97
+ await instance.listen({ port: 0 })
98
+
99
+ const { statusCode } = await got.get(`http://localhost:${instance.server.address().port}/success`, { retry: 0 })
100
+ t.equal(statusCode, 200)
101
+
102
+ try {
103
+ await got.get(`http://localhost:${instance.server.address().port}/fail`, { retry: 0 })
104
+ } catch (err) {
105
+ t.equal(err.response.statusCode, 504)
106
+ t.match(err.response.headers['content-type'], /application\/json/)
107
+ t.same(JSON.parse(err.response.body), {
108
+ statusCode: 504,
109
+ code: 'FST_REPLY_FROM_GATEWAY_TIMEOUT',
110
+ error: 'Gateway Timeout',
111
+ message: 'Gateway Timeout'
112
+ })
54
113
  return
55
114
  }
56
115
 
package/types/index.d.ts CHANGED
@@ -16,19 +16,19 @@ import {
16
16
  AgentOptions,
17
17
  IncomingHttpHeaders,
18
18
  RequestOptions,
19
- } from 'http'
19
+ } from 'node:http'
20
20
  import {
21
21
  ClientSessionOptions,
22
22
  ClientSessionRequestOptions,
23
23
  IncomingHttpHeaders as Http2IncomingHttpHeaders,
24
24
  SecureClientSessionOptions,
25
- } from 'http2'
25
+ } from 'node:http2'
26
26
  import {
27
27
  Agent as SecureAgent,
28
28
  AgentOptions as SecureAgentOptions,
29
29
  RequestOptions as SecureRequestOptions
30
- } from 'https'
31
- import { Pool, ProxyAgent } from 'undici'
30
+ } from 'node:https'
31
+ import { Pool, ProxyAgent, Dispatcher } from 'undici'
32
32
 
33
33
  declare module 'fastify' {
34
34
  interface FastifyReply {
@@ -83,6 +83,7 @@ declare namespace fastifyReplyFrom {
83
83
  base: string
84
84
  ) => string;
85
85
  method?: HTTPMethods;
86
+ timeout?: number;
86
87
  }
87
88
 
88
89
  interface Http2Options {
@@ -104,7 +105,7 @@ declare namespace fastifyReplyFrom {
104
105
  disableCache?: boolean;
105
106
  http?: HttpOptions;
106
107
  http2?: Http2Options | boolean;
107
- undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options };
108
+ undici?: Pool.Options & { proxy?: string | URL | ProxyAgent.Options } | { request: Dispatcher['request'] };
108
109
  contentTypesToEncode?: string[];
109
110
  retryMethods?: (HTTPMethods | 'TRACE')[];
110
111
  maxRetriesOn503?: number;
@@ -1,9 +1,10 @@
1
1
  import fastify, { FastifyReply, FastifyRequest, RawReplyDefaultExpression, RawServerBase, RequestGenericInterface, RouteGenericInterface } from 'fastify'
2
2
  import * as http from 'node:http'
3
- import { IncomingHttpHeaders } from 'http2'
3
+ import { IncomingHttpHeaders } from 'node:http2'
4
4
  import * as https from 'node:https'
5
- import { AddressInfo } from 'net'
5
+ import { AddressInfo } from 'node:net'
6
6
  import { expectType } from 'tsd'
7
+ import { Agent, Client, Dispatcher, Pool } from 'undici'
7
8
  import replyFrom, { FastifyReplyFromOptions } from '..'
8
9
  // @ts-ignore
9
10
  import tap from 'tap'
@@ -69,6 +70,7 @@ async function main () {
69
70
  })
70
71
  server.get('/v3', (_request, reply) => {
71
72
  reply.from('/v3', {
73
+ timeout: 1000,
72
74
  body: { hello: 'world' },
73
75
  rewriteRequestHeaders (req, headers) {
74
76
  expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
@@ -101,7 +103,7 @@ async function main () {
101
103
  instance.get('/http2', (_request, reply) => {
102
104
  reply.from('/', {
103
105
  method: 'POST',
104
- // eslint-disable-next-line n/handle-callback-err -- Not a real request, not handling errors
106
+
105
107
  retryDelay: ({ req, res, getDefaultDelay }) => {
106
108
  const defaultDelay = getDefaultDelay()
107
109
  if (defaultDelay) return defaultDelay
@@ -158,6 +160,34 @@ async function main () {
158
160
  })
159
161
  await undiciInstance.ready()
160
162
 
163
+ const undiciInstanceAgent = fastify()
164
+ undiciInstance.register(replyFrom, {
165
+ base: 'http://example2.com',
166
+ undici: new Agent()
167
+ })
168
+ await undiciInstanceAgent.ready()
169
+
170
+ const undiciInstancePool = fastify()
171
+ undiciInstance.register(replyFrom, {
172
+ base: 'http://example2.com',
173
+ undici: new Pool('http://example2.com')
174
+ })
175
+ await undiciInstancePool.ready()
176
+
177
+ const undiciInstanceClient = fastify()
178
+ undiciInstance.register(replyFrom, {
179
+ base: 'http://example2.com',
180
+ undici: new Client('http://example2.com')
181
+ })
182
+ await undiciInstanceClient.ready()
183
+
184
+ const undiciInstanceDispatcher = fastify()
185
+ undiciInstance.register(replyFrom, {
186
+ base: 'http://example2.com',
187
+ undici: new Dispatcher()
188
+ })
189
+ await undiciInstanceDispatcher.ready()
190
+
161
191
  tap.pass('done')
162
192
  tap.end()
163
193
  }