├── .gitignore ├── README.md ├── TODO.md ├── examples ├── 1k.txt ├── reference-browser-native-xhr2.js ├── request-file-upload.js ├── request-get-binary.js ├── request-get.js ├── request-options.js └── request-post.js ├── incomplete-cruft ├── browser-basic.js ├── browz.js ├── generate-test-upload-files.js ├── get-404.js ├── jsonp-test.js ├── node-basic.js ├── node-file.js ├── node-post.js ├── node-request.js ├── node-stream.js ├── node-tcp.js └── uri-encoder-test.js ├── lib ├── .npmignore ├── browser │ ├── index.js │ └── jsonp.js ├── index.js ├── node │ ├── file-client-node.js │ ├── form-content.js │ ├── index.js │ ├── request-http.js │ ├── request-tcp.js │ ├── request-udp.js │ └── response-http.js ├── options.js ├── package.json └── utils.js └── tests ├── .gitignore ├── abort-method-aborts.js ├── app.js ├── deploy.sh ├── get-content-length-of-unicode-string.js ├── has-native-node-request.js ├── index.jade ├── package.json ├── settimout-aborts.js └── test-suite.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.dat 2 | node_modules 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | ### [abstract-http-request](https://git.daplie.com/coolaj86/abstract-http-request) is now at [git.daplie.com/coolaj86/abstract-http-request](https://git.daplie.com/coolaj86/abstract-http-request) 3 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | Good test coverage using foobar3000. 2 | 3 | * Test and Simplify URL string and object parsing / creation 4 | 5 | * contentType - json, application/json, jsonp 6 | * responseType 7 | * xhr2.overrideMimeType("text/plain; charset=x-user-defined"); 8 | * xhr2.responseType = 'arraybuffer'; 9 | * create upload test for the browser 10 | * File 11 | * FileList 12 | * FormData 13 | * BlobBuilder 14 | 15 | * Better support for binary uploads in browser 16 | * Blob, BlobBuilder, etc (not yet implemented in most browsers) -------------------------------------------------------------------------------- /examples/1k.txt: -------------------------------------------------------------------------------- 1 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2 | BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 3 | CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 4 | DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD 5 | EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE 6 | FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 7 | 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 8 | 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111 9 | -------------------------------------------------------------------------------- /examples/reference-browser-native-xhr2.js: -------------------------------------------------------------------------------- 1 | /* 2 | Experiences with Chrome 13 3 | * Both `loadstart` events fire at `send()` 4 | * Neither `loadend` events fire ever 5 | * response only progresses after request has loaded 6 | 7 | For a request without a body (neither FormData nor DOMString): 8 | * `request` events never fire 9 | */ 10 | (function () { 11 | "use strict"; 12 | 13 | var xhr2 = new XMLHttpRequest() 14 | , xhr2Request = xhr2.upload; 15 | ; 16 | 17 | // Make Request 18 | xhr2.open('POST', "/doesnt-exist/doesnt-matter", true); 19 | 20 | // Response (old-fashioned handlers) 21 | xhr2.addEventListener('loadstart', function (ev) { 22 | console.log('res.loadstart', ev); 23 | }); 24 | xhr2.addEventListener('progress', function (ev) { 25 | xhr2.getAllResponseHeaders(); 26 | console.log('res.progress', ev); 27 | }); 28 | xhr2.addEventListener('load', function (ev) { 29 | if ('binary' === options.overrideResponseType) { 30 | xhr2. 31 | } 32 | console.log('res.load', ev, ev.target); 33 | }); 34 | xhr2.addEventListener('loadend', function (ev) { 35 | console.log('res.loadend', ev); 36 | }); 37 | 38 | // Request (upload handlers) 39 | xhr2Request.addEventListener('loadstart', function (ev) { 40 | console.log('req.loadstart', ev); 41 | }); 42 | xhr2Request.addEventListener('progress', function (ev) { 43 | console.log('req.progress', ev); 44 | }); 45 | xhr2Request.addEventListener('load', function (ev) { 46 | console.log('req.load', ev, ev.target); 47 | }); 48 | xhr2Request.addEventListener('loadend', function (ev) { 49 | console.log('req.loadend', ev); 50 | }); 51 | 52 | //xhr2.send("blah=x&yab=y"); 53 | xhr2.send(null); 54 | }()); 55 | -------------------------------------------------------------------------------- /examples/request-file-upload.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | , File = require('File') 6 | , file 7 | ; 8 | 9 | file = new File(__dirname + '/1k.txt'); // can also use Buffer or EventEmitter with `data` and `end` events 10 | // wget http://foobar3000.com/assets/1k.txt 11 | // In the browser this would look something like 12 | /* 13 | file = $('input[type=file]')[0].FileList[0] 14 | */ 15 | 16 | // request.get(href || pathname, query, body, options).when(callback) 17 | request.post("http://foobar3000.com/echo?rawBody=true", null, { 18 | message: 'Hello World' 19 | , attachment: file 20 | //, chunked: true // node-only 21 | }).when(function (err, ahr, data) { 22 | console.log('\n\nGot Response\n'); 23 | console.log(data.toString()); 24 | }); 25 | 26 | }()); 27 | -------------------------------------------------------------------------------- /examples/request-get-binary.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | ; 6 | 7 | request.get('http://foobar3000.com/assets/reference.rgb565', {}, { 8 | overrideResponseType: 'binary' 9 | }).when(function (err, request, data) { 10 | console.log(err, data && (data.length || data.byteLength)); 11 | }); 12 | 13 | }()); 14 | -------------------------------------------------------------------------------- /examples/request-get.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | ; 6 | 7 | // request.get(href || pathname, query, options).when(callback) 8 | request.get("http://foobar3000.com/echo?foo=bar&baz=qux&baz=quux&corge").when(function (err, ahr, data) { 9 | console.log(arguments); 10 | }); 11 | }()); 12 | -------------------------------------------------------------------------------- /examples/request-options.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | ; 6 | 7 | // request(options).when(callback); 8 | request({ 9 | "method": "POST" 10 | 11 | // urlObj 12 | , "hostname": "foobar3000.com" 13 | , "port": "80" 14 | , "pathname": "/echo" 15 | , "query": { 16 | "foo": "bar" 17 | , "baz": [ 18 | "qux" 19 | , "quux" 20 | ] 21 | , "corge": "" 22 | } 23 | , "hash": "#grault" 24 | 25 | // headers 26 | , "headers": { 27 | "X-Request-Header": "Testing" 28 | , "Content-Type": "application/json" 29 | } 30 | 31 | // body 32 | , "body": { 33 | "garply": { 34 | "waldo" : "fred" 35 | "plugh": { 36 | "xyzzy": "thud" 37 | } 38 | } 39 | } 40 | 41 | // other 42 | , "overrideResponseType": "binary" 43 | , "timeout": 5000 44 | }); 45 | 46 | }()); 47 | -------------------------------------------------------------------------------- /examples/request-post.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | ; 6 | 7 | // request.get(href || pathname, query, body, options).when(callback) 8 | request.post("http://foobar3000.com/echo?rawBody=true", null, { 9 | message: 'Hello World' 10 | }).when(function (err, ahr, data) { 11 | console.log(err, data); 12 | }); 13 | 14 | }()); 15 | -------------------------------------------------------------------------------- /incomplete-cruft/browser-basic.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var ahr = require('ahr'), 3 | tests; 4 | 5 | // TODO test against XHR2 / CORS enabled sites 6 | tests = { 7 | "simple200": "http://www.google.com", 8 | "redirect301": "http://coolaj86.github.com", 9 | "encodedUrl": "http://www.google.com/search?hl=en&client=firefox-a&rls=org.mozilla%3Aen-US%3Aofficial&q=%22json+to+x-www-form-urlencoded%22&aq=f&aqi=&aql=&oq=&gs_rfai=", 10 | } 11 | 12 | Object.keys(tests).forEach(function (key) { 13 | // As simple as it gets 14 | ahr.http({ 15 | url: tests[key], 16 | method: 'GET' 17 | }).when(function (err, xhr, data) { 18 | if (err) { 19 | console.log("\n'" + key + "' FAIL..."); 20 | console.log('Status: ' + xhr.statusText); 21 | console.log('Headers: ' + JSON.stringify(xhr.getAllResponseHeaders())); 22 | console.log('Error: '); 23 | console.log(err); 24 | console.log(data); 25 | return; 26 | } 27 | console.log("'" + key + "' Pass"); 28 | }); 29 | }); 30 | }()); 31 | -------------------------------------------------------------------------------- /incomplete-cruft/browz.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var browz = require('browz') 5 | , browzer; 6 | 7 | function jsonp(req, next) { 8 | if (!req.jsonp) { 9 | next(); 10 | } 11 | req.jsonpCallback = 'jsonp' + new Date().valueOf(); 12 | req.emit('loadstart'); 13 | // In the world of CORS/XHR2 this is a silly notion, 14 | // but this just serves as an example 15 | if ('POST' === req.method) { 16 | req.query['X-HTTP-Method-Override'] = 'POST'; 17 | } 18 | insertScriptTag(req, function (res) { 19 | req.emit('load'); 20 | req.emit('loaded'); 21 | }); 22 | req.on('response', function (res) { 23 | }); 24 | } 25 | 26 | function json(res, next) { 27 | if (/json/.test(res.getHeader('Content-Type'))) { 28 | try { 29 | res.data = JSON.parse(data); 30 | } catch(e) { 31 | res.emit('error', ); 32 | } 33 | } 34 | } 35 | 36 | function GitHub() { 37 | browzer = browz.up( 38 | browz.jsonp 39 | , browz.auth("user", "pass") 40 | ).down( 41 | browz.cookies 42 | browz.json 43 | ); 44 | } 45 | 46 | browzer.get("http://github.com", function (res) { 47 | res.on('progress', function (ev) { 48 | if (ev.chunk) { 49 | md5sum.churn(ev.chunk); 50 | } 51 | console.log("Housten, we have progress"); 52 | }) 53 | res.on('load', function (ev) { 54 | if (!md5sum.length) { 55 | md5sum.churn(ev.data); 56 | } 57 | console.log(ev.data.username, md5sum.digest(16)); 58 | }); 59 | }); 60 | 61 | /* 62 | ... 63 | */ 64 | github.login("secret_token"); 65 | }()); 66 | -------------------------------------------------------------------------------- /incomplete-cruft/generate-test-upload-files.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var fs = require('fs') 5 | , bank = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 6 | 7 | function writeFile(name, num) { 8 | var i, j, c, bindex, data = ''; 9 | 10 | for (i = 0; i < num; i += 1) { 11 | bindex = i % bank.length; 12 | c = bank[bindex]; 13 | for (j = 0; j < 1023; j += 1) { 14 | data += c; 15 | } 16 | data += '\n'; 17 | } 18 | 19 | fs.writeFileSync(name, data); 20 | } 21 | 22 | writeFile('256kb.dat', 256); 23 | writeFile('16kb.dat', 16); 24 | writeFile('2k_b.dat', 2); 25 | writeFile('1k_a.dat', 1); 26 | }()); 27 | -------------------------------------------------------------------------------- /incomplete-cruft/get-404.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | , assert = require('assert') 6 | , get 7 | ; 8 | 9 | get = request("http://foobar3000.com/?status=404"); 10 | 11 | get.when(function (err, response, data) { 12 | console.log(response); 13 | //assert.ok(/404/.test(data.toString('utf8'))); 14 | //console.log('[pass] Found 404 page'); 15 | }); 16 | }()); 17 | -------------------------------------------------------------------------------- /incomplete-cruft/jsonp-test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var parseJsonp = require('./utils').parseJsonp; 5 | 6 | function parseJsonpWithSubStr(jsonp, jsonResp) { 7 | var lparen = jsonpResp.indexOf('(') 8 | , rparen = jsonpResp.lastIndexOf(')'); 9 | 10 | if (!lparen) { 11 | return new Error('No JSONP left-paren Matched'); 12 | } 13 | if (!rparen) { 14 | return new Error('No JSONP right-paren Matched'); 15 | } 16 | // jsonPad = jsonResp.substr(); 17 | } 18 | 19 | var jsonps = [ 20 | 'jsonp7634567({ "a": "b" })' 21 | , "jsonp7634567( {} )" 22 | , " jsonp7634567( {} ) " 23 | , "jsonp7634567( {} )" 24 | , "jsonp7634567([2,4,8])" 25 | , "jsonp7634567(1)" 26 | , "jsonp7634567(1)" 27 | , "jsonp7634567(\"abc\")" 28 | , "jsonp7634567(\"({})\")" 29 | , "jsonp7634567(\"{(})\")" 30 | , "jsonp7634567(\"{()}\")" 31 | , "jsonp7634567(\"{(})\")" 32 | , "jsonp7634567(\"{(})\")" 33 | ] 34 | , jsonperrs = [ 35 | " jsonp7634567() " 36 | , " [1,3,9] " 37 | ]; 38 | 39 | var jsonpCallback = "jsonp7634567"; 40 | jsonps.forEach(function (jsonp) { 41 | var data = parseJsonp(jsonpCallback, jsonp); 42 | console.log(data); 43 | }); 44 | }()); 45 | -------------------------------------------------------------------------------- /incomplete-cruft/node-basic.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('../lib/ahr2') 5 | , tests 6 | , File = require('file-api').File; 7 | 8 | tests = [ 9 | { 10 | key: "simple200", 11 | href: "http://www.google.com", 12 | regex: /Google Search/ 13 | }, 14 | { 15 | key: "port80", 16 | href: "http://www.google.com:80", 17 | regex: /Google Search/ 18 | }, 19 | { 20 | key: "port5984", 21 | href: "http://mikeal.couchone.com:5984", 22 | regex: /{"couchdb":"Welcome","version":"1/ 23 | }, 24 | { 25 | key: "redirect301", 26 | href: "http://coolaj86.github.com", 27 | regex: /CoolAJ86/ 28 | }, 29 | { 30 | key: "encodedUrl", 31 | href: "http://www.google.com/search?hl=en&client=firefox-a&rls=org.mozilla%3Aen-US%3Aofficial&q=%22json+to+x-www-form-urlencoded%22&aq=f&aqi=&aql=&oq=&gs_rfai=", 32 | regex: /x-www-form-urlencoded/ 33 | }, 34 | { 35 | key: "encodedParams", 36 | href: "http://www.google.com/search", 37 | query: { 38 | hl: "en", 39 | client: "firefox-a", 40 | rls: "org.mozilla:en-US:official", 41 | q: "\"json+to+x-www-form-urlencoded\"", 42 | aq: "f", 43 | aqi: "", 44 | aql: "", 45 | oq: "", 46 | gs_rfai : "" 47 | }, 48 | regex: /x-www-form-urlencoded/ 49 | }, 50 | { 51 | key: "jsonp", 52 | href: "http://api.flickr.com/services/feeds/photos_public.gne?format=json", 53 | query: { tags: "cat", tagmode: "any", "jsoncallback": "jsonp_" + (new Date()).valueOf() }, 54 | //options: { jsonp: "jsoncallback" }, // turn off jsonp for regex matching 55 | regex: /jsonp_\d+\(/ 56 | }, 57 | { 58 | key: "POST json", 59 | href: "http://mikeal.couchone.com:5984/testjs", 60 | options: { 61 | method:'POST', 62 | headers: { 'content-type': 'application/json', 'accept': 'application/json'}, 63 | body: { _id: Math.floor(Math.random()*100000000).toString() } 64 | }, 65 | regex: /"ok":true/ 66 | } 67 | ]; 68 | 69 | /* 70 | tests.forEach(function (test) { 71 | // As simple as it gets 72 | test.options = test.options || {}; 73 | test.options.href = test.href; 74 | test.options.query = test.query; 75 | request.http(test.options).when(function (err, response, data) { 76 | if (err || !data || !data.match(test.regex)) { 77 | console.log("\n'" + test.key + "' FAIL..."); 78 | console.log('Status: ' + response.statusCode); 79 | console.log('Headers: ' + JSON.stringify(response.headers)); 80 | console.log('Error: ' + err); 81 | console.log('Data: ' + data.substring(0,100) + '...'); 82 | return; 83 | } 84 | console.log("'" + test.key + "' Passes Expected Regex Match"); 85 | }); 86 | }); 87 | */ 88 | 89 | //.when(handleResponses)); 90 | function handleResponses(err, response, data, i) { 91 | var test = tests[i]; 92 | if (data instanceof Buffer) { 93 | data = data.toString(); 94 | } 95 | if (err || !data || !data.match(test.regex)) { 96 | console.log("\n\n"); 97 | console.log("\n'" + test.key + "' FAIL..."); 98 | console.log('Status: ' + response.statusCode); 99 | console.log('Headers: ' + JSON.stringify(response.headers)); 100 | console.log('Error: ' + err); 101 | console.log('Data: ' + data.substring(0,200) + '...'); 102 | console.log("\n\n"); 103 | return; 104 | } 105 | console.log("'" + test.key + "' Passes Expected Regex Match"); 106 | } 107 | 108 | 109 | var all = []; 110 | tests.forEach(function (test, i) { 111 | // As simple as it gets 112 | test.options = test.options || {}; 113 | test.options.href = test.href; 114 | test.options.query = test.query; 115 | all.push( 116 | request.http(test.options) 117 | .when(function (err, response, data) { 118 | handleResponses.call(null, err, response, data, i); 119 | }) 120 | ); 121 | }); 122 | 123 | console.log("If nothing happens, then the joins failed"); 124 | request.join(all) 125 | .when(function (arr) { 126 | console.log("'join' Passes."); 127 | }); 128 | 129 | }()); 130 | -------------------------------------------------------------------------------- /incomplete-cruft/node-file.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var request = require('../lib/ahr2'), 3 | tests; 4 | 5 | tests = [ 6 | { 7 | key: "file_relative_path1", 8 | href: "16kb.dat", 9 | regex: /E\nF/ 10 | }, 11 | { 12 | key: "file_relative_path2", 13 | href: "./16kb.dat", 14 | regex: /E\nF/ 15 | }, 16 | { 17 | key: "file_relative_path3", 18 | href: "file:16kb.dat", 19 | regex: /E\nF/ 20 | }, 21 | { 22 | key: "file_absolute_path", 23 | href: "file:///tmp/16kb.dat", 24 | regex: /E\nF/ 25 | }, 26 | ]; 27 | 28 | 29 | //.when(handleResponses)); 30 | function handleResponses(err, response, data, i) { 31 | var test = tests[i]; 32 | if (data instanceof Buffer) { 33 | data = data.toString(); 34 | } 35 | if (err || !data || !data.match(test.regex)) { 36 | console.log("\n'" + test.key + "' FAIL..."); 37 | console.log('Status: ' + response.statusCode); 38 | console.log('Headers: ' + JSON.stringify(response.headers)); 39 | console.log('Error: ' + err); 40 | console.log('Data: ' + data.substring(0,100) + '...'); 41 | return; 42 | } 43 | console.log("'" + test.key + "' Passes Expected Regex Match"); 44 | } 45 | 46 | 47 | var all = []; 48 | tests.forEach(function (test, i) { 49 | // As simple as it gets 50 | test.options = test.options || {}; 51 | test.options.href = test.href; 52 | test.options.query = test.query; 53 | all.push( 54 | request.http(test.options) 55 | .when(function (err, response, data) { 56 | handleResponses.call(null, err, response, data, i); 57 | }) 58 | ); 59 | }); 60 | 61 | console.log("If nothing happens, then the joins failed"); 62 | request.join(all) 63 | .when(function (arr) { 64 | console.log("'join' Passes."); 65 | }); 66 | 67 | }()); 68 | -------------------------------------------------------------------------------- /incomplete-cruft/node-post.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('../lib/ahr2') 5 | , tests 6 | , File = require('File'); 7 | 8 | tests = [ 9 | { 10 | key: "POST 16kb locally", 11 | href: "http://127.0.0.1:9000/blah", 12 | options: { 13 | method:'POST', 14 | headers: { 'accept': 'application/json'}, 15 | body: { 16 | file_name: 'super hexabet file' 17 | , upload_file: new File('/tmp/16kb.dat') 18 | } 19 | }, 20 | regex: /Upload in progress/ 21 | }, 22 | { 23 | key: "POST 2 1kb locally", 24 | href: "http://127.0.0.1:9000/blah", 25 | options: { 26 | chunked: true, 27 | method:'POST', 28 | headers: { 'accept': 'application/json'}, 29 | body: { 30 | file_name_0: 'super alhpa file' 31 | , file_name_1: 'super beta file' 32 | , upload_file_0: new File('/tmp/1k_a.dat') 33 | , upload_file_1: new File('/tmp/1k_b.dat') 34 | } 35 | }, 36 | regex: /Upload in progress/ 37 | } 38 | /* 39 | */ 40 | ]; 41 | 42 | /* 43 | tests.forEach(function (test) { 44 | // As simple as it gets 45 | test.options = test.options || {}; 46 | test.options.href = test.href; 47 | test.options.query = test.query; 48 | request.http(test.options).when(function (err, response, data) { 49 | if (err || !data || !data.match(test.regex)) { 50 | console.log("\n'" + test.key + "' FAIL..."); 51 | console.log('Status: ' + response.statusCode); 52 | console.log('Headers: ' + JSON.stringify(response.headers)); 53 | console.log('Error: ' + err); 54 | console.log('Data: ' + data.substring(0,100) + '...'); 55 | return; 56 | } 57 | console.log("'" + test.key + "' Passes Expected Regex Match"); 58 | }); 59 | }); 60 | */ 61 | 62 | //.when(handleResponses)); 63 | function handleResponses(err, response, data, i) { 64 | var test = tests[i]; 65 | if (data instanceof Buffer) { 66 | data = data.toString(); 67 | } 68 | if (err || !data || !data.match(test.regex)) { 69 | console.log("\n\n"); 70 | console.log("\n'" + test.key + "' FAIL..."); 71 | console.log('Status: ' + response.statusCode); 72 | console.log('Headers: ' + JSON.stringify(response.headers)); 73 | console.log('Error: ' + err); 74 | console.log('Data: ' + data.substring(0,200) + '...'); 75 | console.log("\n\n"); 76 | return; 77 | } 78 | console.log("'" + test.key + "' Passes Expected Regex Match"); 79 | } 80 | 81 | 82 | var all = []; 83 | tests.forEach(function (test, i) { 84 | // As simple as it gets 85 | test.options = test.options || {}; 86 | test.options.href = test.href; 87 | test.options.query = test.query; 88 | all.push( 89 | request.http(test.options) 90 | .when(function (err, response, data) { 91 | handleResponses.call(null, err, response, data, i); 92 | }) 93 | ); 94 | }); 95 | 96 | console.log("If nothing happens, then the joins failed"); 97 | request.join(all) 98 | .when(function (arr) { 99 | console.log("'join' Passes."); 100 | }); 101 | 102 | }()); 103 | -------------------------------------------------------------------------------- /incomplete-cruft/node-request.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var request = require('../lib/ahr') 3 | , sys = require('sys') 4 | , assert = require('assert') 5 | , h = {'content-type': 'application/json', 'accept': 'application/json'} 6 | ; 7 | 8 | function testports (port) { 9 | var uri = port ? 'http://mikeal.couchone.com' + ":" + port : 'http://mikeal.couchone.com'; 10 | sys.puts(uri) 11 | request({uri:uri}, function (error, response, body) { 12 | console.log("this test: " + uri); 13 | if (error) {throw new Error(error)}; 14 | assert.equal(response.statusCode, 200); 15 | assert.equal(body.slice(0, '{"couchdb":"Welcome",'.length), '{"couchdb":"Welcome",'); 16 | }) 17 | } 18 | testports(); 19 | testports(80) 20 | testports(5984) 21 | 22 | function testportsStream (port) { 23 | var uri = port ? 'http://mikeal.couchone.com' + ":" + port : 'http://mikeal.couchone.com'; 24 | sys.puts(uri) 25 | var body = '' 26 | var bs = {write:function (chunk) {body += chunk}} 27 | request({uri:uri}, function (error, response) { 28 | if (error) {throw new Error(error)}; 29 | assert.equal(response.statusCode, 200); 30 | assert.equal(body.slice(0, '{"couchdb":"Welcome",'.length), '{"couchdb":"Welcome",'); 31 | }) 32 | } 33 | 34 | testports(); 35 | 36 | var randomnumber=Math.floor(Math.random()*100000000).toString(); 37 | request({uri:'http://mikeal.couchone.com/testjs', method:'POST', headers: h, body:'{"_id":"'+randomnumber+'"}'}, 38 | function (error, response, body) { 39 | if (error) {throw new Error(error)}; 40 | assert.equal(response.statusCode, 201, body); 41 | }); 42 | 43 | /* 44 | 45 | var options = {uri:'http://gmail.com'}; 46 | request(options, function (error, response, body) { 47 | console.log(body); 48 | assert.equal(response.statusCode, 200); 49 | assert.equal(options.uri.host, 'www.google.com'); 50 | assert.equal(response.socket.port, 443); 51 | }) 52 | */ 53 | }()); 54 | -------------------------------------------------------------------------------- /incomplete-cruft/node-stream.js: -------------------------------------------------------------------------------- 1 | var request = require("../lib/ahr.js"), 2 | result = true, 3 | count = 0; 4 | 5 | request({ uri:"file:256kb.dat", stream: true }).whenever(function (err, fs, data, end) { 6 | if (err) { 7 | console.log(err); 8 | } 9 | if (data) { 10 | if (!/[0-9,A-F]/.test(data)) { 11 | result = false; 12 | console.log("Unexpected Error: regex doesn't match contents") 13 | } else { 14 | count += 1; 15 | } 16 | } else if (!end) { 17 | console.log("Unexpected Error: no data"); 18 | } 19 | if (end && result && count > 2) { 20 | console.log('File Test Passes'); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /incomplete-cruft/node-tcp.js: -------------------------------------------------------------------------------- 1 | // test against `netcat -l 4080` 2 | (function () { 3 | "use strict"; 4 | 5 | var request = require('ahr2'); 6 | 7 | request.tcp({ 8 | hostname: 'localhost' 9 | , port: '4080' 10 | , timeout: 10 * 1000 // optional 11 | , encodedBody: "Hello Friend\n" // optional 12 | }).when(function (err, ahr, data) { 13 | console.log('all done', err, ahr, data); 14 | }); 15 | 16 | }()); 17 | -------------------------------------------------------------------------------- /incomplete-cruft/uri-encoder-test.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var addParamsToUri = require('./uri-encoder').addParamsToUri; 5 | 6 | function test() { 7 | return ('' === addParamsToUri("", {}) && 8 | 'http://example.com' === addParamsToUri("http://example.com", {}) && 9 | // some sites use this notation for boolean values 10 | // should undefind be counted as a user-mistake? and null do the 'right thing' ? 11 | 'http://example.com' === addParamsToUri("http://example.com", {foo: undefined}) && 12 | 'http://example.com?foo' === addParamsToUri("http://example.com", {foo: null}) && 13 | 'http://example.com?foo' === addParamsToUri("http://example.com#anchor", {foo: null}) && 14 | 'http://example.com?foo=bar' === addParamsToUri("http://example.com", {foo: 'bar'}) && 15 | 'http://example.com?foo=bar&bar=baz' === addParamsToUri("http://example.com?foo=bar", {bar: 'baz'}) && 16 | 'http://example.com?fo%26%25o=ba%3Fr' === addParamsToUri("http://example.com", {'fo&%o': 'ba?r'}) 17 | ); 18 | }; 19 | 20 | test(); 21 | }()); 22 | -------------------------------------------------------------------------------- /lib/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /lib/browser/index.js: -------------------------------------------------------------------------------- 1 | /*jslint devel: true, debug: true, es5: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */ 2 | // This module is meant for modern browsers. Not much abstraction or 1337 majic 3 | (function (undefined) { 4 | "use strict"; 5 | 6 | var url //= require('url') 7 | , browserJsonpClient = require('./jsonp') 8 | , triedHeaders = {} 9 | , nativeHttpClient 10 | , globalOptions 11 | , restricted 12 | , debug = false 13 | ; // TODO underExtend localOptions 14 | 15 | // Restricted Headers 16 | // http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method 17 | restricted = [ 18 | "Accept-Charset" 19 | , "Accept-Encoding" 20 | , "Connection" 21 | , "Content-Length" 22 | , "Cookie" 23 | , "Cookie2" 24 | , "Content-Transfer-Encoding" 25 | , "Date" 26 | , "Expect" 27 | , "Host" 28 | , "Keep-Alive" 29 | , "Referer" 30 | , "TE" 31 | , "Trailer" 32 | , "Transfer-Encoding" 33 | , "Upgrade" 34 | , "User-Agent" 35 | , "Via" 36 | ]; 37 | restricted.forEach(function (val, i, arr) { 38 | arr[i] = val.toLowerCase(); 39 | }); 40 | 41 | if (!window.XMLHttpRequest) { 42 | window.XMLHttpRequest = function() { 43 | return new ActiveXObject('Microsoft.XMLHTTP'); 44 | }; 45 | } 46 | if (window.XDomainRequest) { 47 | // TODO fix IE's XHR/XDR to act as normal XHR2 48 | // check if the location.host is the same (name, port, not protocol) as origin 49 | } 50 | 51 | 52 | function encodeData(options, xhr2) { 53 | var data 54 | , ct = options.overrideResponseType || xhr2.getResponseHeader("content-type") || "" 55 | , text 56 | , len 57 | ; 58 | 59 | ct = ct.toLowerCase(); 60 | 61 | if (xhr2.responseType && xhr2.response) { 62 | text = xhr2.response; 63 | } else { 64 | text = xhr2.responseText; 65 | } 66 | 67 | len = text.length; 68 | 69 | if ('binary' === ct) { 70 | if (window.ArrayBuffer && xhr2.response instanceof window.ArrayBuffer) { 71 | return xhr2.response; 72 | } 73 | 74 | // TODO how to wrap this for the browser and Node?? 75 | if (options.responseEncoder) { 76 | return options.responseEncoder(text); 77 | } 78 | 79 | // TODO only Chrome 13 currently handles ArrayBuffers well 80 | // imageData could work too 81 | // http://synth.bitsnbites.eu/ 82 | // http://synth.bitsnbites.eu/play.html 83 | // var ui8a = new Uint8Array(data, 0); 84 | var i 85 | , ui8a = Array(len) 86 | ; 87 | 88 | for (i = 0; i < text.length; i += 1) { 89 | ui8a[i] = (text.charCodeAt(i) & 0xff); 90 | } 91 | 92 | return ui8a; 93 | } 94 | 95 | if (ct.indexOf("xml") >= 0) { 96 | return xhr2.responseXML; 97 | } 98 | 99 | if (ct.indexOf("jsonp") >= 0 || ct.indexOf("javascript") >= 0) { 100 | console.log("forcing of jsonp not yet supported"); 101 | return text; 102 | } 103 | 104 | if (ct.indexOf("json") >= 0) { 105 | try { 106 | data = JSON.parse(text); 107 | } catch(e) { 108 | data = text; 109 | } 110 | return data; 111 | } 112 | 113 | return xhr2.responseText; 114 | } 115 | 116 | function browserHttpClient(req, res) { 117 | var options = req.userOptions 118 | , xhr2 119 | , xhr2Request 120 | , timeoutToken 121 | ; 122 | 123 | function onTimeout() { 124 | req.emit("timeout", new Error("timeout after " + options.timeout + "ms")); 125 | } 126 | 127 | function resetTimeout() { 128 | clearTimeout(timeoutToken); 129 | timeoutToken = setTimeout(onTimeout, options.timeout); 130 | } 131 | 132 | function sanatizeHeaders(header) { 133 | var value = options.headers[header] 134 | , headerLc = header.toLowerCase() 135 | ; 136 | 137 | // only warn the user once about bad headers 138 | if (-1 !== restricted.indexOf(header.toLowerCase())) { 139 | if (!triedHeaders[headerLc]) { 140 | console.warn('Ignoring all attempts to set restricted header ' + header + '. See (http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method)'); 141 | } 142 | triedHeaders[headerLc] = true; 143 | return; 144 | } 145 | 146 | try { 147 | // throws INVALID_STATE_ERROR if called before `open()` 148 | xhr2.setRequestHeader(header, value); 149 | } catch(e) { 150 | console.error('failed to set header: ' + header); 151 | console.error(e); 152 | } 153 | } 154 | 155 | // A little confusing that the request object gives you 156 | // the response handlers and that the upload gives you 157 | // the request handlers, but oh well 158 | xhr2 = new XMLHttpRequest(); 159 | xhr2Request = xhr2.upload; 160 | 161 | /* Proper States */ 162 | xhr2.addEventListener('loadstart', function (ev) { 163 | // this fires when the request starts, 164 | // but shouldn't fire until the request has loaded 165 | // and the response starts 166 | req.emit('loadstart', ev); 167 | resetTimeout(); 168 | }, true); 169 | xhr2.addEventListener('progress', function (ev) { 170 | if (!req.loaded) { 171 | req.loaded = true; 172 | req.emit('progress', {}); 173 | req.emit('load', {}); 174 | } 175 | if (!res.loadstart) { 176 | res.headers = xhr2.getAllResponseHeaders(); 177 | res.loadstart = true; 178 | res.emit('loadstart', ev); 179 | } 180 | res.emit('progress', ev); 181 | resetTimeout(); 182 | }, true); 183 | xhr2.addEventListener('load', function (ev) { 184 | if (xhr2.status >= 400) { 185 | ev.error = new Error(xhr2.status); 186 | } 187 | ev.target.result = encodeData(options, xhr2); 188 | res.emit('load', ev); 189 | }, true); 190 | /* 191 | xhr2Request.addEventListener('loadstart', function (ev) { 192 | req.emit('loadstart', ev); 193 | resetTimeout(); 194 | }, true); 195 | */ 196 | xhr2Request.addEventListener('load', function (ev) { 197 | resetTimeout(); 198 | req.loaded = true; 199 | req.emit('load', ev); 200 | res.loadstart = true; 201 | res.emit('loadstart', {}); 202 | }, true); 203 | xhr2Request.addEventListener('progress', function (ev) { 204 | resetTimeout(); 205 | req.emit('progress', ev); 206 | }, true); 207 | 208 | 209 | /* Error States */ 210 | xhr2.addEventListener('abort', function (ev) { 211 | res.emit('abort', ev); 212 | }, true); 213 | xhr2Request.addEventListener('abort', function (ev) { 214 | req.emit('abort', ev); 215 | }, true); 216 | xhr2.addEventListener('error', function (ev) { 217 | res.emit('error', ev); 218 | }, true); 219 | xhr2Request.addEventListener('error', function (ev) { 220 | req.emit('error', ev); 221 | }, true); 222 | // the "Request" is what timeouts 223 | // the "Response" will timeout as well 224 | xhr2.addEventListener('timeout', function (ev) { 225 | req.emit('timeout', ev); 226 | }, true); 227 | xhr2Request.addEventListener('timeout', function (ev) { 228 | req.emit('timeout', ev); 229 | }, true); 230 | 231 | /* Cleanup */ 232 | res.on('loadend', function () { 233 | // loadend is managed by AHR 234 | req.status = xhr2.status; 235 | res.status = xhr2.status; 236 | clearTimeout(timeoutToken); 237 | }); 238 | 239 | if (options.username) { 240 | xhr2.open(options.method, options.href, true, options.username, options.password); 241 | } else { 242 | xhr2.open(options.method, options.href, true); 243 | } 244 | 245 | Object.keys(options.headers).forEach(sanatizeHeaders); 246 | 247 | setTimeout(function () { 248 | if ('binary' === options.overrideResponseType) { 249 | xhr2.overrideMimeType("text/plain; charset=x-user-defined"); 250 | xhr2.responseType = 'arraybuffer'; 251 | } 252 | try { 253 | xhr2.send(options.encodedBody); 254 | } catch(e) { 255 | req.emit('error', e); 256 | } 257 | }, 1); 258 | 259 | 260 | req.abort = function () { 261 | xhr2.abort(); 262 | }; 263 | res.abort = function () { 264 | xhr2.abort(); 265 | }; 266 | 267 | res.browserRequest = xhr2; 268 | return res; 269 | } 270 | 271 | function send(req, res) { 272 | var options = req.userOptions; 273 | // TODO fix this ugly hack 274 | url = url || require('url'); 275 | if (options.jsonp && options.jsonpCallback) { 276 | return browserJsonpClient(req, res); 277 | } 278 | return browserHttpClient(req, res); 279 | } 280 | 281 | module.exports = send; 282 | }()); 283 | -------------------------------------------------------------------------------- /lib/browser/jsonp.js: -------------------------------------------------------------------------------- 1 | /* 2 | loadstart; 3 | progress; 4 | abort; 5 | error; 6 | load; 7 | timeout; 8 | loadend; 9 | */ 10 | (function () { 11 | "use strict"; 12 | 13 | function browserJsonpClient(req, res) { 14 | // TODO check for Same-domain / XHR2/CORS support 15 | // before attempting to insert script tag 16 | // Those support headers and such, which are good 17 | var options = req.userOptions 18 | , cbkey = options.jsonpCallback 19 | , script = document.createElement("script") 20 | , head = document.getElementsByTagName("head")[0] || document.documentElement 21 | , addParamsToUri = require('../utils').addParamsToUri 22 | , timeout 23 | , fulfilled; // TODO move this logic elsewhere into the emitter 24 | 25 | // cleanup: cleanup window and dom 26 | function cleanup() { 27 | fulfilled = true; 28 | window[cbkey] = undefined; 29 | try { 30 | delete window[cbkey]; 31 | // may have already been removed 32 | head.removeChild(script); 33 | } catch(e) {} 34 | } 35 | 36 | function abortRequest() { 37 | req.emit('abort'); 38 | cleanup(); 39 | } 40 | 41 | function abortResponse() { 42 | res.emit('abort'); 43 | cleanup(); 44 | } 45 | 46 | function prepareResponse() { 47 | // Sanatize data, Send, Cleanup 48 | function onSuccess(data) { 49 | var ev = { 50 | lengthComputable: false, 51 | loaded: 1, 52 | total: 1 53 | }; 54 | if (fulfilled) { 55 | return; 56 | } 57 | 58 | clearTimeout(timeout); 59 | res.emit('loadstart', ev); 60 | // sanitize 61 | data = JSON.parse(JSON.stringify(data)); 62 | res.emit('progress', ev); 63 | ev.target = { result: data }; 64 | res.emit('load', ev); 65 | cleanup(); 66 | } 67 | 68 | function onTimeout() { 69 | res.emit('timeout', {}); 70 | res.emit('error', new Error('timeout')); 71 | cleanup(); 72 | } 73 | 74 | window[cbkey] = onSuccess; 75 | // onError: Set timeout if script tag fails to load 76 | if (options.timeout) { 77 | timeout = setTimeout(onTimeout, options.timeout); 78 | } 79 | } 80 | 81 | function makeRequest() { 82 | var ev = {} 83 | , jsonp = {}; 84 | 85 | function onError(ev) { 86 | res.emit('error', ev); 87 | } 88 | 89 | // ?search=kittens&jsonp=jsonp123456 90 | jsonp[options.jsonp] = options.jsonpCallback; 91 | options.href = addParamsToUri(options.href, jsonp); 92 | 93 | // Insert JSONP script into the DOM 94 | // set script source to the service that responds with thepadded JSON data 95 | req.emit('loadstart', ev); 96 | try { 97 | script.setAttribute("type", "text/javascript"); 98 | script.setAttribute("async", "async"); 99 | script.setAttribute("src", options.href); 100 | // Note that this only works in some browsers, 101 | // but it's better than nothing 102 | script.onerror = onError; 103 | head.insertBefore(script, head.firstChild); 104 | } catch(e) { 105 | req.emit('error', e); 106 | } 107 | 108 | // failsafe cleanup 109 | setTimeout(cleanup, 2 * 60 * 1000); 110 | // a moot point since the "load" occurs so quickly 111 | req.emit('progress', ev); 112 | req.emit('load', ev); 113 | } 114 | 115 | setTimeout(makeRequest, 0); 116 | req.abort = abortRequest; 117 | res.abort = abortResponse; 118 | prepareResponse(); 119 | 120 | return res; 121 | } 122 | 123 | module.exports = browserJsonpClient; 124 | }()); 125 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*jslint devel: true, debug: true, es5: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */ 2 | (function () { 3 | "use strict"; 4 | 5 | var EventEmitter = require('events').EventEmitter 6 | , Future = require('future') 7 | , Join = require('join') 8 | , ahrOptions 9 | , nextTick 10 | , utils 11 | , preset 12 | ; 13 | 14 | function nextTick(fn, a, b, c, d) { 15 | try { 16 | process.nextTick(fn, a, b, c, d); 17 | } catch(e) { 18 | setTimeout(fn, 0, a, b, c, d); 19 | } 20 | } 21 | 22 | ahrOptions = require('./options'); 23 | utils = require('./utils'); 24 | 25 | preset = utils.preset; 26 | 27 | // The normalization starts here! 28 | function NewEmitter() { 29 | var emitter = new EventEmitter() 30 | , promise = Future() 31 | , ev = { 32 | lengthComputable: false 33 | , loaded: 0 34 | , total: undefined 35 | }; 36 | 37 | function loadend(ev, errmsg) { 38 | ev.error = ev.error || errmsg && new Error(errmsg); 39 | nextTick(function () { 40 | emitter.emit('loadend', ev); 41 | }); 42 | } 43 | 44 | emitter.done = 0; 45 | 46 | // any error in the quest causes the response also to fail 47 | emitter.on('loadend', function (ev) { 48 | emitter.done += 1; 49 | 50 | if (emitter.done > 1) { 51 | console.warn('loadend called ' + emitter.done + ' times'); 52 | return; 53 | } 54 | 55 | // in FF this is only a getter, setting is not allowed 56 | if (!ev.target) { 57 | ev.target = {}; 58 | } 59 | 60 | promise.fulfill(emitter.error || ev.error, emitter, ev.target.result, ev.error ? false : true); 61 | }); 62 | 63 | emitter.on('timeout', function (ev) { 64 | if (!emitter.error) { 65 | emitter.error = ev; 66 | loadend(ev, 'timeout'); 67 | } 68 | }); 69 | 70 | emitter.on('abort', function (ev) { 71 | if (!emitter.error) { 72 | loadend(ev, 'abort'); 73 | } 74 | }); 75 | 76 | emitter.on('error', function (err, evn) { 77 | // TODO rethrow the error if there are no listeners (incl. promises) 78 | //if (respEmitter.listeners.loadend) {} 79 | 80 | emitter.error = err; 81 | ev.error = err; 82 | if (evn) { 83 | ev.lengthComputable = evn.lengthComputable || true; 84 | ev.loaded = evn.loaded || 0; 85 | ev.total = evn.total; 86 | } 87 | if (!emitter.error) { 88 | loadend(ev); 89 | } 90 | }); 91 | 92 | // TODO there can actually be multiple load events per request 93 | // as is the case with mjpeg, streaming media, and ad-hoc socket-ish things 94 | emitter.on('load', function (evn) { 95 | // ensure that `loadend` is after `load` for all interested parties 96 | loadend(evn); 97 | }); 98 | 99 | // TODO 3.0 remove when 100 | emitter.when = promise.when; 101 | 102 | return emitter; 103 | } 104 | 105 | 106 | // 107 | // Emulate `request` 108 | // 109 | function ahr(options, callback) { 110 | var NativeHttpClient 111 | , req = NewEmitter() 112 | , res = NewEmitter() 113 | ; 114 | 115 | res.request = req.request = req; 116 | req.response = res.response = res; 117 | 118 | if (callback || options.callback) { 119 | // TODO 3.0 remove when 120 | return ahr(options).when(callback); 121 | } 122 | 123 | if ('string' === typeof options) { 124 | options = { 125 | href: options 126 | }; 127 | } 128 | 129 | ahrOptions.handleOptions(options); 130 | 131 | // todo throw all the important properties in the request 132 | req.userOptions = options; 133 | // in the browser tradition 134 | res.upload = req; 135 | 136 | // if the request fails, then the response must also fail 137 | req.on('error', function (err, ev) { 138 | if (!res.error) { 139 | res.emit('error', err, ev); 140 | } 141 | }); 142 | req.on('timeout', function (ev) { 143 | if (!res.error) { 144 | res.emit('timeout', ev); 145 | } 146 | }); 147 | 148 | try { 149 | // tricking pakmanager to ignore the node stuff 150 | var client = './node'; 151 | NativeHttpClient = require(client); 152 | } catch(e) { 153 | NativeHttpClient = require('./browser'); 154 | } 155 | 156 | return NativeHttpClient(req, res); 157 | }; 158 | ahr.globalOptionKeys = ahrOptions.globalOptionKeys; 159 | ahr.globalOption = ahrOptions.globalOption; 160 | ahr.setGlobalOptions = ahrOptions.setGlobalOptions; 161 | ahr.handleOptions = ahrOptions.handleOptions; 162 | 163 | 164 | // TODO 3.0 remove join 165 | ahr.join = Join; 166 | 167 | 168 | // 169 | // 170 | // All of these convenience methods are safe to cut if needed to save kb 171 | // 172 | // 173 | function allRequests(method, href, query, body, jsonp, options, callback) { 174 | options = options || {}; 175 | 176 | if (method) { options.method = method; } 177 | if (href) { options.href = href; } 178 | if (jsonp) { options.jsonp = jsonp; } 179 | 180 | if (query) { options.query = preset((query || {}), (options.query || {})) } 181 | if (body) { options.body = body; } 182 | 183 | return ahr(options, callback); 184 | } 185 | 186 | ahr.http = ahr; 187 | ahr.file = ahr; 188 | // TODO copy the jquery / reqwest object syntax 189 | // ahr.ajax = ahr; 190 | 191 | // HTTP jQuery-like body-less methods 192 | ['HEAD', 'GET', 'DELETE', 'OPTIONS'].forEach(function (verb) { 193 | verb = verb.toLowerCase(); 194 | ahr[verb] = function (href, query, options, callback) { 195 | return allRequests(verb, href, query, undefined, undefined, options, callback); 196 | }; 197 | }); 198 | 199 | // Correcting an oversight of jQuery. 200 | // POST and PUT can have both query (in the URL) and data (in the body) 201 | ['POST', 'PUT'].forEach(function (verb) { 202 | verb = verb.toLowerCase(); 203 | ahr[verb] = function (href, query, body, options, callback) { 204 | return allRequests(verb, href, query, body, undefined, options, callback); 205 | }; 206 | }); 207 | 208 | // JSONP 209 | ahr.jsonp = function (href, jsonp, query, options, callback) { 210 | if (!jsonp || 'string' !== typeof jsonp) { 211 | throw new Error("'jsonp' is not an optional parameter.\n" + 212 | "If you believe that this should default to 'callback' rather" + 213 | "than throwing an error, please file a bug"); 214 | } 215 | 216 | return allRequests('GET', href, query, undefined, jsonp, options, callback); 217 | }; 218 | 219 | // HTTPS 220 | ahr.https = function (options, callback) { 221 | if ('string' === typeof options) { 222 | options = { 223 | href: options 224 | }; 225 | } 226 | 227 | options.ssl = true; 228 | options.protocol = "https:"; 229 | 230 | return ahr(options, callback); 231 | }; 232 | 233 | ahr.tcp = function (options, callback) { 234 | if ('string' === typeof options) { 235 | options = { 236 | href: options 237 | }; 238 | } 239 | 240 | options.protocol = "tcp:"; 241 | 242 | return ahr(options, callback); 243 | }; 244 | 245 | ahr.udp = function (options, callback) { 246 | if ('string' === typeof options) { 247 | options = { 248 | href: options 249 | }; 250 | } 251 | 252 | options.protocol = "udp:"; 253 | 254 | return ahr(options, callback); 255 | }; 256 | 257 | module.exports = ahr; 258 | }()); 259 | -------------------------------------------------------------------------------- /lib/node/file-client-node.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var fs = require('fs'); 5 | 6 | function nodeFileClient(req, res) { 7 | var options = req.userOptions 8 | , size = 0 9 | , data = [] 10 | , reqEv; 11 | 12 | function onFreadData(chunk) { 13 | size += chunk.length; 14 | res.emit('progress', { 15 | lengthComputable: true 16 | , loaded: size 17 | , total: size 18 | }); 19 | res.emit('data', chunk); 20 | data.push(chunk); 21 | } 22 | 23 | function onFreadError(err) { 24 | res.emit('error', err); 25 | } 26 | 27 | function onFreadEnd() { 28 | var buffer = Buffer.concat(data); 29 | res.emit('load', { 30 | lengthComputable: true 31 | , loaded: size 32 | , total: size 33 | , target: { 34 | result: buffer 35 | } 36 | }); 37 | } 38 | 39 | // Request automatically succeeds 40 | reqEv = { 41 | lengthComputable: false, 42 | loaded: 0, 43 | total: undefined 44 | }; 45 | req.emit('loadstart', reqEv); 46 | req.emit('load', reqEv); 47 | req.emit('loadend', reqEv); 48 | 49 | // Response... not so much 50 | // TODO JSON, MIME, etc 51 | res = fs.createReadStream(options.pathname); 52 | res.on('error', onFreadError); 53 | res.on('data', onFreadData); 54 | res.on('end', onFreadEnd); 55 | 56 | return res; 57 | } 58 | 59 | module.exports = nodeFileClient; 60 | }()); 61 | -------------------------------------------------------------------------------- /lib/node/form-content.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var EventEmitter = require('events').EventEmitter; 5 | 6 | // TODO create(filters) and streamers 7 | // TODO for the real FormData sizes >= 3gb the length 8 | // should be given in kb or mb 9 | 10 | function FormContent(body) { 11 | this.body = body; 12 | } 13 | 14 | FormContent.prototype.serialize = function () { 15 | var emitter = new EventEmitter() 16 | , self = this; 17 | 18 | function fireAllEvents() { 19 | var body = self.body 20 | , length = body.length; 21 | 22 | emitter.emit('size', length); 23 | emitter.emit('loadstart', { 24 | lengthComputable: true, 25 | loaded: 0, 26 | total: length 27 | }); 28 | emitter.emit('progress', { 29 | lengthComputable: true, 30 | loaded: length, 31 | total: length 32 | }); 33 | emitter.emit('load', { 34 | lengthComputable: true, 35 | loaded: length, 36 | total: length, 37 | target: { 38 | result: body 39 | } 40 | }); 41 | emitter.emit('loadend', { 42 | lengthComputable: true, 43 | loaded: length, 44 | total: length 45 | }); 46 | } 47 | 48 | process.nextTick(fireAllEvents); 49 | 50 | return emitter; 51 | }; 52 | 53 | module.exports.FormContent = FormContent; 54 | }()); 55 | -------------------------------------------------------------------------------- /lib/node/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./request-http'); 2 | -------------------------------------------------------------------------------- /lib/node/request-http.js: -------------------------------------------------------------------------------- 1 | /*jslint white: false, devel: true, onevar: true, undef: true, node: true, nomen: true, regexp: true, plusplus: true, bitwise: true, es5: true, newcap: true, strict: true, maxerr: 5 */ 2 | (function (undefined) { 3 | "use strict"; 4 | 5 | require('bufferjs'); 6 | 7 | var nodeFileClient = require('./file-client-node') 8 | , nodeTcpClient = require('./request-tcp') 9 | , nodeUdpClient = require('./request-udp') 10 | , nodeHttpResponse = require('./response-http') 11 | , url = require('url') 12 | , http = require('http') 13 | , https = require('https') 14 | , FormData = require('FormData') 15 | , FormContent = require('./form-content').FormContent 16 | , utils = require('../utils') 17 | , parseJsonp = utils.parseJsonp 18 | , preset = utils.preset 19 | , globalOptions; 20 | 21 | 22 | globalOptions = { 23 | redirectCountMax: 3 24 | }; 25 | 26 | function nodeHttpRequest(req, res) { 27 | var requestOptions 28 | , options = req.userOptions 29 | , ev 30 | , size = 0 31 | , nativeNodeRequest 32 | , httpClient; 33 | 34 | // ECONNECT, EPARSE 35 | function onRequestError(err) { 36 | clearTimeout(req.timeoutToken); // Clear connection timeout 37 | req.cancelled = true; 38 | req.emit('error', err); 39 | } 40 | 41 | // Set timeout for initial contact of server 42 | function onRequestTimeout() { 43 | req.cancelled = true; 44 | req.emit('error', new Error('timeout')); 45 | req.abort(); 46 | } 47 | 48 | function makeRequest() { 49 | var encodedBody = options.encodedBody 50 | , bodyStream 51 | ; 52 | 53 | function clientRequest() { 54 | 55 | function abort() { 56 | nativeNodeRequest && nativeNodeRequest.abort(); 57 | } 58 | 59 | function abortNextTick() { 60 | process.nextTick(abort); 61 | } 62 | 63 | requestOptions = { 64 | host: options.hostname 65 | , port: options.port 66 | , path: options.pathname + options.search || '' 67 | , method: options.method 68 | , headers: options.headers // auth is in the headers 69 | }; 70 | 71 | requestOptions.headers.host = requestOptions.host 72 | if (!requestOptions.port) { 73 | requestOptions.port = 80; 74 | } 75 | 76 | if (80 !== parseInt(requestOptions.port) && 443 !== parseInt(requestOptions.port)) { 77 | requestOptions.headers.host += (':' + requestOptions.port); 78 | } 79 | 80 | // create Connection, Request 81 | httpClient = ('https:' === options.protocol) ? https : http; 82 | nativeNodeRequest = httpClient.request(requestOptions, function (response) { 83 | var ev = {} 84 | ; 85 | 86 | if (req.nodeData) { 87 | ev.lengthComputable = true; 88 | ev.loaded = req.nodeData.length; 89 | ev.total = req.nodeData.length; 90 | } 91 | 92 | req.emit('load', ev); 93 | clearTimeout(req.timeoutToken); 94 | req.nodeHttpRequest = nodeHttpRequest; 95 | res.nodeResponse = response; 96 | res.request = req; 97 | nodeHttpResponse(req, res); 98 | }); 99 | 100 | nativeNodeRequest.on('error', onRequestError); 101 | 102 | req.nodeRequest = nativeNodeRequest; 103 | req.abort = abortNextTick; 104 | res.abort = abortNextTick; 105 | req.headers = nativeNodeRequest.headers; 106 | 107 | // can't call emit in the same tick the handler is assigned 108 | process.nextTick(function () { 109 | req.emit('loadstart', {}); 110 | }); 111 | } 112 | 113 | function sendBody() { 114 | // TODO stream 115 | clientRequest(); 116 | bodyStream.on('progress', function (ev) { 117 | req.emit('progress', ev); 118 | }); 119 | bodyStream.on('load', function (ev) { 120 | var data; 121 | if (ev && ev.target) { 122 | data = ev.target.result; 123 | } else { 124 | data = ev; 125 | } 126 | nativeNodeRequest.end(data); 127 | req.nodeData = data; 128 | req.emit('progress', { 129 | lengthComputable: true 130 | , loaded: data.length 131 | , total: data.length 132 | , target: { 133 | result: data 134 | } 135 | }); 136 | }); 137 | } 138 | 139 | if (!encodedBody) { 140 | clientRequest(); 141 | nativeNodeRequest.end(); 142 | return res; 143 | } 144 | 145 | if (encodedBody instanceof FormData) { 146 | // Chunked encoding is off by default because: 147 | // * If the body is a stream, we can't compute the length 148 | // * Many (prehaps most) webservers don't support client-side chunked encoding 149 | encodedBody.setNodeChunkedEncoding(options.chunked); 150 | // TODO .nodeSetChunkedEncoding(options.chunked); 151 | // TODO pass in headers instead of nodeGetContentType? 152 | bodyStream = encodedBody.serialize(); 153 | // must get boundary, etc 154 | options.headers["Content-Type"] = encodedBody.getContentType(); 155 | // TODO .nodeGetContentType(); 156 | } else { 157 | bodyStream = new FormContent(encodedBody).serialize(); 158 | // TODO .nodeSerialize(); 159 | } 160 | 161 | // TODO document and use forceChunked 162 | if (options.chunked) { 163 | // Node sets this by default 164 | options.headers['Transfer-Encoding'] = 'chunked'; 165 | delete options.headers["Content-Length"]; 166 | sendBody(); 167 | } else { 168 | bodyStream.on('size', function (size) { 169 | options.headers["Content-Length"] = size; 170 | delete options.headers['Transfer-Encoding']; 171 | sendBody(); 172 | }); 173 | } 174 | } 175 | 176 | req.timeoutToken = setTimeout(onRequestTimeout, options.timeout); 177 | 178 | makeRequest(); 179 | 180 | return res; 181 | } 182 | 183 | function send(req, res) { 184 | var options = req.userOptions; 185 | 186 | switch(options.protocol) { 187 | case 'file:': 188 | return nodeFileClient(req, res); 189 | break; 190 | 191 | case 'tcp:': 192 | case 'tcps:': 193 | return nodeTcpClient(req, res); 194 | break; 195 | 196 | case 'udp:': 197 | return nodeUdpClient(req, res); 198 | break; 199 | 200 | /* 201 | case 'https:': 202 | httpClient = https; 203 | break; 204 | 205 | case 'http:': 206 | default: 207 | httpClient = http; 208 | */ 209 | } 210 | 211 | if (options.jsonp) { 212 | options.stream = undefined; 213 | delete options.stream; 214 | } 215 | 216 | // can be set to undefined 217 | if (!('user-agent' in options.headers)) { 218 | options.headers['user-agent'] = 'Node.JS (AbstractHttpRequest v2)'; 219 | } 220 | 221 | return nodeHttpRequest(req, res); 222 | } 223 | 224 | module.exports = send; 225 | }()); 226 | -------------------------------------------------------------------------------- /lib/node/request-tcp.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var net = require('net'); 5 | 6 | // TODO AHR2 style options parsing 7 | function create(req, res) { 8 | var options = req.userOptions 9 | , socket 10 | ; 11 | 12 | function onRequestError(err) { 13 | req.emit('error', err); 14 | } 15 | 16 | function onResponseError(err) { 17 | res.emit('error', err); 18 | } 19 | 20 | function onRequestDrain() { 21 | socket.removeListener('error', onRequestError); 22 | socket.on('error', onResponseError); 23 | req.emit('load', {}); 24 | req.isComplete = true; 25 | } 26 | 27 | function onConnect() { 28 | req.emit('progress', {}); 29 | 30 | socket.end(options.encodedBody); 31 | socket.on('drain', onRequestDrain); 32 | 33 | // same default timeout as http 34 | socket.setTimeout(options.timeout || 120 * 1000, function () { 35 | socket.destroy(); 36 | 37 | if (!req.isComplete) { 38 | req.emit('timeout', {}); 39 | } else { 40 | res.emit('timeout', {}); 41 | } 42 | }); 43 | } 44 | 45 | 46 | socket = new net.Socket({ 47 | allowHalfOpen: true 48 | // , fd: null 49 | // , type: null 50 | }); 51 | 52 | socket.on('error', onRequestError); 53 | 54 | socket.on('data', function (chunk) { 55 | res.emit('progress', { target: { result: chunk } }); 56 | }); 57 | 58 | socket.on('end', function () { 59 | res.status = 200; 60 | res.emit('load', {}); 61 | }); 62 | 63 | 64 | // makes a request as a client 65 | socket.connect(options.port, options.hostname, onConnect); 66 | 67 | return res; 68 | } 69 | 70 | module.exports = create; 71 | }()); 72 | -------------------------------------------------------------------------------- /lib/node/request-udp.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var dgram = require('dgram'); 5 | 6 | function create(req, res) { 7 | var options = req.userOptions 8 | , udpWait = options.udpWait || 0 9 | , client = dgram.createSocket("udp4") 10 | , buffer = new Buffer(options.encodedBody) 11 | ; 12 | 13 | // TODO 14 | if (udpWait) { 15 | // startListener 16 | } 17 | 18 | function handleSent(err) { 19 | client.close(); 20 | 21 | if (err) { 22 | req.emit('error', err); 23 | return; 24 | } 25 | 26 | req.emit('load', {}); 27 | res.emit('load', {}); 28 | /* 29 | req.status = 0; 30 | res.status = 0; 31 | */ 32 | } 33 | 34 | client.send( 35 | buffer 36 | , 0 37 | , buffer.length 38 | , options.port 39 | , options.hostname 40 | , handleSent 41 | ); 42 | 43 | return res; 44 | } 45 | 46 | module.exports = create; 47 | }()); 48 | -------------------------------------------------------------------------------- /lib/node/response-http.js: -------------------------------------------------------------------------------- 1 | /*jslint white: false, devel: true, onevar: true, undef: true, node: true, nomen: true, regexp: true, plusplus: true, bitwise: true, es5: true, newcap: true, strict: true, maxerr: 5 */ 2 | (function (undefined) { 3 | "use strict"; 4 | 5 | require('bufferjs'); 6 | 7 | var url = require('url') 8 | , utils = require('../utils') 9 | , parseJsonp = utils.parseJsonp 10 | , extend = utils.extend; 11 | 12 | // TODO NewEvent(type, size, total, data, chunk, err) 13 | function onResponse(req, res) { 14 | var data = [] 15 | , response = res.nodeResponse 16 | , options = req.userOptions; 17 | 18 | function onRedirect() { 19 | // Redirect when requested 20 | options.href = response.headers.location; 21 | extend(options, url.parse(options.href)); 22 | options.redirectCount += 1; 23 | if (options.headers) { 24 | delete options.headers.host; 25 | } 26 | 27 | // Seems to work just fine 28 | // TODO 29 | // XXX wrap loadstart, error, load, progress 30 | // TODO 31 | req.on = function () {}; 32 | req.emit = function () {}; 33 | req.nodeHttpRequest(req, res); 34 | } 35 | 36 | function onTimeout() { 37 | var err = new Error('timeout'); 38 | res.error = err; 39 | res.cancelled = true; 40 | res.emit('error', err); 41 | } 42 | 43 | function onData(chunk) { 44 | clearTimeout(res.timeoutToken); 45 | res.timeoutToken = setTimeout(onTimeout, options.timeout); 46 | 47 | res.size += chunk.length; 48 | data.push(chunk); 49 | res.emit('data', chunk); 50 | res.emit('progress', { 51 | // TODO check the content-length if available 52 | lengthComputable: false 53 | , loaded: res.size 54 | , total: undefined 55 | , nodePart: chunk 56 | }); 57 | } 58 | 59 | function onError(err) { 60 | res.emit('error', err); 61 | } 62 | 63 | function onEnd() { 64 | if (res.cancelled) { 65 | return; 66 | } 67 | 68 | // TODO try/catch to use https://github.com/bnoordhuis/node-buffertools 69 | data = Buffer.concat(data); 70 | 71 | clearTimeout(res.timeoutToken); // Clear request timeout 72 | 73 | if ('undefined' === typeof res.error && options.jsonp && data) { 74 | data = data.toString('utf8'); 75 | data = parseJsonp(options.jsonpCallback, data); 76 | } 77 | 78 | // TODO this should go into a connect-like system 79 | // this will handle bad json headers such as javascript/ or x-json 80 | // but shouldn't match headers such as foobar/rajsonite 81 | if (/[^a-z]json\b/i.exec(res.headers['content-type'])) { 82 | try { 83 | data = JSON.parse(data.toString('utf8')); 84 | } catch(e) { 85 | res.emit('error', e); 86 | return; 87 | } 88 | } 89 | 90 | res.emit('load', { 91 | lengthComputable: true 92 | , loaded: data.length 93 | , total: data.length 94 | , target: { 95 | result: data 96 | } 97 | }); 98 | } 99 | 100 | res.statusCode = response.statusCode; 101 | res.headers = response.headers; 102 | 103 | if (res.statusCode >= 400) { 104 | // Error on Error 105 | res.error = res.error || new Error(res.statusCode); 106 | } else if (response.statusCode >= 300 && 107 | response.statusCode !== 304 && 108 | response.headers.location && 109 | options.followRedirect && 110 | options.redirectCount < options.redirectCountMax) { 111 | return onRedirect(); 112 | } 113 | 114 | // Set timeout for request 115 | res.timeoutToken = setTimeout(onTimeout, options.timeout); 116 | res.size = 0; 117 | 118 | // TODO use headers to determine size 119 | res.emit('loadstart', {}); 120 | response.on('data', onData); 121 | response.on('error', onError); 122 | response.on('end', onEnd); 123 | } 124 | 125 | module.exports = onResponse; 126 | }()); 127 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | /*jslint devel: true, debug: true, es5: true, onevar: true, undef: true, nomen: true, eqeqeq: true, plusplus: true, bitwise: true, regexp: true, newcap: true, immed: true, strict: true */ 2 | (function () { 3 | "use strict"; 4 | 5 | var globalOptions 6 | , ahrOptions = exports 7 | , url = require('url') 8 | , querystring = require('querystring') 9 | , File = require('File') 10 | , FileList = require('FileList') 11 | , btoa = require('btoa') 12 | , utils = require('./utils') 13 | , location 14 | , uriEncodeObject 15 | , clone 16 | , preset 17 | , objectToLowerCase 18 | ; 19 | 20 | /* 21 | * Some browsers don't yet have support for FormData. 22 | * This isn't a real fix, but it stops stuff from crashing. 23 | * 24 | * This should probably be replaced with a real FormData impl, but whatever. 25 | */ 26 | function FormData() { 27 | } 28 | 29 | try { 30 | FormData = require('FormData'); 31 | } catch (e) { 32 | console.warn('FormData does not exist; using a NOP instead'); 33 | } 34 | 35 | // TODO get the "root" dir... somehow 36 | try { 37 | location = require('./location'); 38 | } catch(e) { 39 | location = require('location'); 40 | } 41 | 42 | uriEncodeObject = utils.uriEncodeObject; 43 | clone = utils.clone; 44 | preset = utils.preset; 45 | objectToLowerCase = utils.objectToLowerCase; 46 | 47 | globalOptions = { 48 | ssl: false, 49 | method: 'GET', 50 | headers: { 51 | //'accept': "application/json; charset=utf-8, */*; q=0.5" 52 | }, 53 | redirectCount: 0, 54 | redirectCountMax: 5, 55 | // contentType: 'json', 56 | // accept: 'json', 57 | followRedirect: true, 58 | timeout: 20000 59 | }; 60 | 61 | 62 | // 63 | // Manage global options while keeping state safe 64 | // 65 | ahrOptions.globalOptionKeys = function () { 66 | return Object.keys(globalOptions); 67 | }; 68 | 69 | ahrOptions.globalOption = function (key, val) { 70 | if ('undefined' === typeof val) { 71 | return globalOptions[key]; 72 | } 73 | if (null === val) { 74 | val = undefined; 75 | } 76 | globalOptions[key] = val; 77 | }; 78 | 79 | ahrOptions.setGlobalOptions = function (bag) { 80 | Object.keys(bag).forEach(function (key) { 81 | globalOptions[key] = bag[key]; 82 | }); 83 | }; 84 | 85 | 86 | /* 87 | * About the HTTP spec and which methods allow bodies, etc: 88 | * http://stackoverflow.com/questions/299628/is-an-entity-body-allowed-for-an-http-delete-request 89 | */ 90 | function checkBodyAllowed(options) { 91 | var method = options.method.toUpperCase(); 92 | if ('HEAD' !== method && 'GET' !== method && 'DELETE' !== method && 'OPTIONS' !== method) { 93 | return true; 94 | } 95 | if (options.body && !options.forceAllowBody) { 96 | throw new Error("The de facto standard is that '" + method + "' should not have a body.\n" + 97 | "Most web servers just ignore it. Please use 'query' rather than 'body'.\n" + 98 | "Also, you may consider filing this as a bug - please give an explanation.\n" + 99 | "Finally, you may allow this by passing { forceAllowBody: 'true' } "); 100 | } 101 | if (options.body && options.jsonp) { 102 | throw new Error("The de facto standard is that 'jsonp' should not have a body (and I don't see how it could have one anyway).\n" + 103 | "If you consider filing this as a bug please give an explanation."); 104 | } 105 | } 106 | 107 | 108 | /* 109 | Node.js 110 | 111 | > var url = require('url'); 112 | > var urlstring = 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'; 113 | > url.parse(urlstring, true); 114 | { href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash', 115 | protocol: 'http:', 116 | host: 'user:pass@host.com:8080', 117 | auth: 'user:pass', 118 | hostname: 'host.com', 119 | port: '8080', 120 | pathname: '/p/a/t/h', 121 | search: '?query=string', 122 | hash: '#hash', 123 | 124 | slashes: true, 125 | query: {'query':'string'} } // 'query=string' 126 | */ 127 | 128 | /* 129 | Browser 130 | 131 | href: "http://user:pass@host.com:8080/p/a/t/h?query=string#hash" 132 | protocol: "http:" 133 | host: "host.com:8080" 134 | hostname: "host.com" 135 | port: "8080" 136 | pathname: "/p/a/t/h" 137 | search: '?query=string', 138 | hash: "#hash" 139 | 140 | origin: "http://host.com:8080" 141 | */ 142 | 143 | function handleUri(options) { 144 | var presets 145 | , urlObj 146 | ; 147 | 148 | presets = clone(globalOptions); 149 | 150 | if (!options) { 151 | throw new Error('ARe yOu kiddiNg me? You have to provide some sort of options'); 152 | } 153 | 154 | if ('string' === typeof options) { 155 | options = { 156 | href: options 157 | }; 158 | } 159 | if (options.uri || options.url) { 160 | console.log('Use `options.href`. `options.url` and `options.uri` are obsolete'); 161 | options.href = options.href || options.url || options.url; 162 | } 163 | if (options.params) { 164 | console.log('Use `options.query`. `options.params` is obsolete'); 165 | options.query = options.query || options.params; 166 | } 167 | 168 | 169 | // 170 | // pull `urlObj` from `options` 171 | // 172 | if (options.href) { 173 | urlObj = url.parse(options.href, true, true); 174 | // ignored anyway 175 | delete urlObj.href; 176 | // these trump other options 177 | delete urlObj.host; 178 | delete urlObj.search; 179 | } else { 180 | urlObj = { 181 | protocol: options.protocol || location.protocol 182 | // host trumps auth, hostname, and port 183 | , host: options.host 184 | , auth: options.auth 185 | , hostname: options.hostname || location.hostname 186 | , port: options.port || location.port 187 | , pathname: url.resolve(location.pathname, options.pathname || '') || '/' 188 | // search trumps query 189 | //, search: options.search 190 | , query: options.query || querystring.parse(options.search||"") 191 | , hash: options.hash 192 | }; 193 | } 194 | delete options.href; 195 | delete options.host; 196 | delete options.auth; 197 | delete options.hostname; 198 | delete options.port; 199 | delete options.path; 200 | delete options.search; 201 | delete options.query; 202 | delete options.hash; 203 | 204 | // Use SSL if desired 205 | if ('https:' === urlObj.protocol || '443' === urlObj.port || true === options.ssl) { 206 | options.ssl = true; 207 | urlObj.port = urlObj.port || '443'; 208 | // hopefully no one would set prt 443 to standard http 209 | urlObj.protocol = 'https:'; 210 | } 211 | 212 | if ('tcp:' === urlObj.protocol || 'tcps:' === urlObj.protocol || 'udp:' === urlObj.protocol) { 213 | options.method = options.method || 'POST'; 214 | } 215 | 216 | if (!options.method && (options.body || options.encodedBody)) { 217 | options.method = 'POST'; 218 | } 219 | 220 | if (options.jsonp) { 221 | // i.e. /path/to/res?x=y&jsoncallback=jsonp8765 222 | // i.e. /path/to/res?x=y&json=jsonp_ae75f 223 | options.jsonpCallback = 'jsonp_' + (new Date()).valueOf(); 224 | options.dataType = 'jsonp'; 225 | urlObj.query[options.jsonp] = options.jsonpCallback; 226 | } 227 | 228 | // for the sake of the browser, but it doesn't hurt node 229 | if (!urlObj.auth && options.username && options.password) { 230 | urlObj.auth = options.username + ':' + options.password; 231 | } else if (urlObj.auth) { 232 | urlObj.username = urlObj.auth.split(':')[0]; 233 | urlObj.password = urlObj.auth.split(':')[1]; 234 | } 235 | 236 | urlObj.href = url.format(urlObj); 237 | urlObj = url.parse(urlObj.href, true, true); 238 | 239 | preset(options, presets); 240 | preset(options, urlObj); 241 | options.syncback = options.syncback || function () {}; 242 | 243 | return options; 244 | } 245 | 246 | function handleHeaders(options) { 247 | var presets 248 | , ua 249 | ; 250 | 251 | presets = clone(globalOptions); 252 | 253 | options.headers = options.headers || {}; 254 | if (options.jsonp) { 255 | options.headers.accept = "text/javascript"; 256 | } 257 | // TODO user-agent should retain case 258 | options.headers = objectToLowerCase(options.headers || {}); 259 | options.headers = preset(options.headers, presets.headers); 260 | // TODO port? 261 | options.headers.host = options.hostname; 262 | options.headers = objectToLowerCase(options.headers); 263 | if (options.contentType) { 264 | options.headers['content-type'] = options.contentType; 265 | } 266 | 267 | // for the sake of node, but it doesn't hurt the browser 268 | if (options.auth) { 269 | options.headers.authorization = 'Basic ' + btoa(options.auth); 270 | } 271 | 272 | return options; 273 | } 274 | 275 | function hasFiles(body, formData) { 276 | var hasFile = false; 277 | if ('object' !== typeof body) { 278 | return false; 279 | } 280 | Object.keys(body).forEach(function (key) { 281 | var item = body[key]; 282 | if (item instanceof File) { 283 | hasFile = true; 284 | } else if (item instanceof FileList) { 285 | hasFile = true; 286 | } 287 | }); 288 | return hasFile; 289 | } 290 | function addFiles(body, formData) { 291 | 292 | Object.keys(body).forEach(function (key) { 293 | var item = body[key]; 294 | 295 | if (item instanceof File) { 296 | formData.append(key, item); 297 | } else if (item instanceof FileList) { 298 | item.forEach(function (file) { 299 | formData.append(key, file); 300 | }); 301 | } else { 302 | formData.append(key, item); 303 | } 304 | }); 305 | } 306 | 307 | // TODO convert object/map body into array body 308 | // { "a": 1, "b": 2 } --> [ "name": "a", "value": 1, "name": "b", "value": 2 ] 309 | // this would be more appropriate and in better accordance with the http spec 310 | // as it allows for a value such as "a" to have multiple values rather than 311 | // having to do "a1", "a2" etc 312 | function handleBody(options) { 313 | function bodyEncoder() { 314 | checkBodyAllowed(options); 315 | 316 | if (options.encodedBody) { 317 | return; 318 | } 319 | 320 | // 321 | // Check for HTML5 FileApi files 322 | // 323 | if (hasFiles(options.body)) { 324 | options.encodedBody = new FormData(); 325 | addFiles(options.body, options.encodedBody); 326 | } 327 | if (options.body instanceof FormData) { 328 | options.encodedBody = options.body; 329 | } 330 | if (options.encodedBody instanceof FormData) { 331 | // TODO: is this necessary? This breaks in the browser 332 | // options.headers["content-type"] = "multipart/form-data"; 333 | return; 334 | } 335 | 336 | if ('string' === typeof options.body) { 337 | options.encodedBody = options.body; 338 | } 339 | 340 | if (!options.headers["content-type"]) { 341 | //options.headers["content-type"] = "application/x-www-form-urlencoded"; 342 | options.headers["content-type"] = "application/json"; 343 | } 344 | 345 | if (!options.encodedBody) { 346 | if (options.headers["content-type"].match(/application\/json/) || 347 | options.headers["content-type"].match(/text\/javascript/)) { 348 | options.encodedBody = JSON.stringify(options.body); 349 | try { 350 | // so that node can know the true length of json strings 351 | options.encodedBody = new Buffer(options.encodedBody); 352 | } catch(e) { 353 | // this must be a browser 354 | // it will know the true length of a string without me telling it. 355 | } 356 | } else if (options.headers["content-type"].match(/application\/x-www-form-urlencoded/)) { 357 | options.encodedBody = uriEncodeObject(options.body); 358 | } 359 | 360 | if (!options.encodedBody) { 361 | throw new Error("'" + options.headers["content-type"] + "'" + "is not yet supported and you have not specified 'encodedBody'"); 362 | } 363 | 364 | options.headers["content-length"] = options.encodedBody.length; 365 | } 366 | } 367 | 368 | function removeContentBodyAndHeaders() { 369 | if (options.body) { 370 | throw new Error('You gave a body for one of HEAD, GET, DELETE, or OPTIONS'); 371 | } 372 | 373 | options.encodedBody = ""; 374 | options.headers["content-type"] = undefined; 375 | options.headers["content-length"] = undefined; 376 | options.headers["transfer-encoding"] = undefined; 377 | delete options.headers["content-type"]; 378 | delete options.headers["content-length"]; 379 | delete options.headers["transfer-encoding"]; 380 | } 381 | 382 | if ('file:' === options.protocol) { 383 | options.header = undefined; 384 | delete options.header; 385 | return; 386 | } 387 | 388 | // Create & Send body 389 | // TODO support streaming uploads 390 | options.headers["transfer-encoding"] = undefined; 391 | delete options.headers["transfer-encoding"]; 392 | 393 | if (options.body || options.encodedBody) { 394 | bodyEncoder(options); 395 | } else { // no body || body not allowed 396 | removeContentBodyAndHeaders(options); 397 | } 398 | } 399 | 400 | ahrOptions.handleOptions = function (options) { 401 | handleUri(options); 402 | handleHeaders(options); 403 | handleBody(options); 404 | 405 | return options; 406 | }; 407 | }()); 408 | -------------------------------------------------------------------------------- /lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ahr2" 3 | , "main": "index" 4 | , "version": "2.2.9" 5 | , "homepage": "https://github.com/coolaj86/abstract-http-request" 6 | , "description": "An Abstract Http Request for Node.JS (http/https) and the Browser (XMLHttpRequeuest2). For `npm install ahr2` for Node and `pakmanager build` for Ender / Pakmanager. It should be required as `var request = require('ahr2')`" 7 | , "repository": { 8 | "type": "git" 9 | , "url": "git://github.com/coolaj86/abstract-http-request.git" 10 | } 11 | , "keywords": ["xhr", "ahr", "xmlhttprequest", "http", "https", "file", "browser", "xhr2", "cors", "xdm", "jsonp"] 12 | , "author": "AJ ONeal (http://coolaj86.info)" 13 | , "contributors": [ 14 | "T. Jameson Little " 15 | ] 16 | , "engines": { 17 | "node": ">= 0.4.0" 18 | } 19 | , "browserDependencies": { 20 | "events.node": ">= 0.4.0" 21 | , "url": ">= 0.0.0" 22 | , "querystring": ">= 0.0.0" 23 | , "future": ">= 2.1.0" 24 | , "join": ">= 2.1.0" 25 | } 26 | , "dependencies": { 27 | "future": ">= 2.1.0" 28 | , "join": ">= 2.1.0" 29 | , "btoa": "1.x" 30 | , "bufferjs": "1.0.x" 31 | , "File": ">= 0.0.0" 32 | , "FileList": ">= 0.0.0" 33 | , "FormData": ">= 0.0.0" 34 | , "navigator": ">= 0.0.0" 35 | , "location": ">= 0.0.0" 36 | } 37 | , "lib": "." 38 | , "directories": { 39 | "lib": "." 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /*jslint white: false, onevar: true, undef: true, node: true, nomen: true, regexp: false, plusplus: true, bitwise: true, es5: true, newcap: true, maxerr: 5 */ 2 | (function () { 3 | "use strict"; 4 | 5 | var utils = exports 6 | , jsonpRegEx = /\s*([\$\w]+)\s*\(\s*(.*)\s*\)\s*/; 7 | 8 | utils.clone = function (obj) { 9 | return JSON.parse(JSON.stringify(obj)); 10 | }; 11 | 12 | // useful for extending global options onto a local variable 13 | utils.extend = function (global, local) { 14 | //global = utils.clone(global); 15 | Object.keys(local).forEach(function (key) { 16 | global[key] = local[key] || global[key]; 17 | }); 18 | return global; 19 | }; 20 | 21 | // useful for extending global options onto a local variable 22 | utils.preset = function (local, global) { 23 | // TODO copy functions 24 | // TODO recurse / deep copy 25 | global = utils.clone(global); 26 | Object.keys(global).forEach(function (key) { 27 | if ('undefined' === typeof local[key]) { 28 | local[key] = global[key]; 29 | } 30 | }); 31 | return local; 32 | }; 33 | 34 | utils.objectToLowerCase = function (obj, recurse) { 35 | // Make headers all lower-case 36 | Object.keys(obj).forEach(function (key) { 37 | var value; 38 | 39 | value = obj[key]; 40 | delete obj[key]; 41 | key = key.toLowerCase(); 42 | /* 43 | if ('string' === typeof value) { 44 | obj[key] = value.toLowerCase(); 45 | } else { 46 | obj[key] = value; 47 | } 48 | */ 49 | obj[key] = value; 50 | }); 51 | return obj; 52 | }; 53 | 54 | utils.parseJsonp = function (jsonpCallback, jsonp) { 55 | var match = jsonp.match(jsonpRegEx) 56 | , data 57 | , json; 58 | 59 | if (!match || !match[1] || !match[2]) { 60 | throw new Error('No JSONP matched'); 61 | } 62 | if (jsonpCallback !== match[1]) { 63 | throw new Error('JSONP callback doesn\'t match'); 64 | } 65 | json = match[2]; 66 | 67 | data = JSON.parse(json); 68 | return data; 69 | }; 70 | 71 | utils.uriEncodeObject = function(json) { 72 | var query = ''; 73 | 74 | try { 75 | JSON.parse(JSON.stringify(json)); 76 | } catch(e) { 77 | return 'ERR_CYCLIC_DATA_STRUCTURE'; 78 | } 79 | 80 | if ('object' !== typeof json) { 81 | return 'ERR_NOT_AN_OBJECT'; 82 | } 83 | 84 | Object.keys(json).forEach(function (key) { 85 | var param, value; 86 | 87 | // assume that the user meant to delete this element 88 | if ('undefined' === typeof json[key]) { 89 | return; 90 | } 91 | 92 | param = encodeURIComponent(key); 93 | value = encodeURIComponent(json[key]); 94 | query += '&' + param; 95 | 96 | // assume that the user wants just the param name sent 97 | if (null !== json[key]) { 98 | query += '=' + value; 99 | } 100 | }); 101 | 102 | // remove first '&' 103 | return query.substring(1); 104 | }; 105 | 106 | utils.addParamsToUri = function(uri, params) { 107 | var query 108 | , anchor = '' 109 | , anchorpos; 110 | 111 | uri = uri || ""; 112 | anchor = ''; 113 | params = params || {}; 114 | 115 | // just in case this gets used client-side 116 | if (-1 !== (anchorpos = uri.indexOf('#'))) { 117 | anchor = uri.substr(anchorpos); 118 | uri = uri.substr(0, anchorpos); 119 | } 120 | 121 | query = utils.uriEncodeObject(params); 122 | 123 | // cut the leading '&' if no other params have been written 124 | if (query.length > 0) { 125 | if (!uri.match(/\?/)) { 126 | uri += '?' + query; 127 | } else { 128 | uri += '&' + query; 129 | } 130 | } 131 | 132 | return uri + anchor; 133 | }; 134 | }()); 135 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.css 3 | pakmanaged.* 4 | public 5 | -------------------------------------------------------------------------------- /tests/abort-method-aborts.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('../lib') 5 | , assert = require('assert') 6 | , emitter 7 | , calledLoadStart 8 | , calledError 9 | ; 10 | 11 | emitter = request.get('http://google.com'); 12 | emitter.when(function (err, ahr, data) { 13 | assert.ok(err, 'Expected aborted error'); 14 | assert.ok(calledLoadStart, 'Expected loadstart to be called'); 15 | assert.ok(calledError, 'Expected error to be called'); 16 | }); 17 | 18 | emitter.request.on('loadstart', function () { 19 | calledLoadStart = true; 20 | assert.ok(emitter.abort, 'Expected the abort method to be exposed abstractly'); 21 | emitter.abort(); 22 | }); 23 | 24 | emitter.request.on('error', function () { 25 | calledError = true; 26 | }); 27 | }()); 28 | -------------------------------------------------------------------------------- /tests/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var suite = require('./test-suite') 5 | ; 6 | 7 | }()); 8 | -------------------------------------------------------------------------------- /tests/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -u 3 | set -e 4 | 5 | rm -rf public 6 | mkdir -p public 7 | 8 | jade *.jade 9 | mv *.html public 10 | 11 | pakmanager build 12 | rm -f pakmanaged.html 13 | mv pakmanaged.* public 14 | -------------------------------------------------------------------------------- /tests/get-content-length-of-unicode-string.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('ahr2') 5 | , assert = require('assert') 6 | , hasUnicode 7 | ; 8 | 9 | hasUnicode = { 10 | "87dbe29b-1f21-4ad5-bab2-c81c6cc044a3": { 11 | "name": "18 - If I Ain’t Got You (Live).mp3", 12 | "relativePath": "/Users/coolaj86/Downloads/Maroon 5/Hands All Over", 13 | "lastModificationDate": "2012-03-01T15:53:28.000Z", 14 | "size": 9663094 15 | } 16 | }; 17 | 18 | request.post('http://foobar3000.com/echo/echo.json', null, hasUnicode).when(function (err, ahr, data) { 19 | assert.ok(!err, 'should not have parse error error: ' + err + String(data)); 20 | assert.ok(data['87dbe29b-1f21-4ad5-bab2-c81c6cc044a3']); 21 | }); 22 | 23 | }()); 24 | -------------------------------------------------------------------------------- /tests/has-native-node-request.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('../lib') 5 | , assert = require('assert') 6 | , emitter 7 | , calledLoadStart 8 | ; 9 | 10 | emitter = request.get('http://google.com'); 11 | emitter.when(function (err, ahr, data) { 12 | //console.log(ahr); 13 | assert.ok(calledLoadStart, 'Expected loadstart to be called'); 14 | }); 15 | 16 | emitter.request.on('loadstart', function () { 17 | calledLoadStart = true; 18 | assert.ok(emitter.request, 'Expected a reference to the abstract request object on the response object'); 19 | assert.ok(emitter.request.nodeRequest, 'Expected a reference to the actual node request on the abstract one'); 20 | assert.ok(emitter.request.nodeRequest.abort, 'Expected the abort method to be exposed natively'); 21 | assert.ok(emitter.abort, 'Expected the abort method to be exposed abstractly'); 22 | }); 23 | }()); 24 | -------------------------------------------------------------------------------- /tests/index.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | script(src='pakmanaged.js') 5 | body 6 | | Hello World! 7 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "AJ ONeal (http://coolaj86.info/)", 3 | "name": "ahr-test", 4 | "description": "Tests that ahr installs from npm and pakmanager and that it runs reasonably well", 5 | "version": "2.2.2", 6 | "homepage": "http://github.com/coolaj86/abstract-http-request", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/coolaj86/abstract-http-request.git" 10 | }, 11 | "main": "app.js", 12 | "scripts": { 13 | "test": "node index.js" 14 | , "postinstall": "./deploy.sh" 15 | }, 16 | "engines": { 17 | "node": ">= 0.4.11" 18 | }, 19 | "browserDependencies": { 20 | "assert": "0.4.x" 21 | , "sequence": "2.x" 22 | , "ahr2": ">= 2.2.1" 23 | }, 24 | "dependencies": { 25 | "ahr2": ">= 2.2.1" 26 | , "sequence": "2.x" 27 | }, 28 | "devDependencies": { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/settimout-aborts.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var request = require('../lib') 5 | , assert = require('assert') 6 | , emitter 7 | , calledLoadStart 8 | , calledError 9 | ; 10 | 11 | emitter = request.get('http://google.com', null, { timeout: 1 }); 12 | emitter.when(function (err, ahr, data) { 13 | assert.ok(err, 'Expected aborted error'); 14 | assert.ok(calledLoadStart, 'Expected loadstart to be called'); 15 | assert.ok(calledError, 'Expected error to be called'); 16 | }); 17 | 18 | emitter.request.on('loadstart', function () { 19 | calledLoadStart = true; 20 | assert.ok(emitter.abort, 'Expected the abort method to be exposed abstractly'); 21 | }); 22 | 23 | emitter.request.on('error', function () { 24 | calledError = true; 25 | }); 26 | }()); 27 | -------------------------------------------------------------------------------- /tests/test-suite.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | // TODO an assert that calls next 5 | 6 | var assert = require('assert') 7 | , sequence = require('sequence')() 8 | , mockHostName = 'cors.foobar3000.com' 9 | , mockHostPort = '3273' 10 | , mockHost = mockHostName + (mockHostPort ? ':' + mockHostPort : '') 11 | , request 12 | ; 13 | 14 | try { 15 | request = require('ahr2'); 16 | console.log('testing from npm'); 17 | } catch(e) { 18 | request = require('../lib'); 19 | console.log('testing from ../lib'); 20 | } 21 | 22 | function assertDeepAlike(a, b, key) { 23 | var alike = true 24 | , res; 25 | 26 | if ('object' !== typeof a) { 27 | res = (a === b); 28 | if (!res) { 29 | console.error('dissimilar:', key, a, b); 30 | } 31 | return res; 32 | } 33 | 34 | if ('object' !== typeof b) { 35 | console.error('type mismatch'); 36 | console.info(a); 37 | console.info(b); 38 | return false; 39 | } 40 | 41 | Object.keys(a).forEach(function (key) { 42 | alike = alike && assertDeepAlike(a[key], b[key], key); 43 | }); 44 | 45 | return alike; 46 | } 47 | 48 | function hrefHost(next) { 49 | // curl "http://localhost:8000" 50 | var href = "http://" + mockHost + "/echo.json" 51 | ; 52 | 53 | request( 54 | href 55 | , function (err, ahr, data) { 56 | var mockObj 57 | ; 58 | 59 | mockObj = { 60 | "pathname": "/echo.json" 61 | , "method": "GET" 62 | , "headers": { 63 | "host": mockHost 64 | } 65 | , "trailers": {} 66 | }; 67 | 68 | assert.ok(!err); 69 | assert.ok(assertDeepAlike(mockObj, data)); 70 | 71 | next(); 72 | } 73 | ); 74 | } 75 | 76 | function hrefHostPathQuery(next) { 77 | // curl "http://localhost:8000?foo=bar&baz=qux&baz=quux&corge" 78 | var href = "http://" + mockHost + "/echo.json?foo=bar&baz=qux&baz=quux&corge" 79 | ; 80 | 81 | request( 82 | href 83 | , function (err, ahr, data) { 84 | assert.ok(assertDeepAlike({ 85 | "query": { 86 | "foo": "bar" 87 | , "baz": [ 88 | "qux" 89 | , "quux" 90 | ] 91 | , "corge": "" 92 | } 93 | , "pathname": "/echo.json" 94 | , "method": "GET" 95 | , "headers": { 96 | "host": mockHost 97 | } 98 | }, data)); 99 | next(); 100 | } 101 | ); 102 | } 103 | 104 | function paramsHrefBody(next) { 105 | // curl "http://localhost:8000?foo=bar&baz=qux&baz=quux&corge" -d '' 106 | var href = "http://" + mockHost + "/echo.json?foo=bar&baz=qux&baz=quux&corge" 107 | , body = { 108 | "grault": "garply" 109 | , "waldo": [ 110 | "fred" 111 | , "plug" 112 | , "xyzzy" 113 | ] 114 | , "thud": "" 115 | } 116 | ; 117 | 118 | request( 119 | { 120 | href: href 121 | , body: body 122 | } 123 | , function (err, ahr, data) { 124 | var mockObj 125 | ; 126 | 127 | mockObj = { 128 | "query": { 129 | "foo": "bar" 130 | , "baz": [ 131 | "qux" 132 | , "quux" 133 | ] 134 | , "corge": "" 135 | } 136 | , "body": { 137 | "grault": "garply" 138 | , "waldo": [ 139 | "fred" 140 | , "plug" 141 | , "xyzzy" 142 | ] 143 | , "thud": "" 144 | } 145 | , "pathname": "/echo.json" 146 | , "method": "POST" 147 | , "headers": { 148 | "host": mockHost 149 | , "content-type": "application/json" 150 | //, "content-type": "application/x-www-form-urlencoded" 151 | } 152 | }; 153 | 154 | assert.ok(assertDeepAlike(mockObj, data)); 155 | next(); 156 | } 157 | ); 158 | } 159 | 160 | // curl "http://localhost:8000?foo=bar&baz=qux&baz=quux&corge" -d 'blahblah=yada&yada=blah' 161 | function paramsFull(next) { 162 | // curl "http://localhost:8000/doesntexist?foo=bar&baz=qux&baz=quux&corge" 163 | var params = { 164 | //href: "http://localhost:8000/doesntexist?foo=bar&baz=qux&baz=quux&corge" 165 | /* 166 | protocol, hostname, and port can be taken from location.js 167 | */ 168 | protocol: "http:" 169 | //, host: "localhost:8000" 170 | , hostname: mockHostName 171 | , port: mockHostPort 172 | , pathname: "/doesntexist" 173 | , query: { 174 | "foo": "bar" 175 | , "baz": [ 176 | "qux" 177 | , "quux" 178 | ] 179 | , "corge": "" 180 | } 181 | } 182 | ; 183 | 184 | request( 185 | params 186 | , function (err, ahr, data) { 187 | var mockObj 188 | ; 189 | 190 | mockObj = { 191 | "query": { 192 | "foo": "bar" 193 | , "baz": [ 194 | "qux" 195 | , "quux" 196 | ] 197 | , "corge": "" 198 | } 199 | , "pathname": "/doesntexist" 200 | , "method": "GET" 201 | , "headers": { 202 | "host": mockHost 203 | } 204 | }; 205 | 206 | assert.ok(assertDeepAlike(mockObj, data)); 207 | next(); 208 | } 209 | ); 210 | } 211 | 212 | function paramsPartial(next) { 213 | } 214 | 215 | 216 | sequence 217 | .then(hrefHost) 218 | .then(hrefHostPathQuery) 219 | .then(paramsHrefBody) 220 | .then(paramsFull) 221 | //.then(paramsPartial) 222 | // TODO merge href / query 223 | //.then(paramsHrefQueryMerge) 224 | .then(function () { 225 | console.info('[PASS] all tests passed'); 226 | }); 227 | ; 228 | 229 | }()); 230 | --------------------------------------------------------------------------------