This repository has been archived by the owner on Dec 14, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 353
/
fetch.js
503 lines (425 loc) · 14.7 KB
/
fetch.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
// Work-In-Progress 'prollyfill' for Fetch API
// Standard: https://fetch.spec.whatwg.org/#fetch-api
//
// As usual, the intent is to produce a forward-compatible
// subset so that code can be written using future standard
// functionality; not every case is considered or supported.
// Requires ES2015: Promise, Symbol.iterator (or polyfill)
// Requires: URL (or polyfill)
// Example:
// fetch('README.md')
// .then(function(response) { return response.text(); })
// .then(function(text) { alert(text); });
(function(global) {
'use strict';
// Web IDL concepts
// https://heycam.github.io/webidl/#idl-ByteString
function ByteString(value) {
value = String(value);
if (value.match(/[^\x00-\xFF]/)) throw TypeError('Not a valid ByteString');
return value;
}
// https://heycam.github.io/webidl/#idl-USVString
function USVString(value) {
value = String(value);
return value.replace(
/([\u0000-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF])/g,
function (c) {
if (/^[\uD800-\uDFFF]$/.test(c)) return '\uFFFD';
return c;
});
}
function ushort(x) { return x & 0xFFFF; }
// 2 Terminology
function byteLowerCase(s) {
return String(s).replace(/[A-Z]/g, function(c) { return c.toLowerCase(); });
}
function byteUpperCase(s) {
return String(s).replace(/[a-z]/g, function(c) { return c.toUpperCase(); });
}
function byteCaseInsensitiveMatch(a, b) {
return byteLowerCase(a) === byteLowerCase(b);
}
// 2.1 HTTP
// 2.1.1 Methods
function isForbiddenMethod(m) {
m = byteUpperCase(m);
return m === 'CONNECT' || m === 'TRACE' || m === 'TRACK';
}
function normalizeMethod(m) {
var u = byteUpperCase(m);
if (u === 'DELETE' || u === 'GET' || u === 'HEAD' || u === 'OPTIONS' ||
u === 'POST' || u === 'PUT') return u;
return m;
}
function isName(s) {
return /^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/.test(s);
}
function isValue(s) {
// TODO: Implement me
return true;
}
function isForbiddenHeaderName(n) {
n = String(n).toLowerCase();
var forbidden = {
'accept-charset': true,
'accept-encoding': true,
'access-control-request-headers': true,
'access-control-request-method': true,
'connection': true,
'content-length': true,
'cookie': true,
'cookie2': true,
'date': true,
'dnt': true,
'expect': true,
'host': true,
'keep-alive': true,
'origin': true,
'referer': true,
'te': true,
'trailer': true,
'transfer-encoding': true,
'upgrade': true,
'user-agent': true,
'via': true
};
return forbidden[n] || n.substring(0, 6) === 'proxy-' || n.substring(0, 4) === 'sec-';
}
function isForbiddenResponseHeaderName(n) {
n = String(n).toLowerCase();
var forbidden = {
'set-cookie': true,
'set-cookie2': true
};
return forbidden[n];
}
function isSimpleHeader(name, value) {
name = String(name).toLowerCase();
return name === 'accept' || name === 'accept-language' || name === 'content-language' ||
(name === 'content-type' &&
['application/x-www-form-encoded', 'multipart/form-data', 'text/plain'].indexOf(value) !== -1);
}
//
// 5.1 Headers class
//
// typedef (Headers or sequence<sequence<ByteString>> or OpenEndedDictionary<ByteString>) HeadersInit;
// Constructor(optional HeadersInit init)
function Headers(init) {
this._guard = 'none';
this._headerList = [];
if (init) fill(this, init);
}
function fill(headers, init) {
if (init instanceof Headers) {
init._headerList.forEach(function(header) {
headers.append(header[0], header[1]);
});
} else if (Array.isArray(init)) {
init.forEach(function(header) {
if (!Array.isArray(header) || header.length !== 2) throw TypeError();
headers.append(header[0], header[1]);
});
} else {
init = Object(init);
Object.keys(init).forEach(function(key) {
headers.append(key, init[key]);
});
}
}
// interface Headers
Headers.prototype = {
// void append(ByteString name, ByteString value);
append: function append(name, value) {
name = ByteString(name);
if (!isName(name) || !isValue(value)) throw TypeError();
if (this._guard === 'immutable') throw TypeError();
else if (this._guard === 'request' && isForbiddenHeaderName(name)) return;
else if (this._guard === 'request-no-CORS' && !isSimpleHeader(name, value)) return;
else if (this._guard === 'response' && isForbiddenResponseHeaderName(name)) return;
name = name.toLowerCase();
this._headerList.push([name, value]);
},
// void delete(ByteString name);
'delete': function delete_(name) {
name = ByteString(name);
if (!isName(name)) throw TypeError();
if (this._guard === 'immutable') throw TypeError();
else if (this._guard === 'request' && isForbiddenHeaderName(name)) return;
else if (this._guard === 'request-no-CORS' && !isSimpleHeader(name, 'invalid')) return;
else if (this._guard === 'response' && isForbiddenResponseHeaderName(name)) return;
name = name.toLowerCase();
var index = 0;
while (index < this._headerList.length) {
if (this._headerList[index][0] === name)
this._headerList.splice(index, 1);
else
++index;
}
},
// ByteString? get(ByteString name);
get: function get(name) {
name = ByteString(name);
if (!isName(name)) throw TypeError();
name = name.toLowerCase();
for (var index = 0; index < this._headerList.length; ++index) {
if (this._headerList[index][0] === name)
return this._headerList[index][1];
}
return null;
},
// sequence<ByteString> getAll(ByteString name);
getAll: function getAll(name) {
name = ByteString(name);
if (!isName(name)) throw TypeError();
name = name.toLowerCase();
var sequence = [];
for (var index = 0; index < this._headerList.length; ++index) {
if (this._headerList[index][0] === name)
sequence.push(this._headerList[index][1]);
}
return sequence;
},
// boolean has(ByteString name);
has: function has(name) {
name = ByteString(name);
if (!isName(name)) throw TypeError();
name = name.toLowerCase();
for (var index = 0; index < this._headerList.length; ++index) {
if (this._headerList[index][0] === name)
return true;
}
return false;
},
// void set(ByteString name, ByteString value);
set: function set(name, value) {
name = ByteString(name);
if (!isName(name) || !isValue(value)) throw TypeError();
if (this._guard === 'immutable') throw TypeError();
else if (this._guard === 'request' && isForbiddenHeaderName(name)) return;
else if (this._guard === 'request-no-CORS' && !isSimpleHeader(name, value)) return;
else if (this._guard === 'response' && isForbiddenResponseHeaderName(name)) return;
name = name.toLowerCase();
for (var index = 0; index < this._headerList.length; ++index) {
if (this._headerList[index][0] === name) {
this._headerList[index++][1] = value;
while (index < this._headerList.length) {
if (this._headerList[index][0] === name)
this._headerList.splice(index, 1);
else
++index;
}
return;
}
}
this._headerList.push([name, value]);
}
};
Headers.prototype[Symbol.iterator] = function() {
return new HeadersIterator(this);
};
function HeadersIterator(headers) {
this._headers = headers;
this._index = 0;
}
HeadersIterator.prototype = {};
HeadersIterator.prototype.next = function() {
if (this._index >= this._headers._headerList.length)
return { value: undefined, done: true };
return { value: this._headers._headerList[this._index++], done: false };
};
HeadersIterator.prototype[Symbol.iterator] = function() { return this; };
//
// 5.2 Body mixin
//
function Body(_stream) {
// TODO: Handle initialization from other types
this._stream = _stream;
this.bodyUsed = false;
}
// interface FetchBodyStream
Body.prototype = {
// Promise<ArrayBuffer> arrayBuffer();
arrayBuffer: function() {
if (this.bodyUsed) return Promise.reject(TypeError());
this.bodyUsed = true;
if (this._stream instanceof ArrayBuffer) return Promise.resolve(this._stream);
var value = this._stream;
return new Promise(function(resolve, reject) {
var octets = unescape(encodeURIComponent(value)).split('').map(function(c) {
return c.charCodeAt(0);
});
resolve(new Uint8Array(octets).buffer);
});
},
// Promise<Blob> blob();
blob: function() {
if (this.bodyUsed) return Promise.reject(TypeError());
this.bodyUsed = true;
if (this._stream instanceof Blob) return Promise.resolve(this._stream);
return Promise.resolve(new Blob([this._stream]));
},
// Promise<FormData> formData();
formData: function() {
if (this.bodyUsed) return Promise.reject(TypeError());
this.bodyUsed = true;
if (this._stream instanceof FormData) return Promise.resolve(this._stream);
return Promise.reject(Error('Not yet implemented'));
},
// Promise<JSON> json();
json: function() {
if (this.bodyUsed) return Promise.reject(TypeError());
this.bodyUsed = true;
var that = this;
return new Promise(function(resolve, reject) {
resolve(JSON.parse(that._stream));
});
},
// Promise<USVString> text();
text: function() {
if (this.bodyUsed) return Promise.reject(TypeError());
this.bodyUsed = true;
return Promise.resolve(String(this._stream));
}
};
//
// 5.3 Request class
//
// typedef (Request or USVString) RequestInfo;
// Constructor(RequestInfo input, optional RequestInit init)
function Request(input, init) {
if (arguments.length < 1) throw TypeError('Not enough arguments');
Body.call(this, null);
// readonly attribute ByteString method;
this.method = 'GET';
// readonly attribute USVString url;
this.url = '';
// readonly attribute Headers headers;
this.headers = new Headers();
this.headers._guard = 'request';
// readonly attribute DOMString referrer;
this.referrer = null; // TODO: Implement.
// readonly attribute RequestMode mode;
this.mode = null; // TODO: Implement.
// readonly attribute RequestCredentials credentials;
this.credentials = 'omit';
if (input instanceof Request) {
if (input.bodyUsed) throw TypeError();
input.bodyUsed = true;
this.method = input.method;
this.url = input.url;
this.headers = new Headers(input.headers);
this.headers._guard = input.headers._guard;
this.credentials = input.credentials;
this._stream = input._stream;
} else {
input = USVString(input);
this.url = String(new URL(input, self.location));
}
init = Object(init);
if ('method' in init) {
var method = ByteString(init.method);
if (isForbiddenMethod(method)) throw TypeError();
this.method = normalizeMethod(method);
}
if ('headers' in init) {
this.headers = new Headers();
fill(this.headers, init.headers);
}
if ('body' in init)
this._stream = init.body;
if ('credentials' in init &&
(['omit', 'same-origin', 'include'].indexOf(init.credentials) !== -1))
this.credentials = init.credentials;
}
// interface Request
Request.prototype = Body.prototype;
//
// 5.4 Response class
//
// Constructor(optional FetchBodyInit body, optional ResponseInit init)
function Response(body, init) {
if (arguments.length < 1)
body = '';
this.headers = new Headers();
this.headers._guard = 'response';
// Internal
if (body instanceof XMLHttpRequest && '_url' in body) {
var xhr = body;
this.type = 'basic'; // TODO: ResponseType
this.url = USVString(xhr._url);
this.status = xhr.status;
this.ok = 200 <= this.status && this.status <= 299;
this.statusText = xhr.statusText;
xhr.getAllResponseHeaders()
.split(/\r?\n/)
.filter(function(header) { return header.length; })
.forEach(function(header) {
var i = header.indexOf(':');
this.headers.append(header.substring(0, i), header.substring(i + 2));
}, this);
Body.call(this, xhr.responseText);
return;
}
Body.call(this, body);
init = Object(init) || {};
// readonly attribute USVString url;
this.url = '';
// readonly attribute unsigned short status;
var status = 'status' in init ? ushort(init.status) : 200;
if (status < 200 || status > 599) throw RangeError();
this.status = status;
// readonly attribute boolean ok;
this.ok = 200 <= this.status && this.status <= 299;
// readonly attribute ByteString statusText;
var statusText = 'statusText' in init ? String(init.statusText) : 'OK';
if (/[^\x00-\xFF]/.test(statusText)) throw TypeError();
this.statusText = statusText;
// readonly attribute Headers headers;
if ('headers' in init) fill(this.headers, init);
// TODO: Implement these
// readonly attribute ResponseType type;
this.type = 'basic'; // TODO: ResponseType
}
// interface Response
Response.prototype = Body.prototype;
Response.redirect = function() {
// TODO: Implement?
throw Error('Not supported');
};
//
// 5.5 Structured cloning of Headers, FetchBodyStream, Request, Response
//
//
// 5.6 Fetch method
//
// Promise<Response> fetch(RequestInfo input, optional RequestInit init);
function fetch(input, init) {
return new Promise(function(resolve, reject) {
var r = new Request(input, init);
var xhr = new XMLHttpRequest(), async = true;
xhr._url = r.url;
try { xhr.open(r.method, r.url, async); } catch (e) { throw TypeError(e.message); }
for (var iter = r.headers[Symbol.iterator](), step = iter.next();
!step.done; step = iter.next())
xhr.setRequestHeader(step.value[0], step.value[1]);
if (r.credentials === 'include')
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE) return;
if (xhr.status === 0)
reject(new TypeError('Network error'));
else
resolve(new Response(xhr));
};
xhr.send(r._stream);
});
}
// Exported
if (!('fetch' in global)) {
global.Headers = Headers;
global.Request = Request;
global.Response = Response;
global.fetch = fetch;
}
}(self));