@datanimbus/postman-request 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +55 -0
- package/README.md +1315 -0
- package/index.js +166 -0
- package/lib/auth.js +167 -0
- package/lib/autohttp/agent.js +206 -0
- package/lib/autohttp/headerValidations.js +40 -0
- package/lib/autohttp/index.js +8 -0
- package/lib/autohttp/request.js +192 -0
- package/lib/autohttp/requestName.js +86 -0
- package/lib/cookies.js +20 -0
- package/lib/getProxyFromURI.js +79 -0
- package/lib/har.js +200 -0
- package/lib/hawk.js +89 -0
- package/lib/helpers.js +91 -0
- package/lib/http2/agent.js +92 -0
- package/lib/http2/index.js +8 -0
- package/lib/http2/request.js +369 -0
- package/lib/inflate.js +64 -0
- package/lib/multipart.js +112 -0
- package/lib/oauth.js +147 -0
- package/lib/querystring.js +50 -0
- package/lib/redirect.js +218 -0
- package/lib/socks.js +45 -0
- package/lib/tunnel.js +175 -0
- package/lib/url-parse.js +187 -0
- package/package.json +77 -0
- package/request.js +2098 -0
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
var jsonSafeStringify = require('json-stringify-safe')
|
|
4
|
+
var crypto = require('crypto')
|
|
5
|
+
var Buffer = require('safe-buffer').Buffer
|
|
6
|
+
var { Transform } = require('stream')
|
|
7
|
+
|
|
8
|
+
var defer = typeof setImmediate === 'undefined'
|
|
9
|
+
? process.nextTick
|
|
10
|
+
: setImmediate
|
|
11
|
+
|
|
12
|
+
// Reference: https://github.com/postmanlabs/postman-request/pull/23
|
|
13
|
+
//
|
|
14
|
+
// function paramsHaveRequestBody (params) {
|
|
15
|
+
// return (
|
|
16
|
+
// params.body ||
|
|
17
|
+
// params.requestBodyStream ||
|
|
18
|
+
// (params.json && typeof params.json !== 'boolean') ||
|
|
19
|
+
// params.multipart
|
|
20
|
+
// )
|
|
21
|
+
// }
|
|
22
|
+
|
|
23
|
+
function safeStringify (obj, replacer) {
|
|
24
|
+
var ret
|
|
25
|
+
try {
|
|
26
|
+
ret = JSON.stringify(obj, replacer)
|
|
27
|
+
} catch (e) {
|
|
28
|
+
ret = jsonSafeStringify(obj, replacer)
|
|
29
|
+
}
|
|
30
|
+
return ret
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function md5 (str) {
|
|
34
|
+
return crypto.createHash('md5').update(str).digest('hex')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isReadStream (rs) {
|
|
38
|
+
return rs.readable && rs.path && rs.mode
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toBase64 (str) {
|
|
42
|
+
return Buffer.from(str || '', 'utf8').toString('base64')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copy (obj) {
|
|
46
|
+
var o = {}
|
|
47
|
+
Object.keys(obj).forEach(function (i) {
|
|
48
|
+
o[i] = obj[i]
|
|
49
|
+
})
|
|
50
|
+
return o
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function version () {
|
|
54
|
+
var numbers = process.version.replace('v', '').split('.')
|
|
55
|
+
return {
|
|
56
|
+
major: parseInt(numbers[0], 10),
|
|
57
|
+
minor: parseInt(numbers[1], 10),
|
|
58
|
+
patch: parseInt(numbers[2], 10)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function now () {
|
|
63
|
+
return performance.now(); // eslint-disable-line
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class SizeTrackerStream extends Transform {
|
|
67
|
+
constructor (options) {
|
|
68
|
+
super(options)
|
|
69
|
+
this.size = 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_transform (chunk, encoding, callback) {
|
|
73
|
+
this.size += chunk.length
|
|
74
|
+
this.push(chunk)
|
|
75
|
+
callback()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_flush (callback) {
|
|
79
|
+
callback()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
exports.safeStringify = safeStringify
|
|
84
|
+
exports.md5 = md5
|
|
85
|
+
exports.isReadStream = isReadStream
|
|
86
|
+
exports.toBase64 = toBase64
|
|
87
|
+
exports.copy = copy
|
|
88
|
+
exports.version = version
|
|
89
|
+
exports.defer = defer
|
|
90
|
+
exports.SizeTrackerStream = SizeTrackerStream
|
|
91
|
+
exports.now = now
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { EventEmitter } = require('events')
|
|
2
|
+
const http2 = require('http2')
|
|
3
|
+
const { getName: getConnectionName } = require('../autohttp/requestName')
|
|
4
|
+
|
|
5
|
+
class Http2Agent extends EventEmitter {
|
|
6
|
+
constructor (options) {
|
|
7
|
+
super()
|
|
8
|
+
this.options = options
|
|
9
|
+
this.connections = {}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
createConnection (req, uri, options, socket) {
|
|
13
|
+
const _options = {
|
|
14
|
+
...options,
|
|
15
|
+
...this.options
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const name = getConnectionName(_options)
|
|
19
|
+
let connection = this.connections[name]
|
|
20
|
+
|
|
21
|
+
// Force create a new connection if the connection is destroyed or closed or a new socket object is supplied
|
|
22
|
+
if (!connection || connection.destroyed || connection.closed || socket) {
|
|
23
|
+
const connectionOptions = {
|
|
24
|
+
..._options,
|
|
25
|
+
port: _options.port || 443,
|
|
26
|
+
settings: {
|
|
27
|
+
enablePush: false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// check if a socket is supplied
|
|
32
|
+
if (socket) {
|
|
33
|
+
connectionOptions.createConnection = () => socket
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
connection = http2.connect(uri, connectionOptions)
|
|
37
|
+
// Connection is created in an unreferenced state and is referenced when a stream is created
|
|
38
|
+
// This is to prevent the connection from keeping the event loop alive
|
|
39
|
+
connection.unref()
|
|
40
|
+
|
|
41
|
+
// Counting semaphore, but since node is single-threaded, this is just a counter
|
|
42
|
+
// Multiple streams can be active on a connection
|
|
43
|
+
// Each stream refs the connection at the start, and unrefs it on end
|
|
44
|
+
// The connection should terminate if no streams are active on it
|
|
45
|
+
// Could be refactored into something prettier
|
|
46
|
+
const oldRef = connection.ref
|
|
47
|
+
const oldUnref = connection.unref
|
|
48
|
+
|
|
49
|
+
const timeoutHandler = () => {
|
|
50
|
+
delete connectionsMap[name]
|
|
51
|
+
connection.close()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
connection.refCount = 0
|
|
55
|
+
connection.ref = function () {
|
|
56
|
+
this.refCount++
|
|
57
|
+
oldRef.call(this)
|
|
58
|
+
connection.off('timeout', timeoutHandler)
|
|
59
|
+
connection.setTimeout(0)
|
|
60
|
+
}
|
|
61
|
+
const connectionsMap = this.connections
|
|
62
|
+
connection.unref = function () {
|
|
63
|
+
this.refCount--
|
|
64
|
+
if (this.refCount === 0) {
|
|
65
|
+
oldUnref.call(this)
|
|
66
|
+
if (_options.timeout) {
|
|
67
|
+
connection.setTimeout(_options.timeout, timeoutHandler)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add a default error listener to HTTP2 session object to transparently swallow errors incase no streams are active
|
|
73
|
+
// Remove the connection from the connections map if the connection has errored out
|
|
74
|
+
connection.on('error', () => {
|
|
75
|
+
delete this.connections[name]
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
connection.once('close', () => {
|
|
79
|
+
delete this.connections[name]
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
this.connections[name] = connection
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return connection
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = {
|
|
90
|
+
Http2Agent,
|
|
91
|
+
globalAgent: new Http2Agent({})
|
|
92
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
const url = require('url')
|
|
2
|
+
const http2 = require('http2')
|
|
3
|
+
const { EventEmitter } = require('events')
|
|
4
|
+
const { globalAgent } = require('./agent')
|
|
5
|
+
const { validateRequestHeaders } = require('../autohttp/headerValidations')
|
|
6
|
+
|
|
7
|
+
const kHeadersFlushed = Symbol('kHeadersFlushed')
|
|
8
|
+
// Connection headers that should not be set by the user. Ref; https://datatracker.ietf.org/doc/html/rfc9113#name-connection-specific-header-
|
|
9
|
+
const connectionHeaders = ['connection', 'host', 'proxy-connection', 'keep-alive', 'transfer-encoding', 'upgrade']
|
|
10
|
+
|
|
11
|
+
// HTTP/2 error codes. Moving to a separate variable to prevent browser builds from breaking
|
|
12
|
+
const http2Constants = http2.constants || {}
|
|
13
|
+
const rstErrorCodesMap = {
|
|
14
|
+
[http2Constants.NGHTTP2_NO_ERROR]: 'NGHTTP2_NO_ERROR',
|
|
15
|
+
[http2Constants.NGHTTP2_PROTOCOL_ERROR]: 'NGHTTP2_PROTOCOL_ERROR',
|
|
16
|
+
[http2Constants.NGHTTP2_INTERNAL_ERROR]: 'NGHTTP2_INTERNAL_ERROR',
|
|
17
|
+
[http2Constants.NGHTTP2_FLOW_CONTROL_ERROR]: 'NGHTTP2_FLOW_CONTROL_ERROR',
|
|
18
|
+
[http2Constants.NGHTTP2_SETTINGS_TIMEOUT]: 'NGHTTP2_SETTINGS_TIMEOUT',
|
|
19
|
+
[http2Constants.NGHTTP2_STREAM_CLOSED]: 'NGHTTP2_STREAM_CLOSED',
|
|
20
|
+
[http2Constants.NGHTTP2_FRAME_SIZE_ERROR]: 'NGHTTP2_FRAME_SIZE_ERROR',
|
|
21
|
+
[http2Constants.NGHTTP2_REFUSED_STREAM]: 'NGHTTP2_REFUSED_STREAM',
|
|
22
|
+
[http2Constants.NGHTTP2_CANCEL]: 'NGHTTP2_CANCEL',
|
|
23
|
+
[http2Constants.NGHTTP2_COMPRESSION_ERROR]: 'NGHTTP2_COMPRESSION_ERROR',
|
|
24
|
+
[http2Constants.NGHTTP2_CONNECT_ERROR]: 'NGHTTP2_CONNECT_ERROR',
|
|
25
|
+
[http2Constants.NGHTTP2_ENHANCE_YOUR_CALM]: 'NGHTTP2_ENHANCE_YOUR_CALM',
|
|
26
|
+
[http2Constants.NGHTTP2_INADEQUATE_SECURITY]: 'NGHTTP2_INADEQUATE_SECURITY',
|
|
27
|
+
[http2Constants.NGHTTP2_HTTP_1_1_REQUIRED]: 'NGHTTP2_HTTP_1_1_REQUIRED'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function httpOptionsToUri (options) {
|
|
31
|
+
return url.format({
|
|
32
|
+
protocol: 'https',
|
|
33
|
+
host: options.host || 'localhost'
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class Http2Request extends EventEmitter {
|
|
38
|
+
constructor (options) {
|
|
39
|
+
super()
|
|
40
|
+
this.onError = this.onError.bind(this)
|
|
41
|
+
this.onDrain = this.onDrain.bind(this)
|
|
42
|
+
this.onClose = this.onClose.bind(this)
|
|
43
|
+
this.onResponse = this.onResponse.bind(this)
|
|
44
|
+
this.onEnd = this.onEnd.bind(this)
|
|
45
|
+
this.onTimeout = this.onTimeout.bind(this)
|
|
46
|
+
|
|
47
|
+
this.registerListeners = this.registerListeners.bind(this)
|
|
48
|
+
this._flushHeaders = this._flushHeaders.bind(this)
|
|
49
|
+
this[kHeadersFlushed] = false
|
|
50
|
+
|
|
51
|
+
const uri = httpOptionsToUri(options)
|
|
52
|
+
const _options = {
|
|
53
|
+
...options,
|
|
54
|
+
port: Number(options.port || 443),
|
|
55
|
+
path: undefined,
|
|
56
|
+
host: options.hostname || options.host || 'localhost'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (options.socketPath) {
|
|
60
|
+
_options.path = options.socketPath
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const agent = options.agent || globalAgent
|
|
64
|
+
|
|
65
|
+
this._client = agent.createConnection(this, uri, _options)
|
|
66
|
+
|
|
67
|
+
const headers = options.headers || {}
|
|
68
|
+
|
|
69
|
+
this.requestHeaders = {
|
|
70
|
+
...headers,
|
|
71
|
+
[http2.constants.HTTP2_HEADER_PATH]: options.path || '/',
|
|
72
|
+
[http2.constants.HTTP2_HEADER_METHOD]: _options.method,
|
|
73
|
+
[http2.constants.HTTP2_HEADER_AUTHORITY]: _options.host + (_options.port !== 443 ? ':' + options.port : '')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (options.uri.isUnix || headers['host'] === 'unix' || _options.host === 'unix') {
|
|
77
|
+
// The authority field needs to be set to 'localhost' when using unix sockets.
|
|
78
|
+
// The default URL parser supplies the isUnix flag when the host is 'unix'. Added other checks incase using a different parser like WHATWG URL (new URL()).
|
|
79
|
+
// See: https://github.com/nodejs/node/issues/32326
|
|
80
|
+
this.requestHeaders = {
|
|
81
|
+
...this.requestHeaders,
|
|
82
|
+
[http2.constants.HTTP2_HEADER_AUTHORITY]: 'localhost'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.socket = this._client.socket
|
|
87
|
+
this._client.once('error', this.onError)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get _header () {
|
|
91
|
+
return '\r\n' + Object.entries(this.stream.sentHeaders)
|
|
92
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
93
|
+
.join('\r\n') + '\r\n\r\n'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get httpVersion () {
|
|
97
|
+
return '2.0'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
registerListeners () {
|
|
101
|
+
this.stream.on('drain', this.onDrain)
|
|
102
|
+
this.stream.on('error', this.onError)
|
|
103
|
+
this.stream.on('close', this.onClose)
|
|
104
|
+
this.stream.on('response', this.onResponse)
|
|
105
|
+
this.stream.on('end', this.onEnd)
|
|
106
|
+
this.stream.on('timeout', this.onTimeout)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onDrain (...args) {
|
|
110
|
+
this.emit('drain', ...args)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onError (e) {
|
|
114
|
+
this.emit('error', e)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onResponse (response) {
|
|
118
|
+
this.emit('response', new ResponseProxy(response, this.stream))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onEnd () {
|
|
122
|
+
this.emit('end')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onTimeout () {
|
|
126
|
+
this.stream.close()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onClose (...args) {
|
|
130
|
+
if (this.stream.rstCode) {
|
|
131
|
+
// Emit error message in case of abnormal stream closure
|
|
132
|
+
// It is fine if the error is emitted multiple times, since the callback has checks to prevent multiple invocations
|
|
133
|
+
this.onError(new Error(`HTTP/2 Stream closed with error code ${rstErrorCodesMap[this.stream.rstCode]}`))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.emit('close', ...args)
|
|
137
|
+
|
|
138
|
+
this._client.off('error', this.onError)
|
|
139
|
+
this.stream.off('drain', this.onDrain)
|
|
140
|
+
this.stream.off('error', this.onError)
|
|
141
|
+
this.stream.off('response', this.onResponse)
|
|
142
|
+
this.stream.off('end', this.onEnd)
|
|
143
|
+
this.stream.off('close', this.onClose)
|
|
144
|
+
this.stream.off('timeout', this.onTimeout)
|
|
145
|
+
|
|
146
|
+
this.removeAllListeners()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setDefaultEncoding (encoding) {
|
|
150
|
+
if (!this[kHeadersFlushed]) {
|
|
151
|
+
this._flushHeaders()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.stream.setDefaultEncoding(encoding)
|
|
155
|
+
return this
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setEncoding (encoding) {
|
|
159
|
+
if (!this[kHeadersFlushed]) {
|
|
160
|
+
this._flushHeaders()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.stream.setEncoding(encoding)
|
|
164
|
+
return this
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
write (chunk) {
|
|
168
|
+
if (!this[kHeadersFlushed]) {
|
|
169
|
+
this._flushHeaders()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return this.stream.write(chunk)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_flushHeaders (endStream = false) {
|
|
176
|
+
if (this[kHeadersFlushed]) {
|
|
177
|
+
throw new Error('Headers already flushed')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.requestHeaders = Object.fromEntries(
|
|
181
|
+
Object.entries(this.requestHeaders)
|
|
182
|
+
.filter(([key]) => !connectionHeaders.includes(key.toLowerCase()))
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// The client was created in an unreferenced state and is referenced when a stream is created
|
|
186
|
+
this._client.ref()
|
|
187
|
+
this.stream = this._client.request(this.requestHeaders, {endStream})
|
|
188
|
+
|
|
189
|
+
const unreferenceFn = () => {
|
|
190
|
+
this._client.unref()
|
|
191
|
+
this.stream.off('close', unreferenceFn)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.stream.on('close', unreferenceFn)
|
|
195
|
+
|
|
196
|
+
this.registerListeners()
|
|
197
|
+
|
|
198
|
+
this[kHeadersFlushed] = true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
pipe (dest) {
|
|
202
|
+
if (!this[kHeadersFlushed]) {
|
|
203
|
+
this._flushHeaders()
|
|
204
|
+
}
|
|
205
|
+
this.stream.pipe(dest)
|
|
206
|
+
|
|
207
|
+
return dest
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
on (eventName, listener) {
|
|
211
|
+
if (eventName === 'socket') {
|
|
212
|
+
listener(this.socket)
|
|
213
|
+
return this
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return super.on(eventName, listener)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
abort () {
|
|
220
|
+
if (!this[kHeadersFlushed]) {
|
|
221
|
+
this._flushHeaders()
|
|
222
|
+
}
|
|
223
|
+
this.stream.destroy()
|
|
224
|
+
|
|
225
|
+
return this
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
end () {
|
|
229
|
+
if (!this[kHeadersFlushed]) {
|
|
230
|
+
this._flushHeaders(true)
|
|
231
|
+
}
|
|
232
|
+
this.stream.end()
|
|
233
|
+
|
|
234
|
+
return this
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
setTimeout (timeout, cb) {
|
|
238
|
+
if (!this[kHeadersFlushed]) {
|
|
239
|
+
this._flushHeaders()
|
|
240
|
+
}
|
|
241
|
+
this.stream.setTimeout(timeout, cb)
|
|
242
|
+
|
|
243
|
+
return this
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
removeHeader (headerKey) {
|
|
247
|
+
if (this[kHeadersFlushed]) {
|
|
248
|
+
throw new Error('Headers already flushed. Cannot remove header')
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (headerKey.startsWith(':')) {
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
delete this.requestHeaders[headerKey]
|
|
256
|
+
|
|
257
|
+
return this
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setHeader (headerKey, headerValue) {
|
|
261
|
+
if (this[kHeadersFlushed]) {
|
|
262
|
+
throw new Error('Headers already flushed. Cannot set header')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (headerKey.startsWith(':')) {
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.requestHeaders[headerKey] = headerValue
|
|
270
|
+
|
|
271
|
+
return this
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function request (options) {
|
|
276
|
+
// HTTP/2 internal implementation sucks. In case of an invalid HTTP/2 header, it destroys the entire session and
|
|
277
|
+
// emits an error asynchronously, instead of throwing it synchronously. Hence, it makes more sense to perform all
|
|
278
|
+
// validations before sending the request.
|
|
279
|
+
validateRequestHeaders(options.headers)
|
|
280
|
+
|
|
281
|
+
return new Http2Request(options)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
class ResponseProxy extends EventEmitter {
|
|
285
|
+
constructor (response, stream) {
|
|
286
|
+
super()
|
|
287
|
+
this.httpVersion = '2.0'
|
|
288
|
+
this.reqStream = stream
|
|
289
|
+
this.response = response
|
|
290
|
+
this.on = this.on.bind(this)
|
|
291
|
+
this.registerRequestListeners()
|
|
292
|
+
this.socket = this.reqStream.session.socket
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
registerRequestListeners () {
|
|
296
|
+
this.reqStream.on('error', (e) => this.emit('error', e))
|
|
297
|
+
this.reqStream.on('close', () => {
|
|
298
|
+
this.emit('close')
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
on (eventName, listener) {
|
|
303
|
+
super.on(eventName, listener)
|
|
304
|
+
if (eventName === 'data') {
|
|
305
|
+
// Attach the data listener to the request stream only when there is a listener.
|
|
306
|
+
// This is because the data event is emitted by the request stream and the response stream is a proxy
|
|
307
|
+
// that forwards the data event to the response object.
|
|
308
|
+
// If there is no listener attached and we use the event forwarding pattern above, the data event will still be emitted
|
|
309
|
+
// but with no listeners attached to it, thus causing data loss.
|
|
310
|
+
this.reqStream.on('data', (chunk) => {
|
|
311
|
+
this.emit('data', chunk)
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (eventName === 'end') {
|
|
316
|
+
// Incase of bodies with no data, the end event is emitted immediately after the response event. In such cases, the consumer might not have attached the end listener yet. (eg: postman-echo.com/gets)
|
|
317
|
+
// Thus, when the end event is emitted, we check if the request stream has already ended. If it has, we emit the end event immediately.
|
|
318
|
+
// Otherwise, we wait for the request stream to end and then emit the end event.
|
|
319
|
+
if (this.reqStream.readableEnded) {
|
|
320
|
+
process.nextTick(listener)
|
|
321
|
+
} else {
|
|
322
|
+
this.reqStream.on('end', listener)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return this
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
get statusCode () {
|
|
329
|
+
return this.response[http2.constants.HTTP2_HEADER_STATUS]
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
get rawHeaders () {
|
|
333
|
+
return Object.entries(this.response).flat()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
get headers () {
|
|
337
|
+
return Object.fromEntries(Object.entries(this.response))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
pause () {
|
|
341
|
+
this.reqStream.pause()
|
|
342
|
+
return this
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
resume () {
|
|
346
|
+
this.reqStream.resume()
|
|
347
|
+
return this
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
pipe (dest) {
|
|
351
|
+
this.reqStream.pipe(dest)
|
|
352
|
+
return dest
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
setEncoding (encoding) {
|
|
356
|
+
this.reqStream.setEncoding(encoding)
|
|
357
|
+
return this
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
destroy () {
|
|
361
|
+
this.reqStream.destroy()
|
|
362
|
+
return this
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = {
|
|
367
|
+
request,
|
|
368
|
+
Http2Request
|
|
369
|
+
}
|
package/lib/inflate.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
var zlib = require('zlib')
|
|
4
|
+
var stream = require('stream')
|
|
5
|
+
var inherit = require('util').inherits
|
|
6
|
+
var Buffer = require('safe-buffer').Buffer
|
|
7
|
+
var Inflate
|
|
8
|
+
|
|
9
|
+
Inflate = function (options) {
|
|
10
|
+
this.options = options
|
|
11
|
+
this._stream = null
|
|
12
|
+
stream.Transform.call(this)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
inherit(Inflate, stream.Transform)
|
|
16
|
+
|
|
17
|
+
Inflate.prototype._transform = function (chunk, encoding, callback) {
|
|
18
|
+
var self = this
|
|
19
|
+
if (!self._stream) {
|
|
20
|
+
// If the response stream does not have a valid deflate header, use `InflateRaw`
|
|
21
|
+
if ((Buffer.from(chunk, encoding)[0] & 0x0F) === 0x08) {
|
|
22
|
+
self._stream = zlib.createInflate(self.options)
|
|
23
|
+
} else {
|
|
24
|
+
self._stream = zlib.createInflateRaw(self.options)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
self._stream.on('error', function (error) {
|
|
28
|
+
self.emit('error', error)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
self._stream.on('data', function (chunk) {
|
|
32
|
+
self.push(chunk)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
self._stream.once('end', function () {
|
|
36
|
+
self._ended = true
|
|
37
|
+
self.push(null)
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
self._stream.write(chunk, encoding, callback)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
Inflate.prototype._flush = function (callback) {
|
|
45
|
+
if (this._stream && !this._ended) {
|
|
46
|
+
this._stream.once('end', callback)
|
|
47
|
+
this._stream.end()
|
|
48
|
+
} else {
|
|
49
|
+
callback()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates an intelligent inflate stream, that can handle deflate responses from older servers,
|
|
55
|
+
* which do not send the correct GZip headers in the response. See http://stackoverflow.com/a/37528114
|
|
56
|
+
* for details on why this is needed.
|
|
57
|
+
*
|
|
58
|
+
* @param {Object=} options - Are passed to the underlying `Inflate` or `InflateRaw` constructor.
|
|
59
|
+
*
|
|
60
|
+
* @returns {*}
|
|
61
|
+
*/
|
|
62
|
+
module.exports.createInflate = function (options) {
|
|
63
|
+
return new Inflate(options)
|
|
64
|
+
}
|