├── index.html ├── README.md ├── .gitignore ├── .eslintrc.yaml ├── LICENSE.txt ├── docs ├── intro │ ├── XCeptor.png │ ├── index.html │ └── README.md └── index.html ├── tests ├── upload.html ├── abort.html ├── overrided.html ├── amd.html ├── without-new.html ├── responsetype.html ├── extends-from-xhr.html ├── header-case.html ├── illegal-invocation.html ├── consts.html ├── ng-file-upload.html ├── ng-file-upload-xceptor-first.html ├── thenable.html ├── sync.html ├── inject-with-prop.html ├── sync-load-event.html ├── loaded-duplicately.html ├── match-request.html ├── jquery2.html ├── full-sections-get.html ├── basic.html ├── addeventlistener.html └── all-methods.html ├── package.json └── src └── xceptor.js /index.html: -------------------------------------------------------------------------------- 1 | docs/index.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docs -> https://yanagieiichi.github.io/xceptor/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | .gitignore 3 | node_modules 4 | /xceptor.js 5 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: eslint:recommended 3 | env: 4 | browser: true 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | Copyright 2015 YanagiEiichi, yanagieiichi@web-tinker.com 3 | 4 | -------------------------------------------------------------------------------- /docs/intro/XCeptor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YanagiEiichi/xceptor/HEAD/docs/intro/XCeptor.png -------------------------------------------------------------------------------- /tests/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /tests/abort.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /tests/overrided.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /tests/amd.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /tests/without-new.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /tests/responsetype.html: -------------------------------------------------------------------------------- 1 | XCeptor 2 | 3 | 4 | 14 | -------------------------------------------------------------------------------- /tests/extends-from-xhr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /tests/header-case.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | -------------------------------------------------------------------------------- /tests/illegal-invocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 23 | -------------------------------------------------------------------------------- /tests/consts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | -------------------------------------------------------------------------------- /tests/ng-file-upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 |
20 | -------------------------------------------------------------------------------- /tests/ng-file-upload-xceptor-first.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /tests/thenable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 32 | -------------------------------------------------------------------------------- /tests/sync.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 29 | -------------------------------------------------------------------------------- /tests/inject-with-prop.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 29 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/intro/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/sync-load-event.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | -------------------------------------------------------------------------------- /tests/loaded-duplicately.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 18 | 19 | 35 | -------------------------------------------------------------------------------- /tests/match-request.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 40 | -------------------------------------------------------------------------------- /tests/jquery2.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 42 | -------------------------------------------------------------------------------- /tests/full-sections-get.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 47 | -------------------------------------------------------------------------------- /tests/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xceptor", 3 | "version": "0.4.0", 4 | "description": "AN INTERCEPTOR OF XHR", 5 | "main": "xceptor.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "prepublish": "global2umd src/xceptor.js XCeptor > xceptor.js", 11 | "test": "npm run lint && npm run prepublish && ui-tester-start tests", 12 | "lint": "eslint src" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/YanagiEiichi/xceptor.git" 17 | }, 18 | "author": "YanagiEiichi", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/YanagiEiichi/xceptor/issues" 22 | }, 23 | "homepage": "https://github.com/YanagiEiichi/xceptor#readme", 24 | "devDependencies": { 25 | "eslint": "^5.15.3", 26 | "global2umd": "0.0.1", 27 | "ui-tester": "^1.2.2" 28 | }, 29 | "reciperConfig": { 30 | "darkColor": "#333", 31 | "normalColor": "#666", 32 | "primaryColor": "#000", 33 | "logoUrl": "", 34 | "languages": [ 35 | "xml", 36 | "markdown", 37 | "bash" 38 | ], 39 | "home": "/xceptor/", 40 | "items": [ 41 | { 42 | "text": "Introduction", 43 | "href": "/xceptor/docs/intro/" 44 | }, 45 | { 46 | "text": "Github", 47 | "href": "//github.com/YanagiEiichi/xceptor/", 48 | "target": "_blank" 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/addeventlistener.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 64 | -------------------------------------------------------------------------------- /tests/all-methods.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 51 | -------------------------------------------------------------------------------- /docs/intro/README.md: -------------------------------------------------------------------------------- 1 | ## XCeptor 2 | 3 | An interceptor of [XHR](https://xhr.spec.whatwg.org/). 4 | 5 | ## Install 6 | 7 | ```bash 8 | npm install xceptor 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```javascript 14 | XCeptor.when(method, route, requestHandler, responseHandler); 15 | ``` 16 | 17 | | name | type | meaning | 18 | | --------------- | ---------------- | --------------------- | 19 | | method | RegExp or String | HTTP Method Matcher | 20 | | route | RegExp or String | Request Path Matcher | 21 | | requestHandler | Function | Request Hook Handler | 22 | | responseHandler | Function | Response Hook Handler | 23 | 24 | In addition, some shortcut methods are provided: 25 | 26 | ```javascript 27 | XCeptor.get(...args); 28 | XCeptor.post(...args); 29 | XCeptor.put(...args); 30 | XCeptor.delete(...args); 31 | XCeptor.patch(...args); 32 | ``` 33 | 34 | ## Schematic Diagram 35 | 36 |
37 |
38 | ## Demo
39 |
40 | ### 1. Mock a resource
41 |
42 | ```html
43 |
44 |
57 | ```
58 |
59 | ### 2. Go to login on 401
60 |
61 | ```html
62 |
63 |
69 | ```
70 |
--------------------------------------------------------------------------------
/src/xceptor.js:
--------------------------------------------------------------------------------
1 | var XCeptor = (function() { // eslint-disable-line no-unused-vars
2 |
3 | 'use strict';
4 |
5 | // Avoid duplicate runing
6 | if (XMLHttpRequest.XCeptor) return XMLHttpRequest.XCeptor;
7 |
8 | // Save original XMLHttpRequest class
9 | var OriginalXMLHttpRequest = XMLHttpRequest;
10 |
11 | // Handlers internal class
12 | var Handlers = function() {};
13 | // To use equivalence Checking
14 | Handlers.check = function(what, value) {
15 | // Note, use a '==' here, match 'null' or 'undefined'
16 | if (what === null || what === value) return true;
17 | // Check 'test' method, match RegExp or RegExp-like
18 | if (typeof what.test === 'function') return what.test(value);
19 | if (typeof what === 'function') return what(value);
20 | };
21 | Handlers.prototype = [];
22 | Handlers.prototype.solve = function(args, resolve, reject) {
23 | var handlers = this;
24 | // This is an asynchronous recursion to traverse handlers
25 | var iterator = function(cursor) {
26 | // This is an asynchronous recursion to resolve thenable resolve
27 | var fixResule = function(result) {
28 | switch (true) {
29 | case result === true: return resolve && resolve();
30 | case result === false: return reject && reject();
31 | // Resolve recursively thenable result
32 | case result && typeof result.then === 'function':
33 | return result.then(fixResule, function(error) { throw error; });
34 | default: iterator(cursor + 1);
35 | }
36 | };
37 | if (cursor < handlers.length) {
38 | fixResule(handlers[cursor].apply(null, args));
39 | } else {
40 | resolve && resolve();
41 | }
42 | };
43 | iterator(0);
44 | };
45 | Handlers.prototype.add = function(handler, method, route) {
46 | if (typeof handler !== 'function') return;
47 | this.push(function(request, response) {
48 | if (Handlers.check(method, request.method) && Handlers.check(route, request.url)) {
49 | return handler(request, response);
50 | }
51 | });
52 | };
53 |
54 | // Create two handlers objects
55 | var requestHandlers = new Handlers();
56 | var responseHandlers = new Handlers();
57 |
58 | var propPrefix = '__internal_';
59 | var propKeys = [
60 | 'readyState',
61 | 'timeout',
62 | 'upload',
63 | 'withCredentials',
64 | 'status',
65 | 'statusText',
66 | 'responseURL',
67 | 'responseType',
68 | 'response',
69 | 'responseText',
70 | 'responseXML'
71 | ];
72 |
73 | // To sync object keys with xhr
74 | var updateKeys = function(from, to, filter) {
75 | for (var i = 0, key; (key = propKeys[i]); i++) {
76 | if (filter && !filter.test(key)) continue;
77 | var xKey = propPrefix + key;
78 | var toKey = xKey in to ? xKey : key;
79 | var fromKey = xKey in from ? xKey : key;
80 | /**/ try { /* Fuck Android 4.3- and IE */ // eslint-disable-line no-multi-spaces
81 | /**/ void to[toKey], void from[fromKey]; // eslint-disable-line no-multi-spaces, indent
82 | /**/ } catch (error) { // eslint-disable-line no-multi-spaces
83 | /**/ continue; // eslint-disable-line no-multi-spaces, indent
84 | /**/ } // eslint-disable-line no-multi-spaces
85 | to[toKey] = from[fromKey];
86 | }
87 | };
88 |
89 | // Event internal class
90 | var Event = function(type, target) {
91 | this.type = type;
92 | this.target = target;
93 | };
94 |
95 | // SimpleEventModel internal decorator
96 | var SimpleEventDecorator = function(Constructor) {
97 | var heap = function(object, name) {
98 | var events = object.__events__;
99 | return name in events ? events[name] : events[name] = [];
100 | };
101 | var addEventListener = function(name, handler) {
102 | heap(this, name).push(handler);
103 | };
104 | var removeEventListener = function(name, handler) {
105 | var list = heap(this, name);
106 | for (var i = 0; i < list.length; i++) {
107 | if (list[i] === handler) list.splice(i, 1), i = 0 / 0;
108 | }
109 | };
110 | var wrapDispatchEvent = function(context) {
111 | var dispatchEvent = function(event) {
112 | var list = heap(this, event.type);
113 | for (var i = 0; i < list.length; i++) list[i].call(context, event);
114 | var key = 'on' + event.type;
115 | if (typeof this[key] === 'function') this[key].call(context, event);
116 | };
117 | return dispatchEvent;
118 | };
119 | var SimpleEventModel = function() {
120 | Constructor.apply(this, arguments);
121 | var nullDesc = { value: null, writable: true, configurable: true };
122 | Object.defineProperties(this, {
123 | onabort: nullDesc,
124 | onerror: nullDesc,
125 | onload: nullDesc,
126 | onloadend: nullDesc,
127 | onloadstart: nullDesc,
128 | onprogress: nullDesc,
129 | onreadystatechange: nullDesc,
130 | ontimeout: nullDesc,
131 | addEventListener: { value: addEventListener, configurable: true },
132 | removeEventListener: { value: removeEventListener, configurable: true },
133 | dispatchEvent: { value: wrapDispatchEvent(this), configurable: true },
134 | __events__: { value: {}, configurable: true }
135 | });
136 | };
137 | SimpleEventModel.prototype = Constructor.prototype;
138 | return SimpleEventModel;
139 | };
140 |
141 | /* Main Process */
142 |
143 | // Create interceptor
144 | var HijackedXHR = function() {
145 | if (!(this instanceof HijackedXHR)) throw new TypeError('Failed to construct \'XMLHttpRequest\': Please use the \'new\' operator, this DOM object constructor cannot be called as a function.');
146 | var xceptor = this;
147 | var xhr = new OriginalXMLHttpRequest();
148 | // Init prop slots
149 | void function() {
150 | for (var i = 0, key; (key = propKeys[i]); i++) {
151 | Object.defineProperty(xceptor, propPrefix + key, {
152 | configurable: true, writable: true, value: xhr[key]
153 | });
154 | }
155 | }();
156 | // Update default values
157 | var request = {
158 | method: null,
159 | url: null,
160 | isAsync: true,
161 | username: void 0,
162 | password: void 0,
163 | headers: [],
164 | overridedMimeType: void 0,
165 | timeout: xceptor.timeout,
166 | withCredentials: xceptor.withCredentials,
167 | responseType: ''
168 | };
169 | var response = { status: xceptor.status, statusText: xceptor.statusText, headers: [] };
170 | var trigger = function(name) { xceptor.dispatchEvent(new Event(name, xceptor)); };
171 | var complete = function() {
172 | responseHandlers.solve([xceptor.__request, xceptor.__response], function() {
173 | updateKeys(xceptor.__response, xceptor);
174 | });
175 | };
176 | Object.defineProperty(xceptor, '__originalXHR', { value: xhr, configurable: true });
177 | Object.defineProperty(xceptor, '__request', { value: request, configurable: true });
178 | Object.defineProperty(xceptor, '__response', { value: response, configurable: true });
179 | Object.defineProperty(xceptor, '__trigger', { value: trigger, configurable: true });
180 | Object.defineProperty(xceptor, '__complete', { value: complete, configurable: true });
181 | var updateResponseHeaders = function() {
182 | if (updateResponseHeaders.disabled) return;
183 | updateResponseHeaders.disabled = true;
184 | response.headers.splice(0, response.headers.length);
185 | response.status = xhr.status;
186 | response.statusText = xhr.statusText;
187 | xhr.getAllResponseHeaders().replace(/.+/g, function($0) {
188 | var result = $0.match(/(^.*?): (.*$)/);
189 | if (!result) return;
190 | response.headers.push({ header: result[1], value: result[2] });
191 | });
192 | };
193 | // Mapping response
194 | updateKeys(xhr, response, /^response/);
195 | // Mapping events
196 | void function() {
197 | xhr.onreadystatechange = function() {
198 | updateKeys(xhr, xceptor);
199 | updateKeys(xhr, response);
200 | if (xhr.readyState > 1) updateResponseHeaders();
201 | if (xhr.readyState === 4) {
202 | complete();
203 | if (request.isAsync) {
204 | setTimeout(function() { trigger('load'); });
205 | } else {
206 | trigger('load');
207 | }
208 | }
209 | trigger('readystatechange');
210 | };
211 | var events = [ 'error', 'timeout', 'abort' ];
212 | var buildEvent = function(name) {
213 | xhr['on' + name] = function() {
214 | xceptor.readyState = xhr.readyState;
215 | trigger(name);
216 | };
217 | };
218 | for (var i = 0; i < events.length; i++) buildEvent(events[i]);
219 | }();
220 | };
221 | HijackedXHR.prototype = Object.create(OriginalXMLHttpRequest.prototype);
222 |
223 | // Methods mapping
224 | HijackedXHR.prototype.open = function(method, url, isAsync, username, password) {
225 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
226 | var request = this.__request;
227 | // Save to 'request'
228 | request.method = (method + '').toUpperCase();
229 | request.url = url + '';
230 | if (isAsync !== void 0) request.isAsync = !!(isAsync * 1);
231 | if (username !== void 0) request.username = username + '';
232 | if (password !== void 0) request.password = password + '';
233 | };
234 |
235 | HijackedXHR.prototype.setRequestHeader = function(header, value) {
236 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
237 | // Save to 'headers'
238 | this.__request.headers.push({ header: header + '', value: value });
239 | };
240 |
241 | HijackedXHR.prototype.overrideMimeType = function(mimetype) {
242 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
243 | // Save to 'request'
244 | this.__request.overridedMimeType = mimetype;
245 | };
246 |
247 | HijackedXHR.prototype.getResponseHeader = function(header) {
248 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
249 | // Read from 'response'
250 | var headers = this.__response.headers;
251 | header = String(header).toLowerCase();
252 | for (var i = 0; i < headers.length; i++) {
253 | if (headers[i].header.toLowerCase() === header) return headers[i].value;
254 | }
255 | return null;
256 | };
257 |
258 | HijackedXHR.prototype.getAllResponseHeaders = function() {
259 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
260 | // Read from 'response'
261 | var response = this.__response;
262 | var headers = response.headers;
263 | var allHeaders = [];
264 | for (var i = 0; i < response.headers.length; i++) {
265 | allHeaders.push(headers[i].header + ': ' + headers[i].value);
266 | }
267 | return allHeaders.join('\r\n');
268 | };
269 |
270 | HijackedXHR.prototype.send = function(data) {
271 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
272 | // Copy setter properties to 'request'
273 | var request = this.__request;
274 | var response = this.__response;
275 | request.data = data;
276 | request.withCredentials = this.withCredentials;
277 | request.timeout = this.timeout;
278 | request.responseType = this.responseType;
279 | var that = this;
280 | // Invoke interceptor
281 | requestHandlers.solve([request, response], function() {
282 | // Actual actions
283 | var xhr = that.__originalXHR;
284 | xhr.open(request.method, request.url, request.isAsync, request.username, request.password);
285 | for (var i = 0; i < request.headers.length; i++) {
286 | xhr.setRequestHeader(request.headers[i].header, request.headers[i].value);
287 | }
288 | if (request.overridedMimeType !== void 0) xhr.overrideMimeType(request.overridedMimeType);
289 | // Assigning before changes, because it may be thrown in sync mode
290 | if (xhr.withCredentials !== request.withCredentials) xhr.withCredentials = request.withCredentials;
291 | if (xhr.timeout !== request.timeout) xhr.timeout = request.timeout;
292 | if (xhr.responseType !== request.responseType) xhr.responseType = request.responseType;
293 | xhr.send(request.data);
294 | }, function() {
295 | // Fake actions
296 | var action = function() {
297 | response.readyState = 3;
298 | updateKeys(response, that);
299 | that.__trigger('readystatechange');
300 | response.readyState = 4;
301 | updateKeys(response, that);
302 | that.__complete();
303 | that.__trigger('readystatechange');
304 | that.__trigger('load');
305 | };
306 | // Fake async
307 | if (request.isAsync) {
308 | setTimeout(action);
309 | } else {
310 | action();
311 | }
312 | });
313 | };
314 |
315 | HijackedXHR.prototype.abort = function() {
316 | if (!(this instanceof HijackedXHR)) throw new TypeError('Illegal invocation');
317 | this.__originalXHR.abort();
318 | };
319 |
320 | // Set accessor props
321 | void function() {
322 | for (var i = 0, key; (key = propKeys[i]); i++) void function(key) {
323 | Object.defineProperty(HijackedXHR.prototype, key, {
324 | configurable: true,
325 | enumerable: true,
326 | set: function(value) { this[propPrefix + key] = value; },
327 | get: function() { return this[propPrefix + key]; }
328 | });
329 | }(key);
330 | }();
331 |
332 | HijackedXHR = SimpleEventDecorator(HijackedXHR);
333 |
334 | // Copy constant names to constructor and prototype
335 | var constantNames = [ 'UNSENT', 'OPENED', 'HEADERS_RECEIVED', 'LOADING', 'DONE' ];
336 | for (var i = 0; i < constantNames.length; i++) {
337 | Object.defineProperty(HijackedXHR, constantNames[i], {
338 | value: OriginalXMLHttpRequest[constantNames[i]],
339 | enumerable: true,
340 | configurable: true
341 | });
342 | }
343 |
344 | // Exports
345 | window.XMLHttpRequest = HijackedXHR;
346 |
347 | // Define xceptor methods
348 | var XCeptor = new function() {
349 | var that = this;
350 | this.when = function(method, route, requestHandler, responseHandler) {
351 | requestHandlers.add(requestHandler, method, route);
352 | responseHandlers.add(responseHandler, method, route);
353 | };
354 | void function() {
355 | var methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEADER', 'OPTIONS' ];
356 | for (var i = 0; i < methods.length; i++) void function(method) {
357 | that[method.toLowerCase()] = function() {
358 | var args = Array.prototype.slice.call(arguments);
359 | return that.when.apply(that, [method].concat(args));
360 | };
361 | }(methods[i]);
362 | }();
363 | };
364 |
365 | Object.defineProperty(HijackedXHR, 'XCeptor', { value: XCeptor, configurable: true });
366 |
367 | return XCeptor;
368 |
369 | })();
370 |
--------------------------------------------------------------------------------