@fastify/reply-from 12.0.2 → 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.
@@ -16,6 +16,9 @@ on:
16
16
 
17
17
  jobs:
18
18
  test:
19
+ permissions:
20
+ contents: write
21
+ pull-requests: write
19
22
  uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
20
23
  with:
21
24
  license-check: true
package/README.md CHANGED
@@ -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.2",
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",
@@ -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,18 +16,18 @@ 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'
30
+ } from 'node:https'
31
31
  import { Pool, ProxyAgent, Dispatcher } from 'undici'
32
32
 
33
33
  declare module 'fastify' {
@@ -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 {
@@ -1,8 +1,8 @@
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
7
  import { Agent, Client, Dispatcher, Pool } from 'undici'
8
8
  import replyFrom, { FastifyReplyFromOptions } from '..'
@@ -70,6 +70,7 @@ async function main () {
70
70
  })
71
71
  server.get('/v3', (_request, reply) => {
72
72
  reply.from('/v3', {
73
+ timeout: 1000,
73
74
  body: { hello: 'world' },
74
75
  rewriteRequestHeaders (req, headers) {
75
76
  expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
@@ -102,7 +103,7 @@ async function main () {
102
103
  instance.get('/http2', (_request, reply) => {
103
104
  reply.from('/', {
104
105
  method: 'POST',
105
- // eslint-disable-next-line n/handle-callback-err -- Not a real request, not handling errors
106
+
106
107
  retryDelay: ({ req, res, getDefaultDelay }) => {
107
108
  const defaultDelay = getDefaultDelay()
108
109
  if (defaultDelay) return defaultDelay