Projekt

Obecné

Profil

Stáhnout (8.18 KB) Statistiky
| Větev: | Revize:
1
var capability = require('./capability')
2
var inherits = require('inherits')
3
var response = require('./response')
4
var stream = require('readable-stream')
5
var toArrayBuffer = require('to-arraybuffer')
6

    
7
var IncomingMessage = response.IncomingMessage
8
var rStates = response.readyStates
9

    
10
function decideMode (preferBinary, useFetch) {
11
	if (capability.fetch && useFetch) {
12
		return 'fetch'
13
	} else if (capability.mozchunkedarraybuffer) {
14
		return 'moz-chunked-arraybuffer'
15
	} else if (capability.msstream) {
16
		return 'ms-stream'
17
	} else if (capability.arraybuffer && preferBinary) {
18
		return 'arraybuffer'
19
	} else if (capability.vbArray && preferBinary) {
20
		return 'text:vbarray'
21
	} else {
22
		return 'text'
23
	}
24
}
25

    
26
var ClientRequest = module.exports = function (opts) {
27
	var self = this
28
	stream.Writable.call(self)
29

    
30
	self._opts = opts
31
	self._body = []
32
	self._headers = {}
33
	if (opts.auth)
34
		self.setHeader('Authorization', 'Basic ' + new Buffer(opts.auth).toString('base64'))
35
	Object.keys(opts.headers).forEach(function (name) {
36
		self.setHeader(name, opts.headers[name])
37
	})
38

    
39
	var preferBinary
40
	var useFetch = true
41
	if (opts.mode === 'disable-fetch' || ('requestTimeout' in opts && !capability.abortController)) {
42
		// If the use of XHR should be preferred. Not typically needed.
43
		useFetch = false
44
		preferBinary = true
45
	} else if (opts.mode === 'prefer-streaming') {
46
		// If streaming is a high priority but binary compatibility and
47
		// the accuracy of the 'content-type' header aren't
48
		preferBinary = false
49
	} else if (opts.mode === 'allow-wrong-content-type') {
50
		// If streaming is more important than preserving the 'content-type' header
51
		preferBinary = !capability.overrideMimeType
52
	} else if (!opts.mode || opts.mode === 'default' || opts.mode === 'prefer-fast') {
53
		// Use binary if text streaming may corrupt data or the content-type header, or for speed
54
		preferBinary = true
55
	} else {
56
		throw new Error('Invalid value for opts.mode')
57
	}
58
	self._mode = decideMode(preferBinary, useFetch)
59
	self._fetchTimer = null
60

    
61
	self.on('finish', function () {
62
		self._onFinish()
63
	})
64
}
65

    
66
inherits(ClientRequest, stream.Writable)
67

    
68
ClientRequest.prototype.setHeader = function (name, value) {
69
	var self = this
70
	var lowerName = name.toLowerCase()
71
	// This check is not necessary, but it prevents warnings from browsers about setting unsafe
72
	// headers. To be honest I'm not entirely sure hiding these warnings is a good thing, but
73
	// http-browserify did it, so I will too.
74
	if (unsafeHeaders.indexOf(lowerName) !== -1)
75
		return
76

    
77
	self._headers[lowerName] = {
78
		name: name,
79
		value: value
80
	}
81
}
82

    
83
ClientRequest.prototype.getHeader = function (name) {
84
	var header = this._headers[name.toLowerCase()]
85
	if (header)
86
		return header.value
87
	return null
88
}
89

    
90
ClientRequest.prototype.removeHeader = function (name) {
91
	var self = this
92
	delete self._headers[name.toLowerCase()]
93
}
94

    
95
ClientRequest.prototype._onFinish = function () {
96
	var self = this
97

    
98
	if (self._destroyed)
99
		return
100
	var opts = self._opts
101

    
102
	var headersObj = self._headers
103
	var body = null
104
	if (opts.method !== 'GET' && opts.method !== 'HEAD') {
105
		if (capability.arraybuffer) {
106
			body = toArrayBuffer(Buffer.concat(self._body))
107
		} else if (capability.blobConstructor) {
108
			body = new global.Blob(self._body.map(function (buffer) {
109
				return toArrayBuffer(buffer)
110
			}), {
111
				type: (headersObj['content-type'] || {}).value || ''
112
			})
113
		} else {
114
			// get utf8 string
115
			body = Buffer.concat(self._body).toString()
116
		}
117
	}
118

    
119
	// create flattened list of headers
120
	var headersList = []
121
	Object.keys(headersObj).forEach(function (keyName) {
122
		var name = headersObj[keyName].name
123
		var value = headersObj[keyName].value
124
		if (Array.isArray(value)) {
125
			value.forEach(function (v) {
126
				headersList.push([name, v])
127
			})
128
		} else {
129
			headersList.push([name, value])
130
		}
131
	})
132

    
133
	if (self._mode === 'fetch') {
134
		var signal = null
135
		var fetchTimer = null
136
		if (capability.abortController) {
137
			var controller = new AbortController()
138
			signal = controller.signal
139
			self._fetchAbortController = controller
140

    
141
			if ('requestTimeout' in opts && opts.requestTimeout !== 0) {
142
				self._fetchTimer = global.setTimeout(function () {
143
					self.emit('requestTimeout')
144
					if (self._fetchAbortController)
145
						self._fetchAbortController.abort()
146
				}, opts.requestTimeout)
147
			}
148
		}
149

    
150
		global.fetch(self._opts.url, {
151
			method: self._opts.method,
152
			headers: headersList,
153
			body: body || undefined,
154
			mode: 'cors',
155
			credentials: opts.withCredentials ? 'include' : 'same-origin',
156
			signal: signal
157
		}).then(function (response) {
158
			self._fetchResponse = response
159
			self._connect()
160
		}, function (reason) {
161
			global.clearTimeout(self._fetchTimer)
162
			if (!self._destroyed)
163
				self.emit('error', reason)
164
		})
165
	} else {
166
		var xhr = self._xhr = new global.XMLHttpRequest()
167
		try {
168
			xhr.open(self._opts.method, self._opts.url, true)
169
		} catch (err) {
170
			process.nextTick(function () {
171
				self.emit('error', err)
172
			})
173
			return
174
		}
175

    
176
		// Can't set responseType on really old browsers
177
		if ('responseType' in xhr)
178
			xhr.responseType = self._mode.split(':')[0]
179

    
180
		if ('withCredentials' in xhr)
181
			xhr.withCredentials = !!opts.withCredentials
182

    
183
		if (self._mode === 'text' && 'overrideMimeType' in xhr)
184
			xhr.overrideMimeType('text/plain; charset=x-user-defined')
185

    
186
		if ('requestTimeout' in opts) {
187
			xhr.timeout = opts.requestTimeout
188
			xhr.ontimeout = function () {
189
				self.emit('requestTimeout')
190
			}
191
		}
192

    
193
		headersList.forEach(function (header) {
194
			xhr.setRequestHeader(header[0], header[1])
195
		})
196

    
197
		self._response = null
198
		xhr.onreadystatechange = function () {
199
			switch (xhr.readyState) {
200
				case rStates.LOADING:
201
				case rStates.DONE:
202
					self._onXHRProgress()
203
					break
204
			}
205
		}
206
		// Necessary for streaming in Firefox, since xhr.response is ONLY defined
207
		// in onprogress, not in onreadystatechange with xhr.readyState = 3
208
		if (self._mode === 'moz-chunked-arraybuffer') {
209
			xhr.onprogress = function () {
210
				self._onXHRProgress()
211
			}
212
		}
213

    
214
		xhr.onerror = function () {
215
			if (self._destroyed)
216
				return
217
			self.emit('error', new Error('XHR error'))
218
		}
219

    
220
		try {
221
			xhr.send(body)
222
		} catch (err) {
223
			process.nextTick(function () {
224
				self.emit('error', err)
225
			})
226
			return
227
		}
228
	}
229
}
230

    
231
/**
232
 * Checks if xhr.status is readable and non-zero, indicating no error.
233
 * Even though the spec says it should be available in readyState 3,
234
 * accessing it throws an exception in IE8
235
 */
236
function statusValid (xhr) {
237
	try {
238
		var status = xhr.status
239
		return (status !== null && status !== 0)
240
	} catch (e) {
241
		return false
242
	}
243
}
244

    
245
ClientRequest.prototype._onXHRProgress = function () {
246
	var self = this
247

    
248
	if (!statusValid(self._xhr) || self._destroyed)
249
		return
250

    
251
	if (!self._response)
252
		self._connect()
253

    
254
	self._response._onXHRProgress()
255
}
256

    
257
ClientRequest.prototype._connect = function () {
258
	var self = this
259

    
260
	if (self._destroyed)
261
		return
262

    
263
	self._response = new IncomingMessage(self._xhr, self._fetchResponse, self._mode, self._fetchTimer)
264
	self._response.on('error', function(err) {
265
		self.emit('error', err)
266
	})
267

    
268
	self.emit('response', self._response)
269
}
270

    
271
ClientRequest.prototype._write = function (chunk, encoding, cb) {
272
	var self = this
273

    
274
	self._body.push(chunk)
275
	cb()
276
}
277

    
278
ClientRequest.prototype.abort = ClientRequest.prototype.destroy = function () {
279
	var self = this
280
	self._destroyed = true
281
	global.clearTimeout(self._fetchTimer)
282
	if (self._response)
283
		self._response._destroyed = true
284
	if (self._xhr)
285
		self._xhr.abort()
286
	else if (self._fetchAbortController)
287
		self._fetchAbortController.abort()
288
}
289

    
290
ClientRequest.prototype.end = function (data, encoding, cb) {
291
	var self = this
292
	if (typeof data === 'function') {
293
		cb = data
294
		data = undefined
295
	}
296

    
297
	stream.Writable.prototype.end.call(self, data, encoding, cb)
298
}
299

    
300
ClientRequest.prototype.flushHeaders = function () {}
301
ClientRequest.prototype.setTimeout = function () {}
302
ClientRequest.prototype.setNoDelay = function () {}
303
ClientRequest.prototype.setSocketKeepAlive = function () {}
304

    
305
// Taken from http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader%28%29-method
306
var unsafeHeaders = [
307
	'accept-charset',
308
	'accept-encoding',
309
	'access-control-request-headers',
310
	'access-control-request-method',
311
	'connection',
312
	'content-length',
313
	'cookie',
314
	'cookie2',
315
	'date',
316
	'dnt',
317
	'expect',
318
	'host',
319
	'keep-alive',
320
	'origin',
321
	'referer',
322
	'te',
323
	'trailer',
324
	'transfer-encoding',
325
	'upgrade',
326
	'via'
327
]
(2-2/3)