├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── batchRequestor.js ├── headerExtractor.js ├── httpClient.js └── multiFetch.js ├── package.json └── test ├── batchRequestor.js ├── httpClient.js ├── multiFetch.js └── multiFetch_customHost.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.11" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 marmelab 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 |
archivedArchived Repository
5 | This code is no longer maintained. Feel free to fork it, but use it at your own risks. 6 |
9 | 10 | # koa-multifetch 11 | 12 | A simple [koa.js](http://koajs.com/) middleware to multiplex several HTTP requests into one. Very useful on mobile devices where latency can kill performance. Based on [Facebook's Batch Requests philosophy](https://developers.facebook.com/docs/graph-api/making-multiple-requests), and inspired by [multifetch](https://github.com/e-conomic/multifetch) for Express. 13 | 14 | ## Installation 15 | 16 | ```sh 17 | npm install koa-multifetch 18 | ``` 19 | 20 | After installing the package, mount the middleware on any URL, using [koa-route](https://github.com/koajs/route). For example, to add `/multi` as a multifetch endpoint: 21 | 22 | ```js 23 | var multifetch = require('koa-multifetch')(); 24 | var koaRoute = require('koa-route'); 25 | app.use(koaRoute.post('/multi', multifetch)); 26 | ``` 27 | 28 | By default koa-multifetch use relative url. In this example all request will be prepended with "/multi". 29 | 30 | But koa-multifetch can take an object of options as parameter, object that can have two properties: 31 | 32 | 1) If you want to allow koa-multifetch to send request anywhere on your site, enable absoluteUrl option with absolute property by calling 33 | ```js 34 | var multifetch = require('koa-multifetch')({absolute: true}); // enable absolute url, absolute is false by default 35 | ``` 36 | 37 | 2) If you want to force koa-multifetch to send request on a specific host (for exemple if you are behind a load balancer on level 7), enable custom host option with header_host property by calling 38 | ```js 39 | var multifetch = require('koa-multifetch')({header_host: 'http://localhost:3000'}); // set a custom host for multiFetch requests 40 | ``` 41 | 42 | koa-multifetch can be mounted both as a GET and POST route. 43 | 44 | ## Usage 45 | 46 | Send a request to the route where the middleware is listening, passing the requests to fetch in parallel as a JSON object in the request body. For instance, to fetch `/products/1` and `/users` in parallel, make the following request: 47 | 48 | ``` 49 | POST /multi HTTP/1.1 50 | Content-Type: application/json 51 | { 52 | "product": "/products/1", 53 | "all_users": "/users" 54 | } 55 | ``` 56 | 57 | The middleware will call both HTTP resources, and return a response with a composite body once all the requests are fetched: 58 | ```js 59 | { 60 | "product": { 61 | "code":"200", 62 | "headers":[ 63 | { "name": "Content-Type", "value": "text/javascript; charset=UTF-8" } 64 | ], 65 | "body": "{ id: 1, name: \"ipad2\", stock: 34 }" 66 | }, 67 | "all_users": { 68 | "code":"200", 69 | "headers":[ 70 | { "name": "Content-Type", "value": "text/javascript; charset=UTF-8" } 71 | ], 72 | "body": "[{ id: 2459, login: \"john\" }, { id: 7473, login: \"jane\" }]" 73 | } 74 | } 75 | ``` 76 | 77 | Any header present in the multifetch request will be automatically added to all sub-requests. 78 | 79 | **Tip**: When mounted as a GET route, koa-multifetch reads the query parameters to determine the requests to fetch: 80 | 81 | ``` 82 | GET /multi?product=/product/1&all_users=/users HTTP/1.1 83 | ``` 84 | 85 | ## License 86 | 87 | koa-multifetch is licensed under the [MIT Licence](LICENSE), courtesy of [marmelab](http://marmelab.com). 88 | -------------------------------------------------------------------------------- /lib/batchRequestor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var headerExtractor = require('./headerExtractor'); 4 | 5 | module.exports = function (requestor) { 6 | return function* batchRequestor(requests, baseUrl, headers) { 7 | var results = {}, 8 | resultsHeaders = {}, 9 | requestsThunks = [], 10 | i = 0, 11 | name; 12 | 13 | for (name in requests) { 14 | requestsThunks[i] = requestor.get(baseUrl + requests[name], headers); 15 | results[name] = i; 16 | i++; 17 | } 18 | 19 | var resultsArray = yield requestsThunks, totalResults = 0; 20 | 21 | for (name in results) { 22 | results[name] = resultsArray[results[name]]; 23 | if (!results[name]) { 24 | continue; 25 | } 26 | 27 | ++totalResults; 28 | 29 | resultsHeaders[name] = headerExtractor.extract(results[name].headers); 30 | } 31 | 32 | var responseHeaders = headerExtractor.compute(resultsHeaders, totalResults); 33 | 34 | return { body: results, headers: responseHeaders }; 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /lib/headerExtractor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /* Extracted from https://github.com/hapijs/wreck library */ 5 | _parseCacheControl: function (field) { 6 | /* 7 | Cache-Control = 1#cache-directive 8 | cache-directive = token [ "=" ( token / quoted-string ) ] 9 | token = [^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+ 10 | quoted-string = "(?:[^"\\]|\\.)*" 11 | */ 12 | 13 | // 1: directive = 2: token 3: quoted-string 14 | var regex = /(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\]|\\.)*)\")))?/g; 15 | 16 | var header = {}; 17 | var err = field.replace(regex, function ($0, $1, $2, $3) { 18 | var value = $2 || $3; 19 | header[$1] = value ? value.toLowerCase() : true; 20 | 21 | return ''; 22 | }); 23 | 24 | var integerFieldValue; 25 | for (var integerField in ['max-age', 's-maxage']) { 26 | if (integerField in header) { 27 | try { 28 | integerFieldValue = parseInt(header[integerField], 10); 29 | if (isNaN(integerFieldValue)) { 30 | return null; 31 | } 32 | 33 | header[integerField] = integerFieldValue; 34 | } 35 | catch (err) { } 36 | } 37 | } 38 | 39 | return (err ? null : header); 40 | }, 41 | 42 | extract: function (headers) { 43 | var header, 44 | cacheControl = false, 45 | varies = []; 46 | 47 | for (var i in headers) { 48 | header = headers[i]; 49 | 50 | switch (header.name.toLowerCase()) { 51 | case 'cache-control': 52 | var parsedCacheControl = this._parseCacheControl(header.value); 53 | 54 | cacheControl = { 55 | maxAge: 0, 56 | sharedMaxAge: 0, 57 | 'private': false, 58 | 'public': false, 59 | noCache: false 60 | }; 61 | 62 | if ('no-cache' in parsedCacheControl) { 63 | cacheControl.noCache = true; 64 | 65 | break; 66 | } 67 | 68 | if ('private' in parsedCacheControl) { 69 | cacheControl['private'] = true; 70 | } 71 | 72 | if ('public' in parsedCacheControl) { 73 | cacheControl['public'] = true; 74 | } 75 | 76 | if ('max-age' in parsedCacheControl) { 77 | cacheControl.maxAge = parsedCacheControl['max-age']; 78 | } 79 | 80 | if ('s-maxage' in parsedCacheControl) { 81 | cacheControl.sharedMaxAge = parsedCacheControl['s-maxage']; 82 | } 83 | 84 | break; 85 | 86 | case 'vary': 87 | header.value.split(',').forEach(function (vary) { 88 | varies.push(vary.trim()); 89 | }); 90 | break; 91 | } 92 | } 93 | 94 | return { 95 | cacheControl: cacheControl, 96 | varies: varies 97 | }; 98 | }, 99 | 100 | compute: function (headers, totalResults) { 101 | var totalCacheControl = 0, name; 102 | 103 | for (name in headers) { 104 | if (false === headers[name].cacheControl) { 105 | continue; 106 | } 107 | 108 | // if one of header contain no-cache so force to no-cache 109 | if (headers[name].cacheControl.noCache) { 110 | return { 'Cache-Control': 'no-cache' }; 111 | } 112 | 113 | ++totalCacheControl; 114 | } 115 | 116 | if (totalCacheControl !== totalResults) { 117 | return {}; 118 | } 119 | 120 | var responseHeaders = {}, 121 | privateCache = false, 122 | publicCache = false, 123 | maxMaxAge = 0, 124 | maxSharedMaxAge = 0, 125 | currentCacheControl = {}, 126 | currentVaries = [], 127 | varies = []; 128 | 129 | for (name in headers) { 130 | currentCacheControl = headers[name].cacheControl; 131 | currentVaries = headers[name].varies; 132 | 133 | if (currentCacheControl.maxAge > maxMaxAge) { 134 | maxMaxAge = currentCacheControl.maxAge; 135 | } 136 | 137 | if (currentCacheControl.sharedMaxAge > maxSharedMaxAge) { 138 | maxSharedMaxAge = currentCacheControl.sharedMaxAge; 139 | } 140 | 141 | if (currentCacheControl['private']) { 142 | privateCache = true; 143 | } 144 | 145 | if (currentCacheControl['public']) { 146 | publicCache = true; 147 | } 148 | 149 | for (var i in currentVaries) { 150 | if (-1 === varies.indexOf(currentVaries[i])) { 151 | varies.push(currentVaries[i]); 152 | } 153 | } 154 | } 155 | 156 | if (privateCache || publicCache || maxMaxAge || maxSharedMaxAge) { 157 | var cacheControl = []; 158 | 159 | if (privateCache) { 160 | cacheControl.push('private'); 161 | } else if (publicCache) { 162 | cacheControl.push('public'); 163 | } 164 | 165 | if (maxMaxAge) { 166 | cacheControl.push('max-age=' + maxMaxAge); 167 | } 168 | 169 | if (maxSharedMaxAge) { 170 | cacheControl.push('s-maxage=' + maxSharedMaxAge); 171 | } 172 | 173 | responseHeaders['Cache-Control'] = cacheControl.join(", "); 174 | } 175 | 176 | if (varies.length) { 177 | responseHeaders.Vary = varies.join(", "); 178 | } 179 | 180 | return responseHeaders; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /lib/httpClient.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (superagent) { 4 | 5 | // co compatible thunk with node style callback 6 | var getUrl = function (url, headers) { 7 | return function (cb) { 8 | 9 | var request = superagent.get(url) 10 | .on('error', function (error) { 11 | // throw error; 12 | cb(error); 13 | }); 14 | 15 | for(var name in headers) { 16 | request.set(name, headers[name]); 17 | } 18 | 19 | request.end(function (res) { 20 | // return res; 21 | cb(null, res); 22 | }); 23 | }; 24 | }; 25 | 26 | function* get(url, headers) { 27 | 28 | headers = headers || {}; 29 | try { 30 | var response = yield getUrl(url, headers); 31 | } catch (error) { 32 | switch(error.code) { 33 | case 'ENOTFOUND': 34 | return { 35 | code: 404, 36 | }; 37 | case 'ECONNREFUSED': 38 | return { 39 | code: 403, 40 | }; 41 | default: 42 | return { 43 | code: 500, 44 | body: error.code + ': ' + error.message 45 | }; 46 | } 47 | } 48 | 49 | if (response.code) { 50 | return response; 51 | } 52 | 53 | var result = { 54 | code: response.status, 55 | headers: [], 56 | body: response.body 57 | }; 58 | for(var name in response.headers) { 59 | if (!response.headers.hasOwnProperty(name)) { 60 | continue; 61 | } 62 | result.headers.push({ 63 | name: name, 64 | value: response.headers[name] 65 | }); 66 | } 67 | 68 | return result; 69 | } 70 | 71 | return { 72 | get: get 73 | }; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/multiFetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var request = require('superagent'); 4 | var coBody = require('co-body'); 5 | var batchRequestor = require('./batchRequestor')(require('./httpClient')(require('superagent'))); 6 | 7 | module.exports = function (options) { 8 | options = options || {}; 9 | var er = 0; 10 | return function* batch() { 11 | var data; 12 | try { 13 | data = yield coBody(this); 14 | } catch (e) { 15 | data = this.request.query; 16 | } 17 | 18 | if (options.header_host) { 19 | this.headers.host = options.header_host; 20 | } 21 | 22 | var baseUrl = options.absolute ? this.headers.host : this.headers.host + this.req._parsedUrl.pathname; 23 | 24 | var result = yield batchRequestor(data, baseUrl, this.headers); 25 | 26 | this.body = result.body; 27 | for (var name in result.headers) { 28 | this.set(name, result.headers[name]); 29 | } 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-multifetch", 3 | "version": "0.6.1", 4 | "description": "A Koa.js middleware for performing internal batch requests", 5 | "main": "lib/multiFetch.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/marmelab/koa-multifetch.git" 12 | }, 13 | "author": "marmelab", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/marmelab/koa-multifetch/issues" 17 | }, 18 | "homepage": "https://github.com/marmelab/koa-multifetch", 19 | "engines": { 20 | "node": ">=0.11" 21 | }, 22 | "scripts": { 23 | "test": "NODE_ENV=test node_modules/mocha/bin/mocha --harmony test/*.js test/**/*.js" 24 | }, 25 | "devDependencies": { 26 | "chai": "~1.9.2", 27 | "koa": "~0.13.0", 28 | "koa-route": "~2.2.0", 29 | "mocha": "~1.21.5", 30 | "mockery": "^1.4.0", 31 | "supertest": "~0.14.0" 32 | }, 33 | "dependencies": { 34 | "co": "~3.1.0", 35 | "co-body": "~1.0.0", 36 | "superagent": "~0.20.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/batchRequestor.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var co = require('co'); 3 | 4 | describe('batchRequestor', function () { 5 | var mockRequestor, batchRequestor; 6 | 7 | beforeEach(function () { 8 | mockRequestor = { 9 | get: function* (url, headers) { 10 | this.getCall++; 11 | this.headers = headers; 12 | 13 | return { code: 200, body: 'result for ' + url, headers: {} }; 14 | }, 15 | headers: null, 16 | getCall: 0 17 | }; 18 | batchRequestor = require('../lib/batchRequestor')(mockRequestor); 19 | }); 20 | 21 | it('should return result for all given url', co(function* () { 22 | var result = yield batchRequestor({resource1: '/resource1/5', resource2: '/resource2'}, '/host/api'); 23 | assert.deepEqual(result.body, { 24 | resource1: { code: 200, body: 'result for /host/api/resource1/5', headers: {} }, 25 | resource2: { code: 200, body: 'result for /host/api/resource2', headers: {} } 26 | }); 27 | })); 28 | 29 | it ('should call requestor once for every request with baseurl + requestUrl', co(function* () { 30 | yield batchRequestor({resource1: '/resource1/5', resource2: '/resource2'}, '/host/api'); 31 | 32 | assert.equal(mockRequestor.getCall, 2); 33 | mockRequestor.getCall = 0; 34 | 35 | yield batchRequestor({resource1: '/host/api/resource1/5', resource2: '/host/api/resource2', resource3: '/host/api/resource3'}, '/host/api'); 36 | assert.equal(mockRequestor.getCall, 3); 37 | })); 38 | 39 | it('should use provided headers if any for every request', co(function* () { 40 | var expectedHeaders = { Accept: 'text/json'}; 41 | 42 | yield batchRequestor({resource1: 'api/resource1/5', resource2: 'api/resource2'}, '/host/api', { Accept: 'text/json'}); 43 | assert.deepEqual(mockRequestor.headers, expectedHeaders); 44 | 45 | assert.equal(mockRequestor.getCall, 2); 46 | })); 47 | 48 | it('should include Cache-Control and Vary headers extracted from results', co(function* () { 49 | var responseHeaders = {}; 50 | mockRequestor = { 51 | get: function* (url, headers) { 52 | responseHeaders = {}; 53 | if ('/api/1' === url) { 54 | responseHeaders = [ 55 | { name: 'Cache-Control', value: 'public, max-age=300' }, 56 | { name: 'Vary', value: 'Cookie' } 57 | ]; 58 | } 59 | if ('/api/2' === url) { 60 | responseHeaders = [ 61 | { name: 'Cache-Control', value: 'public, max-age=3600, s-maxage=3600' }, 62 | { name: 'Vary', value: 'User-Agent' } 63 | ]; 64 | } 65 | 66 | return { code: 200, body: 'result for ' + url, headers: responseHeaders }; 67 | } 68 | } 69 | batchRequestor = require('../lib/batchRequestor')(mockRequestor); 70 | 71 | var results = yield batchRequestor({resource1: 'api/1', resource2: 'api/2'}, '/'); 72 | assert.deepEqual(results.headers, { 'Cache-Control': 'public, max-age=3600, s-maxage=3600', Vary: 'Cookie, User-Agent' }); 73 | })); 74 | 75 | it('should not include Cache-Control and Vary header if at least one of results omit them', co(function* () { 76 | var responseHeaders = {}; 77 | mockRequestor = { 78 | get: function* (url, headers) { 79 | responseHeaders = {}; 80 | if ('/api/1' === url) { 81 | responseHeaders = [ 82 | { name: 'Cache-Control', value: 'public, max-age=300' }, 83 | { name: 'Vary', value: 'Cookie' } 84 | ]; 85 | } 86 | 87 | return { code: 200, body: 'result for ' + url, headers: responseHeaders }; 88 | } 89 | } 90 | batchRequestor = require('../lib/batchRequestor')(mockRequestor); 91 | 92 | results = yield batchRequestor({ resource1: 'api/1', resource2: 'api/2' }, '/'); 93 | assert.deepEqual(results.headers, {}); 94 | })); 95 | 96 | it('should set Cache-Control if at least one of results have no-cache directive', co(function* () { 97 | var responseHeaders = {}; 98 | mockRequestor = { 99 | get: function* (url, headers) { 100 | responseHeaders = {}; 101 | if ('/api/1' === url) { 102 | responseHeaders = [ 103 | { name: 'Cache-Control', value: 'no-cache' } 104 | ]; 105 | } 106 | 107 | return { code: 200, body: 'result for ' + url, headers: responseHeaders }; 108 | } 109 | } 110 | batchRequestor = require('../lib/batchRequestor')(mockRequestor); 111 | 112 | results = yield batchRequestor({ resource1: 'api/1', resource2: 'api/2' }, '/'); 113 | assert.deepEqual(results.headers, { 'Cache-Control': 'no-cache' }); 114 | })); 115 | }); 116 | -------------------------------------------------------------------------------- /test/httpClient.js: -------------------------------------------------------------------------------- 1 | var co = require('co'); 2 | var assert = require('chai').assert; 3 | 4 | describe('httpClient', function () { 5 | var httpClient, superAgentMock; 6 | 7 | beforeEach(function () { 8 | superAgentMock = { 9 | get: function (){return this;}, 10 | set: function () {return this;}, 11 | on: function () {return this;}, 12 | end: function (cb) { 13 | cb({}); 14 | } 15 | }; 16 | httpClient = require('../lib/httpClient')(superAgentMock); 17 | }); 18 | 19 | it('should return code 500 and its detail on unknown error', co(function* () { 20 | superAgentMock.on = function (type, cb) { 21 | var error = new Error('unknown error'); 22 | error.code = 'UNEXPECTED'; 23 | cb(error); 24 | }; 25 | 26 | var response = yield httpClient.get('wrong/url'); 27 | 28 | assert.equal(response.code, 500); 29 | assert.equal(response.body, 'UNEXPECTED: unknown error'); 30 | })); 31 | 32 | it('should return res with code 404 on inexistant url', co(function* () { 33 | superAgentMock.on = function (type, cb) { 34 | cb({code: 'ENOTFOUND'}); 35 | }; 36 | 37 | assert.deepEqual(yield httpClient.get('wrong/url'), { 38 | code: 404 39 | }); 40 | })); 41 | 42 | it('should send get request to given url using provided agent returning response code headers and body', co(function* () { 43 | superAgentMock.end = function (cb) { 44 | cb({ 45 | status: 200, 46 | headers: { 47 | "Content-Type": "text/javascript; charset=UTF-8" 48 | }, 49 | body: 'result' 50 | }); 51 | }; 52 | 53 | assert.deepEqual(yield httpClient.get('api/resources'), { 54 | code: 200, 55 | headers: [ 56 | { "name": "Content-Type", "value": "text/javascript; charset=UTF-8" } 57 | ], 58 | body: 'result' 59 | }); 60 | })); 61 | 62 | it('should set the headers values on the request if provided', co(function* () { 63 | 64 | var setCall = 0; 65 | 66 | superAgentMock.set = function (name, value) { 67 | setCall++; 68 | var expectation = { 69 | 'Accept': 'text/javascript; charset=UTF-8', 70 | 'Access-Token': '4589edf54' 71 | }; 72 | assert.equal(expectation[name], value); 73 | 74 | return this; 75 | }; 76 | 77 | yield httpClient.get('api/resources', {'Access-Token': '4589edf54', 'Accept': 'text/javascript; charset=UTF-8'}); 78 | 79 | assert.equal(setCall, 2, 'set method should have been called twice'); 80 | })); 81 | 82 | it('should parse the response headers', co(function* () { 83 | 84 | superAgentMock.end = function (cb) { 85 | cb({ 86 | status: 200, 87 | headers: { 88 | 'response-header': 'some header', 89 | 'one-more': 'another header' 90 | }, 91 | body: 'result' 92 | }); 93 | }; 94 | 95 | assert.deepEqual((yield httpClient.get('api/resources')).headers, [ 96 | { "name": "response-header", "value": "some header" }, 97 | { "name": "one-more", "value": "another header" } 98 | ]); 99 | 100 | })); 101 | 102 | }); 103 | -------------------------------------------------------------------------------- /test/multiFetch.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var co = require('co'); 3 | var koa = require('koa'); 4 | var koaRoute = require('koa-route'); 5 | var request = require('supertest'); 6 | var batch = require('../lib/multiFetch'); 7 | 8 | describe('multifetch', function () { 9 | var app; 10 | 11 | before(function () { 12 | app = koa(); 13 | app.use(koaRoute.post('/api', batch())); 14 | app.use(koaRoute.get('/api', batch())); 15 | app.use(koaRoute.get('/api/resource1', function* () { 16 | this.set('Custom-Header', 'why not'); 17 | this.body = {result: 'resource1'}; 18 | })); 19 | app.use(koaRoute.get('/api/resource2/:id', function* (id) { 20 | this.set('Other-Custom-Header', 'useful'); 21 | this.body = {result: 'resource2/' + id}; 22 | })); 23 | app.use(koaRoute.get('/api/boom', function* (id) { 24 | throw new Error('boom'); 25 | })); 26 | app.use(koaRoute.get('/api/protected', function* (id) { 27 | if (this.get('Authorization') === 'Token abcdef') { 28 | this.body = true; 29 | return; 30 | } 31 | this.status = 403; 32 | })); 33 | }); 34 | 35 | it ('should return code 404 if url is not found', function (done) { 36 | request.agent(app.listen()) 37 | .post('/api') 38 | .send({wrong: '/wrong'}) 39 | .expect(function (res) { 40 | assert.equal(res.body.wrong.code, 404); 41 | assert.deepEqual(res.body.wrong.body, {}); 42 | }) 43 | .end(done); 44 | }); 45 | 46 | it ('should return code 500 if server error occur', function (done) { 47 | request.agent(app.listen()) 48 | .post('/api') 49 | .send({boom: '/boom'}) 50 | .expect(function (res) { 51 | assert.equal(res.body.boom.code, 500); 52 | }) 53 | .end(done); 54 | }); 55 | 56 | describe('GET', function () { 57 | 58 | it('should call each passed request and return their result', function (done) { 59 | request.agent(app.listen()) 60 | .get('/api?resource1=/resource1&resource2=/resource2/5') 61 | .expect(function (res) { 62 | assert.equal(res.body.resource1.code, 200); 63 | assert.deepEqual(res.body.resource1.body, {result: 'resource1'}); 64 | assert.equal(res.body.resource2.code, 200); 65 | assert.deepEqual(res.body.resource2.body, {result: 'resource2/5'}); 66 | }) 67 | .end(done); 68 | }); 69 | 70 | it('should use main request headers on each sub request', co(function* () { 71 | var response = yield function (done) { 72 | request.agent(app.listen()) 73 | .get('/api?protected=/protected') 74 | .set('Authorization', 'Token wrongtoken') 75 | .end(done); 76 | }; 77 | 78 | assert.equal(response.body.protected.code, 403); 79 | 80 | yield function (done) { 81 | request.agent(app.listen()) 82 | .get('/api?protected=/protected') 83 | .set('Authorization', 'Token abcdef') 84 | .expect(200) 85 | .end(done); 86 | }; 87 | 88 | assert.equal(response.body.protected.code, 403); 89 | })); 90 | 91 | it('should return the header for each request', function (done) { 92 | request.agent(app.listen()) 93 | .get('/api?resource1=/resource1&resource2=/resource2/5') 94 | .expect(function (res) { 95 | assert.include(res.body.resource1.headers, {name: 'custom-header', value: 'why not'}); 96 | assert.include(res.body.resource2.headers, {name: 'other-custom-header', value: 'useful'}); 97 | }) 98 | .end(done); 99 | }); 100 | 101 | }); 102 | 103 | describe('POST', function () { 104 | 105 | it ('should call each passed request and return their result', function (done) { 106 | request.agent(app.listen()) 107 | .post('/api') 108 | .send({resource1: '/resource1', resource2: '/resource2/5'}) 109 | .expect(function (res) { 110 | assert.equal(res.body.resource1.code, 200); 111 | assert.deepEqual(res.body.resource1.body, {result: 'resource1'}); 112 | assert.equal(res.body.resource2.code, 200); 113 | assert.deepEqual(res.body.resource2.body, {result: 'resource2/5'}); 114 | }) 115 | .end(done); 116 | }); 117 | 118 | it ('should return the header for each request', function (done) { 119 | request.agent(app.listen()) 120 | .post('/api') 121 | .send({resource1: '/resource1', resource2: '/resource2/5'}) 122 | .expect(function (res) { 123 | assert.include(res.body.resource1.headers, {name: 'custom-header', value: 'why not'}); 124 | assert.include(res.body.resource2.headers, {name: 'other-custom-header', value: 'useful'}); 125 | }) 126 | .end(done); 127 | }); 128 | 129 | }); 130 | 131 | describe('absolute url to true', function () { 132 | 133 | before (function () { 134 | app.use(koaRoute.post('/absolute_path', batch({absolute: true}))); 135 | }); 136 | 137 | describe('POST', function () { 138 | 139 | it('should call each passed request and return their result', co(function* () { 140 | var response = yield function (done) { 141 | request.agent(app.listen()) 142 | .post('/absolute_path') 143 | .send({resource1: '/api/resource1', resource2: '/api/resource2/5'}) 144 | .end(done); 145 | }; 146 | 147 | assert.equal(response.body.resource1.code, 200); 148 | assert.deepEqual(response.body.resource1.body, {result: 'resource1'}); 149 | assert.equal(response.body.resource2.code, 200); 150 | assert.deepEqual(response.body.resource2.body, {result: 'resource2/5'}); 151 | })); 152 | 153 | it ('should return the header for each request', function (done) { 154 | request.agent(app.listen()) 155 | .post('/absolute_path') 156 | .send({resource1: '/api/resource1', resource2: '/api/resource2/5'}) 157 | .expect(function (res) { 158 | assert.include(res.body.resource1.headers, {name: 'custom-header', value: 'why not'}); 159 | assert.include(res.body.resource2.headers, {name: 'other-custom-header', value: 'useful'}); 160 | }) 161 | .end(done); 162 | }); 163 | 164 | }); 165 | }); 166 | 167 | after(function () { 168 | delete app; 169 | }); 170 | 171 | }); 172 | -------------------------------------------------------------------------------- /test/multiFetch_customHost.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var co = require('co'); 3 | var koa = require('koa'); 4 | var koaRoute = require('koa-route'); 5 | var request = require('supertest'); 6 | var mockery = require('mockery'); 7 | 8 | describe('multifetch with custom header_host', function () { 9 | var app; 10 | var hostTest = null; 11 | 12 | before(function () { 13 | var mock_httpClient = function () { 14 | return { 15 | get: function *(url, headers) { 16 | hostTest = headers.host; 17 | } 18 | }; 19 | }; 20 | mockery.registerAllowable('./httpClient', true); 21 | mockery.enable({ 22 | useCleanCache: true, 23 | warnOnReplace: false, 24 | warnOnUnregistered: false 25 | }); 26 | mockery.registerMock('./httpClient', mock_httpClient); 27 | var batch = require('../lib/multiFetch'); 28 | 29 | app = koa(); 30 | app.use(koaRoute.get('/api', batch({header_host: 'http://localhost:3000'}))); 31 | app.use(koaRoute.get('/api/resource1', function* () { 32 | this.set('Custom-Header', 'why not'); 33 | this.body = {result: 'resource1'}; 34 | })); 35 | }); 36 | 37 | it('should set custom host on htttClient subrequest', co(function* () { 38 | var response = yield function (done) { 39 | request.agent(app.listen()) 40 | .get('/api?resource1=/resource1') 41 | .end(done); 42 | }; 43 | assert.equal(hostTest, 'http://localhost:3000'); 44 | })); 45 | 46 | after(function () { 47 | mockery.disable(); 48 | delete app; 49 | }); 50 | 51 | }); 52 | --------------------------------------------------------------------------------