├── 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 | --------------------------------------------------------------------------------