Skip to content

Commit b51bc3a

Browse files
yohaygyohay-ma
andauthored
issue #95 - adding onError hook (#96)
* 1. adding onError hook 2. Fix to failed service unavailable error test 3. add .idea/ to git ignore file * fixed code review comments: 1. index.ts - error type was changed from string to Error 2. onErrorNoOp was renamed to onErrorDefault 3. rollback timeout errors during tests * fixed code review comments: 1. using httpStatus instead of magic numbers 2. using fake-timers instead of setTimeout in tests * fixed code review comments: 1. namespaced imports are supposed to be above the non-namespaced ones 2. added type check to to .ts test * rollback modifying file * fixed code review comments: 1. added onError to .ts test * fixed code review comments: 1. added onError to .ts test * fixed code review comments: 1. added onError to .ts test * fixed sporadic failed test * code review: using http-errors package * code review: 1. removed dev dependency http-status 2. onError reply.send(error) 3. fixed readme.md accordingly * code review: 1. wrap the error in json object 2. updated README.md * code review: 1. updated onError in index.d.ts 2. updated README.md Co-authored-by: Yohay <yohay.go@moonactive.com>
1 parent 034fd9b commit b51bc3a

File tree

9 files changed

+155
-48
lines changed

9 files changed

+155
-48
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ typings/
5858
.env
5959

6060
.vscode/
61+
.idea/
6162

6263
# lockfiles
6364
yarn.lock
64-
package-lock.json
65+
package-lock.json

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,13 @@ Called when an http response is received from the source.
179179
The default behavior is `reply.send(res)`, which will be disabled if the
180180
option is specified.
181181

182+
#### `onError(reply, error)`
183+
184+
Called when an http response is received with error from the source.
185+
The default behavior is `reply.send(error)`, which will be disabled if the
186+
option is specified.
187+
It must reply the error.
188+
182189
#### `rewriteHeaders(headers)`
183190

184191
Called to rewrite the headers of the response, before them being copied

index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ export interface FastifyReplyFromHooks {
3535
reply: FastifyReply<RawServerBase>,
3636
res: RawReplyDefaultExpression<RawServerBase>
3737
) => void;
38+
onError?: (
39+
reply: FastifyReply<RawServerBase>,
40+
error: { error: Error }
41+
) => void;
3842
body?: unknown;
3943
rewriteHeaders?: (
4044
headers: Http2IncomingHttpHeaders | IncomingHttpHeaders

index.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const URL = require('url').URL
55
const lru = require('tiny-lru')
66
const querystring = require('querystring')
77
const Stream = require('stream')
8+
const createError = require('http-errors')
89
const buildRequest = require('./lib/request')
910
const {
1011
filterPseudoHeaders,
@@ -34,6 +35,7 @@ module.exports = fp(function from (fastify, opts, next) {
3435
const onResponse = opts.onResponse
3536
const rewriteHeaders = opts.rewriteHeaders || headersNoOp
3637
const rewriteRequestHeaders = opts.rewriteRequestHeaders || requestHeadersNoOp
38+
const onError = opts.onError || onErrorDefault
3739

3840
if (!source) {
3941
source = req.url
@@ -103,11 +105,11 @@ module.exports = fp(function from (fastify, opts, next) {
103105
this.request.log.warn(err, 'response errored')
104106
if (!this.sent) {
105107
if (err.code === 'ERR_HTTP2_STREAM_CANCEL' || err.code === 'ENOTFOUND') {
106-
this.code(503).send(new Error('Service Unavailable'))
108+
onError(this, { error: new createError.ServiceUnavailable() })
107109
} else if (err instanceof TimeoutError || err.code === 'UND_ERR_REQUEST_TIMEOUT') {
108-
this.code(504).send(new Error('Gateway Timeout'))
110+
onError(this, { error: new createError.GatewayTimeout() })
109111
} else {
110-
this.code(500).send(err)
112+
onError(this, { error: createError(500, err) })
111113
}
112114
}
113115
return
@@ -165,3 +167,7 @@ function headersNoOp (headers) {
165167
function requestHeadersNoOp (originalReq, headers) {
166168
return headers
167169
}
170+
171+
function onErrorDefault (reply, { error }) {
172+
reply.send(error)
173+
}

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"scripts": {
88
"typescript": "tsd",
99
"coverage": "tap -j8 test/*.js --cov",
10-
"test": "standard | snazzy && tap test/*.js && npm run typescript"
10+
"test": "standard | snazzy && tap --ts test/* && npm run typescript"
1111
},
1212
"repository": {
1313
"type": "git",
@@ -29,7 +29,9 @@
2929
},
3030
"homepage": "https://github.com/fastify/fastify-reply-from#readme",
3131
"devDependencies": {
32+
"@sinonjs/fake-timers": "^6.0.1",
3233
"@types/node": "^14.0.4",
34+
"@types/tap": "^14.10.0",
3335
"fastify": "^3.0.0",
3436
"got": "^11.1.3",
3537
"h2url": "^0.2.0",
@@ -50,7 +52,8 @@
5052
"pump": "^3.0.0",
5153
"semver": "^7.2.1",
5254
"tiny-lru": "^7.0.0",
53-
"undici": "^1.0.0"
55+
"undici": "^1.0.0",
56+
"http-errors": "^1.8.0"
5457
},
5558
"tsd": {
5659
"directory": "test"

test/http-timeout.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const t = require('tap')
44
const Fastify = require('fastify')
55
const From = require('..')
66
const got = require('got')
7+
const FakeTimers = require('@sinonjs/fake-timers')
8+
9+
const clock = FakeTimers.createClock()
710

811
t.autoend(false)
912

@@ -13,10 +16,10 @@ t.tearDown(target.close.bind(target))
1316
target.get('/', (request, reply) => {
1417
t.pass('request arrives')
1518

16-
setTimeout(() => {
19+
clock.setTimeout(() => {
1720
reply.status(200).send('hello world')
1821
t.end()
19-
}, 1000)
22+
}, 200)
2023
})
2124

2225
async function main () {
@@ -43,7 +46,7 @@ async function main () {
4346
error: 'Gateway Timeout',
4447
message: 'Gateway Timeout'
4548
})
46-
49+
clock.tick(200)
4750
return
4851
}
4952

test/index.test-d.ts

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import replyFrom, { FastifyReplyFromOptions } from "../";
2-
import fastify from "fastify";
2+
import fastify, {FastifyReply, RawServerBase} from "fastify";
33
import { AddressInfo } from "net";
44
import { IncomingHttpHeaders } from "http2";
55
import { expectType } from 'tsd';
66
import * as http from 'http';
77
import * as http2 from 'http2';
8+
// @ts-ignore
9+
import tap from 'tap'
810

911
const fullOptions: FastifyReplyFromOptions = {
1012
base: "http://example2.com",
@@ -38,52 +40,68 @@ const fullOptions: FastifyReplyFromOptions = {
3840
pipelining: 10
3941
}
4042
};
43+
tap.autoend(false);
4144

42-
const server = fastify();
45+
async function main() {
46+
const server = fastify();
4347

44-
server.register(replyFrom);
48+
server.register(replyFrom);
4549

46-
server.register(replyFrom, {});
50+
server.register(replyFrom, {});
4751

48-
server.register(replyFrom, { http2: true });
52+
server.register(replyFrom, {http2: true});
4953

50-
server.register(replyFrom, fullOptions);
54+
server.register(replyFrom, fullOptions);
5155

52-
server.get("/v1", (erquest, reply) => {
53-
reply.from();
54-
});
55-
server.get("/v3", (erquest, reply) => {
56-
reply.from("/v3", {
57-
body: { hello: "world" },
58-
rewriteRequestHeaders(req, headers) {
59-
expectType<http.IncomingMessage | http2.Http2ServerRequest>(req);
60-
return headers;
61-
}
56+
server.get("/v1", (request, reply) => {
57+
reply.from();
58+
});
59+
server.get("/v3", (request, reply) => {
60+
reply.from("/v3", {
61+
body: {hello: "world"},
62+
rewriteRequestHeaders(req, headers) {
63+
expectType<http.IncomingMessage | http2.Http2ServerRequest>(req);
64+
return headers;
65+
}
66+
});
6267
});
63-
});
6468

6569
// http2
66-
const instance = fastify({ http2: true });
67-
68-
const target = fastify({ http2: true });
70+
const instance = fastify({http2: true});
71+
// @ts-ignore
72+
tap.tearDown(instance.close.bind(instance));
73+
const target = fastify({http2: true});
74+
// @ts-ignore
75+
tap.tearDown(target.close.bind(target));
76+
instance.get("/", (request, reply) => {
77+
reply.from();
78+
});
6979

70-
instance.get("/", (request, reply) => {
71-
reply.from();
72-
});
80+
instance.get("/http2", (request, reply) => {
81+
reply.from("/", {
82+
rewriteHeaders(headers) {
83+
return headers;
84+
},
85+
rewriteRequestHeaders(req, headers: IncomingHttpHeaders) {
86+
return headers;
87+
},
88+
onError(reply: FastifyReply<RawServerBase>, error) {
89+
return reply.send(error.error);
90+
}
91+
});
92+
});
7393

74-
instance.get("/http2", (request, reply) => {
75-
reply.from("/", {
76-
rewriteHeaders(headers) {
77-
return headers;
78-
},
79-
rewriteRequestHeaders(req, headers: IncomingHttpHeaders) {
80-
return headers;
81-
}
94+
await target.listen(0);
95+
const port = (target.server.address() as AddressInfo).port;
96+
instance.register(replyFrom, {
97+
base: `http://localhost:${port}`,
98+
http2: true,
99+
rejectUnauthorized: false
82100
});
83-
});
101+
await instance.listen(0);
102+
103+
tap.pass('done')
104+
tap.end();
105+
}
84106

85-
instance.register(replyFrom, {
86-
base: `http://localhost:${(target.server.address() as AddressInfo).port}`,
87-
http2: true,
88-
rejectUnauthorized: false
89-
});
107+
main();

test/on-error.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use strict'
2+
3+
const t = require('tap')
4+
const Fastify = require('fastify')
5+
const From = require('..')
6+
const got = require('got')
7+
const FakeTimers = require('@sinonjs/fake-timers')
8+
9+
const clock = FakeTimers.createClock()
10+
11+
t.autoend(false)
12+
13+
const target = Fastify()
14+
t.tearDown(target.close.bind(target))
15+
16+
target.get('/', (request, reply) => {
17+
t.pass('request arrives')
18+
19+
clock.setTimeout(() => {
20+
reply.status(200).send('hello world')
21+
t.end()
22+
}, 1000)
23+
})
24+
25+
async function main () {
26+
await target.listen(0)
27+
28+
const instance = Fastify()
29+
t.tearDown(instance.close.bind(instance))
30+
31+
instance.register(From, { http: { requestOptions: { timeout: 100 } } })
32+
33+
instance.get('/', (request, reply) => {
34+
reply.from(`http://localhost:${target.server.address().port}/`,
35+
{
36+
onError: (reply, { error }) => {
37+
t.equal(error.status, 504)
38+
reply.code(error.status).send(error)
39+
}
40+
})
41+
})
42+
43+
await instance.listen(0)
44+
45+
try {
46+
await got.get(`http://localhost:${instance.server.address().port}/`, { retry: 0 })
47+
} catch (err) {
48+
t.equal(err.response.statusCode, 504)
49+
t.match(err.response.headers['content-type'], /application\/json/)
50+
t.deepEqual(JSON.parse(err.response.body), {
51+
statusCode: 504,
52+
error: 'Gateway Timeout',
53+
message: 'Gateway Timeout'
54+
})
55+
clock.tick(1000)
56+
return
57+
}
58+
59+
t.fail()
60+
}
61+
62+
main()

test/undici-timeout.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ const t = require('tap')
44
const Fastify = require('fastify')
55
const From = require('..')
66
const got = require('got')
7+
const FakeTimers = require('@sinonjs/fake-timers')
8+
9+
const clock = FakeTimers.createClock()
710

811
t.autoend(false)
912

@@ -13,7 +16,7 @@ t.tearDown(target.close.bind(target))
1316
target.get('/', (request, reply) => {
1417
t.pass('request arrives')
1518

16-
setTimeout(() => {
19+
clock.setTimeout(() => {
1720
reply.status(200).send('hello world')
1821
t.end()
1922
}, 1000)
@@ -48,7 +51,7 @@ async function main () {
4851
error: 'Gateway Timeout',
4952
message: 'Gateway Timeout'
5053
})
51-
54+
clock.tick(1000)
5255
return
5356
}
5457

0 commit comments

Comments
 (0)