Skip to content
This repository has been archived by the owner on Sep 8, 2023. It is now read-only.

Feature: add support for a read hook #63

Merged
merged 9 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ Service-Client plugins provide the ability to hook into different spots of a req
Plugins are registered with `ServiceClient.mergeConfig({plugins: []})` or `ServiceClient.use([])` and affect all Service-Client instances created thereafter.

### Overview
A plugin exports a function which, when executed, returns an object containing a set of hook functions to be ran during a request's lifecycle. See all available hooks [below](#available-plugins).
A plugin exports a function which, when executed, returns an object containing a set of hook functions to be run during a request's lifecycle. See all available hooks [below](#available-plugins).

Example:
```js
Expand Down Expand Up @@ -322,7 +322,7 @@ async function plugin({client, context, plugins}) {
* This hook is special. The only value that should be returned here is
* a response object. This is helpful for authentication plugins that
* want to retry a request if an invalid response was received. A request
* made within this hook could return it's response object here.
* made within this hook could return its response object here.
*
* There's a catch however. Since response objects from multiple hooks
* cannot be merged, only the first response object will be taken. All
Expand All @@ -335,6 +335,24 @@ async function plugin({client, context, plugins}) {

},

/**
* This hook is special. This hook behaves just like response but can be used to
* validate data after being read. The only value that should be returned here is
* a response object. This is helpful for authentication plugins that
* want to retry a request if an invalid response was received. A request
* made within this hook could return its response object here.
*
* There's a catch however. Since response objects from multiple hooks
* cannot be merged, only the first response object will be taken. All
* other returned responses are discarded.
*
* @param {object} data - data provided to the hook on every request
* @param {object} data.response - the response received from Wreck after being read
*/
async onPostRead(data) {

},

/**
* @param {object} data - data provided to the hook on every request
* @param {Error} data.error - the error object resulting from timeouts, read errors, etc
Expand Down
5 changes: 5 additions & 0 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@ class ServiceClient {
await hooks.error({ context, clientId, requestId, ts, servicename, operation, error })
}
}
if (hooks.onPostRead) {
reqHooks.onPostRead = async (id, response, ts = Date.now()) => {
return hooks.onPostRead({ context, clientId, requestId, ts, servicename, operation, response })
}
}

const doRequest = async function () {
if (self._circuitState.open) {
Expand Down
8 changes: 8 additions & 0 deletions lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ async function init (options) {

return response
}

if (hookName === 'onPostRead') {
const response = results.find((result) => result)

debug('%s(): first result: %j', hookName, response)

return response
}
}
})

Expand Down
12 changes: 12 additions & 0 deletions lib/http/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,18 @@ const makeRequest = async function (method, path, options, hooks) {

response.payload = await Read(response, options.readOptions)

if (hooks.onPostRead) {
response = await hooks.onPostRead(options.id, response) || response
}
// because of the retry, the code is not going to hit the read step
tuckbick marked this conversation as resolved.
Show resolved Hide resolved
// which means that the payload will not be initialized. instead of
// hacking inside hooks to read there, keep the code together and do it
// here
// eslint-disable-next-line no-prototype-builtins
if (!response.hasOwnProperty('payload')) {
response.payload = await Read(response, options.readOptions)
}

debug('makeRequest(): payload read')
oncomplete()
return response
Expand Down
6 changes: 4 additions & 2 deletions test/unit/hooks/standalone.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ describe('Using ServiceClient in a standalone context', () => {
response: suite.sandbox.spy(),
error: suite.sandbox.spy(),
stats: suite.sandbox.spy(),
end: suite.sandbox.spy()
end: suite.sandbox.spy(),
onPostRead: suite.sandbox.spy()
}
ServiceClient.use(() => {
return spies
Expand All @@ -65,8 +66,9 @@ describe('Using ServiceClient in a standalone context', () => {
Sinon.assert.notCalled(spies.error) // no error for this test
Sinon.assert.calledOnce(spies.stats)
Sinon.assert.calledOnce(spies.end)
Sinon.assert.calledOnce(spies.onPostRead)

Sinon.assert.callOrder(spies.request, spies.init, spies.socket, spies.response, spies.stats, spies.end)
Sinon.assert.callOrder(spies.request, spies.init, spies.socket, spies.response, spies.onPostRead, spies.stats, spies.end)
})

it('should call hooks (error thrown in plugin initialization)', async function () {
Expand Down
28 changes: 28 additions & 0 deletions test/unit/http/request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,34 @@ describe('request', () => {
assert.ok(response.payload, 'is body')
})

it('should make a successful request with `read: true` - onPostRead', async function () {
const readStub = suite.sandbox.stub(Wreck, 'read')
readStub.onCall(0).returns(null)
readStub.onCall(1).returns({ message: 'success' })
const hooks = {
onPostRead: () => {
return { statusCode: 200 }
}
}
Nock('http://service.local:80')
.get('/graphql')
.reply(200, {
errors: [{
message:
'Missing or invalid Bearer token in the Authorization header.',
extensions: {
code: 'FORBIDDEN'
}
}
]
}
)

const response = await Request('GET', '/graphql', { baseUrl: 'http://service.local:80', read: true }, hooks)
assert.equal(response.statusCode, 200, 'is ok')
assert.ok(response.payload, 'is body')
})

it('should make a successful request without `connectTimeout`', async function () {
const response = await makeRequest({
getDeferred: () => Request('GET', '/v1/test/stuff', { baseUrl: 'https://service.local:80', connectTimeout: 0 })
Expand Down