Skip to content

Commit 26fc08f

Browse files
authored
Async Reply functions (always emit errors) (#1596)
Functions passed to `Interceptor.reply`, either to respond with an array of `[status, body, headers]` or with just the body, can now be async/promise-returning functions. BREAKING CHANGE: uncaught errors thrown inside of user provided reply functions, whether async or not, will no longer be caught, and will no longer generate a successful response with a status code of 500. Instead, the error will be emitted by the request just like any other unhandled error during the request processing.
1 parent 35221ce commit 26fc08f

File tree

3 files changed

+94
-94
lines changed

3 files changed

+94
-94
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ const scope = nock('http://www.google.com')
395395
})
396396
```
397397

398-
> Note: When using a callback, if you call back with an error as the first argument, that error will be sent in the response body, with a 500 HTTP response status code.
398+
In Nock 11 and later, if an error is passed to the callback, Nock will rethrow it as a programmer error.
399+
In Nock 10 and earlier, the error was sent in the response body, with a 500 HTTP response status code.
399400

400401
You can also return the status code and body using just one function:
401402

lib/request_overrider.js

Lines changed: 25 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { IncomingMessage, ClientRequest } = require('http')
66
const _ = require('lodash')
77
const propagate = require('propagate')
88
const timers = require('timers')
9+
const util = require('util')
910
const zlib = require('zlib')
1011

1112
const common = require('./common')
@@ -213,8 +214,6 @@ function RequestOverrider(req, options, interceptors, remove) {
213214
ended = true
214215
let requestBody, responseBody, responseBuffers
215216

216-
let continued = false
217-
218217
// When request body is a binary buffer we internally use in its hexadecimal representation.
219218
const requestBodyBuffer = Buffer.concat(requestBodyBuffers)
220219
const isBinaryRequestBodyBuffer = common.isUtf8Representable(
@@ -307,41 +306,31 @@ function RequestOverrider(req, options, interceptors, remove) {
307306
if (interceptor.replyFunction) {
308307
const parsedRequestBody = parseJSONRequestBody(req, requestBody)
309308

310-
if (interceptor.replyFunction.length === 3) {
309+
let fn = interceptor.replyFunction
310+
if (fn.length === 3) {
311311
// Handle the case of an async reply function, the third parameter being the callback.
312-
interceptor.replyFunction(
313-
options.path,
314-
parsedRequestBody,
315-
continueWithResponseBody
316-
)
317-
return
312+
fn = util.promisify(fn)
318313
}
319314

320-
const replyResponseBody = interceptor.replyFunction(
321-
options.path,
322-
parsedRequestBody
323-
)
324-
continueWithResponseBody(null, replyResponseBody)
315+
// At this point `fn` is either a synchronous function or a promise-returning function;
316+
// wrapping in `Promise.resolve` makes it into a promise either way.
317+
Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
318+
.then(continueWithResponseBody)
319+
.catch(emitError)
325320
return
326321
}
327322

328323
if (interceptor.fullReplyFunction) {
329324
const parsedRequestBody = parseJSONRequestBody(req, requestBody)
330325

331-
if (interceptor.fullReplyFunction.length === 3) {
332-
interceptor.fullReplyFunction(
333-
options.path,
334-
parsedRequestBody,
335-
continueWithFullResponse
336-
)
337-
return
326+
let fn = interceptor.fullReplyFunction
327+
if (fn.length === 3) {
328+
fn = util.promisify(fn)
338329
}
339330

340-
const fullReplyResult = interceptor.fullReplyFunction(
341-
options.path,
342-
parsedRequestBody
343-
)
344-
continueWithFullResponse(null, fullReplyResult)
331+
Promise.resolve(fn.call(interceptor, options.path, parsedRequestBody))
332+
.then(continueWithFullResponse)
333+
.catch(emitError)
345334
return
346335
}
347336

@@ -367,7 +356,7 @@ function RequestOverrider(req, options, interceptors, remove) {
367356
? interceptor.body
368357
: [interceptor.body]
369358
responseBuffers = bufferData.map(data => Buffer.from(data, 'hex'))
370-
continueWithResponseBody(null, undefined)
359+
continueWithResponseBody()
371360
return
372361
}
373362

@@ -391,36 +380,22 @@ function RequestOverrider(req, options, interceptors, remove) {
391380
}
392381
}
393382

394-
return continueWithResponseBody(null, responseBody)
395-
396-
function continueWithFullResponse(err, fullReplyResult) {
397-
if (!err) {
398-
try {
399-
responseBody = parseFullReplyResult(response, fullReplyResult)
400-
} catch (innerErr) {
401-
emitError(innerErr)
402-
return
403-
}
404-
}
405-
406-
continueWithResponseBody(err, responseBody)
407-
}
383+
return continueWithResponseBody(responseBody)
408384

409-
function continueWithResponseBody(err, responseBody) {
410-
if (continued) {
411-
// subsequent calls from reply callbacks are ignored
385+
function continueWithFullResponse(fullReplyResult) {
386+
try {
387+
responseBody = parseFullReplyResult(response, fullReplyResult)
388+
} catch (innerErr) {
389+
emitError(innerErr)
412390
return
413391
}
414-
continued = true
415392

416-
if (err) {
417-
response.statusCode = 500
418-
responseBody = err.stack
419-
}
393+
continueWithResponseBody(responseBody)
394+
}
420395

396+
function continueWithResponseBody(responseBody) {
421397
// Transform the response body if it exists (it may not exist
422398
// if we have `responseBuffers` instead)
423-
424399
if (responseBody !== undefined) {
425400
debug('transform the response body')
426401

tests/test_reply_function_async.js

Lines changed: 67 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
// callback with the response body or an array containing the status code and
55
// optional response body and headers.
66

7-
const assertRejects = require('assert-rejects')
8-
const http = require('http')
97
const { test } = require('tap')
108
const nock = require('..')
119
const got = require('./got_client')
@@ -51,60 +49,30 @@ test('reply takes a callback for status code', async t => {
5149
scope.done()
5250
})
5351

54-
test('reply should throw on error on the callback', t => {
55-
let dataCalled = false
56-
57-
const scope = nock('http://example.test')
52+
test('reply should throw on error on the callback', async t => {
53+
nock('http://example.test')
5854
.get('/')
5955
.reply(500, (path, requestBody, callback) =>
6056
callback(new Error('Database failed'))
6157
)
6258

63-
// TODO When this request is converted to `got`, it causes the request not
64-
// to match.
65-
const req = http.request(
66-
{
67-
host: 'example.test',
68-
path: '/',
69-
port: 80,
70-
},
71-
res => {
72-
t.equal(res.statusCode, 500, 'Status code is 500')
73-
74-
res.on('data', data => {
75-
dataCalled = true
76-
t.ok(data instanceof Buffer, 'data should be buffer')
77-
t.ok(
78-
data.toString().indexOf('Error: Database failed') === 0,
79-
'response should match'
80-
)
81-
})
82-
83-
res.on('end', () => {
84-
t.ok(dataCalled, 'data handler was called')
85-
scope.done()
86-
t.end()
87-
})
88-
}
89-
)
90-
91-
req.end()
59+
await t.rejects(got('http://example.test'), {
60+
name: 'RequestError',
61+
message: 'Database failed',
62+
})
9263
})
9364

9465
test('an error passed to the callback propagates when [err, fullResponseArray] is expected', async t => {
95-
const scope = nock('http://example.test')
66+
nock('http://example.test')
9667
.get('/')
9768
.reply((path, requestBody, callback) => {
9869
callback(Error('boom'))
9970
})
10071

101-
await assertRejects(got('http://example.test/'), ({ statusCode, body }) => {
102-
t.is(statusCode, 500)
103-
t.matches(body, 'Error: boom')
104-
return true
72+
await t.rejects(got('http://example.test'), {
73+
name: 'RequestError',
74+
message: 'boom',
10575
})
106-
107-
scope.done()
10876
})
10977

11078
test('subsequent calls to the reply callback are ignored', async t => {
@@ -115,7 +83,7 @@ test('subsequent calls to the reply callback are ignored', async t => {
11583
.reply(201, (path, requestBody, callback) => {
11684
callback(null, 'one')
11785
callback(null, 'two')
118-
callback(null, 'three')
86+
callback(new Error('three'))
11987
t.pass()
12088
})
12189

@@ -125,3 +93,59 @@ test('subsequent calls to the reply callback are ignored', async t => {
12593
t.is(statusCode, 201)
12694
t.equal(body, 'one')
12795
})
96+
97+
test('reply can take a status code with an 2-arg async function, and passes it the correct arguments', async t => {
98+
const scope = nock('http://example.com')
99+
.post('/foo')
100+
.reply(201, async (path, requestBody) => {
101+
t.equal(path, '/foo')
102+
t.equal(requestBody, 'request-body')
103+
return 'response-body'
104+
})
105+
106+
const response = await got.post('http://example.com/foo', {
107+
body: 'request-body',
108+
})
109+
110+
t.equal(response.statusCode, 201)
111+
t.equal(response.body, 'response-body')
112+
scope.done()
113+
})
114+
115+
test('reply can take a status code with a 0-arg async function, and passes it the correct arguments', async t => {
116+
const scope = nock('http://example.com')
117+
.get('/')
118+
.reply(async () => [201, 'Hello World!'])
119+
120+
const response = await got('http://example.com/')
121+
122+
t.equal(response.statusCode, 201)
123+
t.equal(response.body, 'Hello World!')
124+
scope.done()
125+
})
126+
127+
test('when reply is called with a status code and an async function that throws, it propagates the error', async t => {
128+
nock('http://example.test')
129+
.get('/')
130+
.reply(201, async () => {
131+
throw Error('oh no!')
132+
})
133+
134+
await t.rejects(got('http://example.test'), {
135+
name: 'RequestError',
136+
message: 'oh no!',
137+
})
138+
})
139+
140+
test('when reply is called with an async function that throws, it propagates the error', async t => {
141+
nock('http://example.test')
142+
.get('/')
143+
.reply(async () => {
144+
throw Error('oh no!')
145+
})
146+
147+
await t.rejects(got('http://example.test'), {
148+
name: 'RequestError',
149+
message: 'oh no!',
150+
})
151+
})

0 commit comments

Comments
 (0)