Skip to content

Commit 33934f8

Browse files
arontsangkibertoad
andauthored
Extensible DispatchHandler (nodejs#1338)
* Create DispatchInterceptors * Add Unit Tests, fix DispatchHandler typescript Add documentation * Switch to simple null check and shortcircuit bind * Add typescript test for Dispatcher events * Move build intecepted dispatch to top level * Restore lost method * Fix TS error * Address code review comments * Fix linting * Type improvements * Fix TS tests * Address code review comments * Fix TS test * Fix types * Address comments * Fix client construction Co-authored-by: Igor Savin <[email protected]>
1 parent 0d1f162 commit 33934f8

31 files changed

+589
-48
lines changed

docs/api/Agent.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Extends: [`ClientOptions`](Pool.md#parameter-pooloptions)
2020

2121
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
2222
* **maxRedirections** `Integer` - Default: `0`. The number of HTTP redirection to follow unless otherwise specified in `DispatchOptions`.
23+
* **interceptors** `{ Agent: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching).
2324

2425
## Instance Properties
2526

docs/api/Client.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Returns: `Client`
2626
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections.
2727
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
2828
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body.
29+
* **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching).
2930

3031
#### Parameter: `ConnectOptions`
3132

docs/api/DispatchInterceptor.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#Interface: DispatchInterceptor
2+
3+
Extends: `Function`
4+
5+
A function that can be applied to the `Dispatcher.Dispatch` function before it is invoked with a dispatch request.
6+
7+
This allows one to write logic to intercept both the outgoing request, and the incoming response.
8+
9+
### Parameter: `Dispatcher.Dispatch`
10+
11+
The base dispatch function you are decorating.
12+
13+
### ReturnType: `Dispatcher.Dispatch`
14+
15+
A dispatch function that has been altered to provide additional logic
16+
17+
### Basic Example
18+
19+
Here is an example of an interceptor being used to provide a JWT bearer token
20+
21+
```js
22+
'use strict'
23+
24+
const insertHeaderInterceptor = dispatch => {
25+
return function InterceptedDispatch(opts, handler){
26+
opts.headers.push('Authorization', 'Bearer [Some token]')
27+
return dispatch(opts, handler)
28+
}
29+
}
30+
31+
const client = new Client('https://localhost:3000', {
32+
interceptors: { Client: [insertHeaderInterceptor] }
33+
})
34+
35+
```
36+
37+
### Basic Example 2
38+
39+
Here is a contrived example of an interceptor stripping the headers from a response.
40+
41+
```js
42+
'use strict'
43+
44+
const clearHeadersInterceptor = dispatch => {
45+
const { DecoratorHandler } = require('undici')
46+
class ResultInterceptor extends DecoratorHandler {
47+
onHeaders (statusCode, headers, resume) {
48+
return super.onHeaders(statusCode, [], resume)
49+
}
50+
}
51+
return function InterceptedDispatch(opts, handler){
52+
return dispatch(opts, new ResultInterceptor(handler))
53+
}
54+
}
55+
56+
const client = new Client('https://localhost:3000', {
57+
interceptors: { Client: [clearHeadersInterceptor] }
58+
})
59+
60+
```

docs/api/Pool.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Extends: [`ClientOptions`](Client.md#parameter-clientoptions)
1919

2020
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
2121
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
22+
* **interceptors** `{ Pool: DispatchInterceptor[] } }` - Default: `{ Pool: [] }` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching).
2223

2324
## Instance Properties
2425

index.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import Dispatcher = require('./types/dispatcher')
22
import { setGlobalDispatcher, getGlobalDispatcher } from './types/global-dispatcher'
33
import { setGlobalOrigin, getGlobalOrigin } from './types/global-origin'
44
import Pool = require('./types/pool')
5+
import { RedirectHandler, DecoratorHandler } from './types/handlers'
6+
57
import BalancedPool = require('./types/balanced-pool')
68
import Client = require('./types/client')
79
import buildConnector = require('./types/connector')
@@ -20,14 +22,17 @@ export * from './types/formdata'
2022
export * from './types/diagnostics-channel'
2123
export { Interceptable } from './types/mock-interceptor'
2224

23-
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent }
25+
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler }
2426
export default Undici
2527

2628
declare function Undici(url: string, opts: Pool.Options): Pool
2729

2830
declare namespace Undici {
2931
var Dispatcher: typeof import('./types/dispatcher')
3032
var Pool: typeof import('./types/pool');
33+
var RedirectHandler: typeof import ('./types/handlers').RedirectHandler
34+
var DecoratorHandler: typeof import ('./types/handlers').DecoratorHandler
35+
var createRedirectInterceptor: typeof import ('./types/interceptors').createRedirectInterceptor
3136
var BalancedPool: typeof import('./types/balanced-pool');
3237
var Client: typeof import('./types/client');
3338
var buildConnector: typeof import('./types/connector');

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const MockPool = require('./lib/mock/mock-pool')
1616
const mockErrors = require('./lib/mock/mock-errors')
1717
const ProxyAgent = require('./lib/proxy-agent')
1818
const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
19+
const DecoratorHandler = require('./lib/handler/DecoratorHandler')
20+
const RedirectHandler = require('./lib/handler/RedirectHandler')
21+
const createRedirectInterceptor = require('./lib/interceptor/redirectInterceptor')
1922

2023
const nodeVersion = process.versions.node.split('.')
2124
const nodeMajor = Number(nodeVersion[0])
@@ -30,6 +33,10 @@ module.exports.BalancedPool = BalancedPool
3033
module.exports.Agent = Agent
3134
module.exports.ProxyAgent = ProxyAgent
3235

36+
module.exports.DecoratorHandler = DecoratorHandler
37+
module.exports.RedirectHandler = RedirectHandler
38+
module.exports.createRedirectInterceptor = createRedirectInterceptor
39+
3340
module.exports.buildConnector = buildConnector
3441
module.exports.errors = errors
3542

lib/agent.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use strict'
22

33
const { InvalidArgumentError } = require('./core/errors')
4-
const { kClients, kRunning, kClose, kDestroy, kDispatch } = require('./core/symbols')
4+
const { kClients, kRunning, kClose, kDestroy, kDispatch, kInterceptors } = require('./core/symbols')
55
const DispatcherBase = require('./dispatcher-base')
66
const Pool = require('./pool')
77
const Client = require('./client')
88
const util = require('./core/util')
9-
const RedirectHandler = require('./handler/redirect')
9+
const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
1010
const { WeakRef, FinalizationRegistry } = require('./compat/dispatcher-weakref')()
1111

1212
const kOnConnect = Symbol('onConnect')
@@ -44,7 +44,14 @@ class Agent extends DispatcherBase {
4444
connect = { ...connect }
4545
}
4646

47+
this[kInterceptors] = options.interceptors && options.interceptors.Agent && Array.isArray(options.interceptors.Agent)
48+
? options.interceptors.Agent
49+
: [createRedirectInterceptor({ maxRedirections })]
50+
4751
this[kOptions] = { ...util.deepClone(options), connect }
52+
this[kOptions].interceptors = options.interceptors
53+
? { ...options.interceptors }
54+
: undefined
4855
this[kMaxRedirections] = maxRedirections
4956
this[kFactory] = factory
5057
this[kClients] = new Map()
@@ -108,12 +115,6 @@ class Agent extends DispatcherBase {
108115
this[kFinalizer].register(dispatcher, key)
109116
}
110117

111-
const { maxRedirections = this[kMaxRedirections] } = opts
112-
if (maxRedirections != null && maxRedirections !== 0) {
113-
opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
114-
handler = new RedirectHandler(this, maxRedirections, opts, handler)
115-
}
116-
117118
return dispatcher.dispatch(opts, handler)
118119
}
119120

lib/balanced-pool.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const {
1313
kGetDispatcher
1414
} = require('./pool-base')
1515
const Pool = require('./pool')
16-
const { kUrl } = require('./core/symbols')
16+
const { kUrl, kInterceptors } = require('./core/symbols')
1717
const { parseOrigin } = require('./core/util')
1818
const kFactory = Symbol('factory')
1919

@@ -53,6 +53,9 @@ class BalancedPool extends PoolBase {
5353
throw new InvalidArgumentError('factory must be a function.')
5454
}
5555

56+
this[kInterceptors] = opts.interceptors && opts.interceptors.BalancedPool && Array.isArray(opts.interceptors.BalancedPool)
57+
? opts.interceptors.BalancedPool
58+
: []
5659
this[kFactory] = factory
5760

5861
for (const upstream of upstreams) {

lib/client.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ const net = require('net')
77
const util = require('./core/util')
88
const Request = require('./core/request')
99
const DispatcherBase = require('./dispatcher-base')
10-
const RedirectHandler = require('./handler/redirect')
1110
const {
1211
RequestContentLengthMismatchError,
1312
ResponseContentLengthMismatchError,
@@ -60,7 +59,8 @@ const {
6059
kCounter,
6160
kClose,
6261
kDestroy,
63-
kDispatch
62+
kDispatch,
63+
kInterceptors
6464
} = require('./core/symbols')
6565

6666
const kClosedResolve = Symbol('kClosedResolve')
@@ -82,6 +82,7 @@ try {
8282

8383
class Client extends DispatcherBase {
8484
constructor (url, {
85+
interceptors,
8586
maxHeaderSize,
8687
headersTimeout,
8788
socketTimeout,
@@ -179,6 +180,9 @@ class Client extends DispatcherBase {
179180
})
180181
}
181182

183+
this[kInterceptors] = interceptors && interceptors.Client && Array.isArray(interceptors.Client)
184+
? interceptors.Client
185+
: [createRedirectInterceptor({ maxRedirections })]
182186
this[kUrl] = util.parseOrigin(url)
183187
this[kConnector] = connect
184188
this[kSocket] = null
@@ -254,11 +258,6 @@ class Client extends DispatcherBase {
254258
}
255259

256260
[kDispatch] (opts, handler) {
257-
const { maxRedirections = this[kMaxRedirections] } = opts
258-
if (maxRedirections) {
259-
handler = new RedirectHandler(this, maxRedirections, opts, handler)
260-
}
261-
262261
const origin = opts.origin || this[kUrl].origin
263262

264263
const request = new Request(origin, opts, handler)
@@ -319,6 +318,7 @@ class Client extends DispatcherBase {
319318
}
320319

321320
const constants = require('./llhttp/constants')
321+
const createRedirectInterceptor = require('./interceptor/redirectInterceptor')
322322
const EMPTY_BUF = Buffer.alloc(0)
323323

324324
async function lazyllhttp () {

lib/core/symbols.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,6 @@ module.exports = {
4848
kMaxRedirections: Symbol('maxRedirections'),
4949
kMaxRequests: Symbol('maxRequestsPerClient'),
5050
kProxy: Symbol('proxy agent options'),
51-
kCounter: Symbol('socket request counter')
51+
kCounter: Symbol('socket request counter'),
52+
kInterceptors: Symbol('dispatch interceptors')
5253
}

0 commit comments

Comments
 (0)