Skip to content

Commit 8a54776

Browse files
authored
feat: add specific timeout for request (#414)
* feat: add specific timeout for request * test: add timeout for tsd * docs: add timeout option * fix: replace logic or to nullish coalescing
1 parent 29017e9 commit 8a54776

File tree

8 files changed

+194
-16
lines changed

8 files changed

+194
-16
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ By default: 0 (disabled)
477477
Override the `'Content-Type'` header of the forwarded request, if we are
478478
already overriding the [`body`](#body).
479479

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+
480484
### Combining with [@fastify/formbody](https://github.com/fastify/fastify-formbody)
481485

482486
`formbody` expects the body to be returned as a string and not an object.

index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
5555
opts = opts || {}
5656
const req = this.request.raw
5757
const method = opts.method || req.method
58+
const timeout = opts.timeout
5859
const onResponse = opts.onResponse
5960
const rewriteHeaders = opts.rewriteHeaders || headersNoOp
6061
const rewriteRequestHeaders = opts.rewriteRequestHeaders || requestHeadersNoOp
@@ -169,7 +170,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
169170
requestImpl = createRequestRetry(request, this, getDefaultDelay)
170171
}
171172

172-
requestImpl({ method, url, qs, headers: requestHeaders, body }, (err, res) => {
173+
requestImpl({ method, url, qs, headers: requestHeaders, body, timeout }, (err, res) => {
173174
if (err) {
174175
this.request.log.warn(err, 'response errored')
175176
if (!this.sent) {

lib/request.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ function buildRequest (opts) {
120120
hostname: opts.url.hostname,
121121
headers: opts.headers,
122122
agent: agents[opts.url.protocol.replace(/^unix:/, '')],
123-
...httpOpts.requestOptions
123+
...httpOpts.requestOptions,
124+
timeout: opts.timeout ?? httpOpts.requestOptions.timeout
124125
})
125126
req.on('error', done)
126127
req.on('response', res => {
@@ -146,8 +147,8 @@ function buildRequest (opts) {
146147
method: opts.method,
147148
headers: Object.assign({}, opts.headers),
148149
body: opts.body,
149-
headersTimeout: undiciOpts.headersTimeout,
150-
bodyTimeout: undiciOpts.bodyTimeout
150+
headersTimeout: opts.timeout ?? undiciOpts.headersTimeout,
151+
bodyTimeout: opts.timeout ?? undiciOpts.bodyTimeout
151152
}
152153

153154
let pool
@@ -212,7 +213,7 @@ function buildRequest (opts) {
212213
if (!isGet && !isDelete) {
213214
end(req, opts.body, done)
214215
}
215-
req.setTimeout(http2Opts.requestTimeout, () => {
216+
req.setTimeout(opts.timeout ?? http2Opts.requestTimeout, () => {
216217
const err = new Http2RequestTimeoutError()
217218
req.close(http2.constants.NGHTTP2_CANCEL)
218219
done(err)

test/http-timeout.test.js

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@ const From = require('..')
66
const got = require('got')
77
const FakeTimers = require('@sinonjs/fake-timers')
88

9-
const clock = FakeTimers.createClock()
10-
119
test('http request timeout', async (t) => {
10+
const clock = FakeTimers.createClock()
1211
const target = Fastify()
1312
t.teardown(target.close.bind(target))
1413

1514
target.get('/', (_request, reply) => {
1615
t.pass('request arrives')
1716

18-
clock.setTimeout(() => {
17+
setTimeout(() => {
1918
reply.status(200).send('hello world')
20-
t.end()
2119
}, 200)
20+
21+
clock.tick(200)
2222
})
2323

2424
await target.listen({ port: 0 })
@@ -45,7 +45,60 @@ test('http request timeout', async (t) => {
4545
error: 'Gateway Timeout',
4646
message: 'Gateway Timeout'
4747
})
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+
4866
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+
})
49102
return
50103
}
51104

test/http2-timeout.test.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { test } = require('tap')
44
const Fastify = require('fastify')
55
const From = require('..')
66
const got = require('got')
7+
const FakeTimers = require('@sinonjs/fake-timers')
78

89
test('http2 request timeout', async (t) => {
910
const target = Fastify({ http2: true, sessionTimeout: 0 })
@@ -49,6 +50,63 @@ test('http2 request timeout', async (t) => {
4950
t.fail()
5051
})
5152

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+
52110
test('http2 session timeout', async (t) => {
53111
const target = Fastify({ http2: true, sessionTimeout: 0 })
54112
t.teardown(target.close.bind(target))

test/undici-timeout.test.js

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
'use strict'
22

3-
const t = require('tap')
3+
const { test } = require('tap')
44
const Fastify = require('fastify')
55
const From = require('..')
66
const got = require('got')
77
const FakeTimers = require('@sinonjs/fake-timers')
88

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()
1211
const target = Fastify()
1312
t.teardown(target.close.bind(target))
1413

1514
target.get('/', (_request, reply) => {
1615
t.pass('request arrives')
1716

18-
clock.setTimeout(() => {
17+
setTimeout(() => {
1918
reply.status(200).send('hello world')
20-
t.end()
2119
}, 1000)
20+
21+
clock.tick(1000)
2222
})
2323

2424
await target.listen({ port: 0 })
@@ -50,7 +50,66 @@ t.test('undici request timeout', async (t) => {
5050
error: 'Gateway Timeout',
5151
message: 'Gateway Timeout'
5252
})
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+
5371
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+
})
54113
return
55114
}
56115

types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ declare namespace fastifyReplyFrom {
8383
base: string
8484
) => string;
8585
method?: HTTPMethods;
86+
timeout?: number;
8687
}
8788

8889
interface Http2Options {

types/index.test-d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ async function main () {
7070
})
7171
server.get('/v3', (_request, reply) => {
7272
reply.from('/v3', {
73+
timeout: 1000,
7374
body: { hello: 'world' },
7475
rewriteRequestHeaders (req, headers) {
7576
expectType<FastifyRequest<RequestGenericInterface, RawServerBase>>(req)
@@ -102,7 +103,7 @@ async function main () {
102103
instance.get('/http2', (_request, reply) => {
103104
reply.from('/', {
104105
method: 'POST',
105-
// eslint-disable-next-line n/handle-callback-err -- Not a real request, not handling errors
106+
106107
retryDelay: ({ req, res, getDefaultDelay }) => {
107108
const defaultDelay = getDefaultDelay()
108109
if (defaultDelay) return defaultDelay

0 commit comments

Comments
 (0)