UNPKG

12.5 kBJavaScriptView Raw
1/**
2 * There is an original fo this file at dweb-transports.httptools
3 * and a duplicate at dweb-archivecontroller which is only there while DwebTransports is being made more usable
4 */
5const nodefetch = require('node-fetch'); // Note, were using node-fetch-npm which had a warning in webpack see https://github.com/bitinn/node-fetch/issues/421 and is intended for clients
6const errors = require('./Errors'); // Standard Dweb Errors
7const TransportError = errors.TransportError;
8const debug = require('debug')('dweb-transports:httptools');
9const queue = require('async/queue');
10
11
12//var fetch,Headers,Request;
13//if (typeof(Window) === "undefined") {
14if (typeof(fetch) === "undefined") {
15 //var fetch = require('whatwg-fetch').fetch; //Not as good as node-fetch-npm, but might be the polyfill needed for browser.safari
16 //XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest; // Note this doesnt work if set to a var or const, needed by whatwg-fetch
17 fetch = nodefetch;
18 Headers = fetch.Headers; // A class
19 Request = fetch.Request; // A class
20} /* else {
21 // If on a browser, need to find fetch,Headers,Request in window
22 console.log("Loading browser version of fetch,Headers,Request");
23 fetch = window.fetch;
24 Headers = window.Headers;
25 Request = window.Request;
26} */
27//TODO-HTTP to work on Safari or mobile will require a polyfill, see https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch for comment
28
29
30httptools = {};
31let httpTaskQueue;
32
33function queueSetup({concurrency}) {
34 httpTaskQueue = queue((task, cb) => {
35 if (task.loopguard === ((typeof window != "undefined") && window.loopguard)) {
36 fetch(task.req)
37 .then(res => {
38 debug("Fetch of %s completed", task.what);
39 httpTaskQueue.concurrency = Math.min(httpTaskQueue.concurrency+1, httpTaskQueue.running()+6);
40 //debug("Raising concurrency to %s", httpTaskQueue.concurrency);
41 cb(null); // This is telling the queue that we are done
42 task.cb(null, res); // This is the caller of the task
43 })
44 .catch(err => {
45 // Adjust concurrency, dont go below running number (which is running-1 because this failed task counts)
46 // and we know browser doesnt complain below 6
47 httpTaskQueue.concurrency = Math.max(httpTaskQueue.concurrency-1, 6, httpTaskQueue.running()-1);
48 //debug("Dropping concurrency to %s", httpTaskQueue.concurrency);
49 cb(err); // Tell queue done with an error
50 if (--task.count > 0) {
51 debug("Retrying fetch of %s in %s ms: %s", task.what, task.ms, err.message);
52 httpTaskQueue.push(task);
53 /* Alternative with timeouts - not needed
54 let timeout = task.ms;
55 task.ms = Math.floor(task.ms*(1+Math.random())); // Spread out delays incase all requesting same time
56 setTimeout(() => { httpTaskQueue.push(task);}, timeout);
57 */
58 } else {
59 debug("Requeued fetch of %s failed: %s", task.what, err.message);
60 task.cb(err);
61 }
62 });
63 } else {
64 err = new Error(`Dropping fetch of ${task.what} as window changed from ${task.loopguard} to ${window.loopguard}`)
65 debug("Dropping fetch of %s as window changed from %s to %s", task.what, task.loopguard, window.loopguard);
66 task.cb(err); // Tell caller it failed
67 cb(err); // Tell queue it failed
68 }
69 }, concurrency)
70}
71queueSetup({concurrency: 6});
72
73function queuedFetch(req, ms, count, what) {
74 return new Promise((resolve, reject) => {
75 count = count || 1; // 0 means 1
76 httpTaskQueue.push({
77 req, count, ms, what,
78 loopguard: (typeof window != "undefined") && window.loopguard, // Optional global parameter, will cancel any loops if changes
79 cb: (err, res) => {
80 if(err) { reject(err); } else {resolve(res); }
81 },
82 });
83 });
84}
85
86async function loopfetch(req, ms, count, what) {
87 /*
88 A workaround for a nasty Chrome issue which fails if there is a (cross-origin?) fetch of more than 6 files. See other WORKAROUND-CHROME-CROSSORIGINFETCH
89 Loops at longer and longer intervals trying
90 req: Request
91 ms: Initial wait between polls
92 count: Max number of times to try (0 means just once)
93 what: Name of what retrieving for log (usually file name or URL)
94 returns Response:
95 */
96 let lasterr;
97 let loopguard = (typeof window != "undefined") && window.loopguard; // Optional global parameter, will cancel any loops if changes
98 count = count || 1; // count of 0 actually means 1
99 while (count-- && (loopguard === ((typeof window != "undefined") && window.loopguard)) ) {
100 try {
101 return await fetch(req);
102 } catch(err) {
103 lasterr = err;
104 debug("Delaying %s by %d ms because %s", what, ms, err.message);
105 await new Promise(resolve => {setTimeout(() => { resolve(); },ms)})
106 ms = Math.floor(ms*(1+Math.random())); // Spread out delays incase all requesting same time
107 }
108 }
109 console.warn("loopfetch of",what,"failed");
110 if (loopguard !== ((typeof window != "undefined") && window.loopguard)) {
111 debug("Looping exited because of page change %s", what);
112 throw new Error("Looping exited because of page change "+ what)
113 } else {
114 throw(lasterr);
115 }
116}
117
118/**
119 * Fetch a url
120 *
121 * @param httpurl string URL.href i.e. http:... or https:...
122 * @param init {headers}
123 * @param wantstream BOOL True if want to return a stream (otherwise buffer)
124 * @param retries INT Number of times to retry if underlying OS call fails (eg. "INSUFFICIENT RESOURCES") (wont retry on 404 etc)
125 * @returns {Promise<*>} Data as text, or json as object or stream depending on Content-Type header adn wantstream
126 * @throws TransportError if fails to fetch
127 */
128httptools.p_httpfetch = async function(httpurl, init, {wantstream=false, retries=undefined}={}) { // Embrace and extend "fetch" to check result etc.
129 try {
130 // THis was get("range") but that works when init.headers is a Headers, but not when its an object
131 debug("p_httpfetch: %s %o", httpurl, init.headers.range || "" );
132 //console.log('CTX=',init["headers"].get('Content-Type'))
133 // Using window.fetch, because it doesn't appear to be in scope otherwise in the browser.
134 let req = new Request(httpurl, init);
135
136 // EITHER Use queuedFetch if have async/queue
137 let response = await queuedFetch(req, 500, retries, httpurl);
138 // OR Use loopfetch if dont have async/queue and hitting browser Insufficient resources
139 //let response = await loopfetch(req, 500, retries, httpurl);
140 // OR use fetch for simplicity
141 //let response = await fetch(req);
142
143 // fetch throws (on Chrome, untested on Firefox or Node) TypeError: Failed to fetch)
144 // Note response.body gets a stream and response.blob gets a blob and response.arrayBuffer gets a buffer.
145 if (response.ok) {
146 let contenttype = response.headers.get('Content-Type');
147 if (wantstream) {
148 return response.body; // Note property while json() or text() are functions
149 } else if ((typeof contenttype !== "undefined") && contenttype.startsWith("application/json")) {
150 return response.json(); // promise resolving to JSON
151 } else if ((typeof contenttype !== "undefined") && contenttype.startsWith("text")) { // Note in particular this is used for responses to store
152 return response.text();
153 } else { // Typically application/octetStream when don't know what fetching
154 return new Buffer(await response.arrayBuffer()); // Convert arrayBuffer to Buffer which is much more usable currently
155 }
156 }
157 // noinspection ExceptionCaughtLocallyJS
158 throw new TransportError(`Transport Error ${httpurl} ${response.status}: ${response.statusText}`);
159 } catch (err) {
160 // Error here is particularly unhelpful - if rejected during the COrs process it throws a TypeError
161 debug("p_httpfetch failed: %s", err.message); // note TypeErrors are generated by CORS or the Chrome anti DDOS 'feature' should catch them here and comment
162 if (err instanceof TransportError) {
163 throw err;
164 } else {
165 throw new TransportError(`Transport error thrown by ${httpurl}: ${err.message}`);
166 }
167 }
168}
169
170/**
171 *
172 * @param httpurl STRING|Url
173 * @param opts
174 * opts {
175 * start, end, // Range of bytes wanted - inclusive i.e. 0,1023 is 1024 bytes
176 * wantstream, // Return a stream rather than data
177 * retries=12, // How many times to retry
178 * noCache // Add Cache-Control: no-cache header
179 * }
180 * @param cb f(err, res) // See p_httpfetch for resul
181 * @returns {Promise<*>} // If no cb.
182 */
183httptools.p_GET = function(httpurl, opts={}, cb) {
184 /* Locate and return a block, based on its url
185 Throws TransportError if fails
186 opts {
187 start, end, // Range of bytes wanted - inclusive i.e. 0,1023 is 1024 bytes
188 wantstream, // Return a stream rather than data
189 retries=12, // How many times to retry
190 noCache // Add Cache-Control: no-cache header
191 }
192 returns result via promise or cb(err, result)
193 */
194 if (typeof httpurl !== "string") httpurl = httpurl.href; // Assume its a URL as no way to use "instanceof" on URL across node/browser
195 let headers = new Headers();
196 if (opts.start || opts.end) headers.append("range", `bytes=${opts.start || 0}-${(opts.end<Infinity) ? opts.end : ""}`);
197 //if (opts.noCache) headers.append("Cache-Control", "no-cache"); It complains about preflight with no-cache
198 const retries = typeof opts.retries === "undefined" ? 12 : opts.retries;
199 let init = { //https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
200 method: 'GET',
201 headers: headers,
202 mode: 'cors',
203 cache: opts.noCache ? 'no-cache' : 'default', // In Chrome, This will set Cache-Control: max-age=0
204 redirect: 'follow', // Chrome defaults to manual
205 keepalive: true // Keep alive - mostly we'll be going back to same places a lot
206 };
207 const prom = httptools.p_httpfetch(httpurl, init, {retries, wantstream: opts.wantstream}); // This s a real http url
208 if (cb) { prom.then((res)=>{ try { cb(null,res)} catch(err) { debug("Uncaught error %O",err)}}).catch((err) => cb(err)); } else { return prom; } // Unpromisify pattern v5
209}
210httptools.p_POST = function(httpurl, opts={}, cb) {
211 /* Locate and return a block, based on its url
212 opts = { data, contenttype, retries }
213 returns result via promise or cb(err, result)
214 */
215 // Throws TransportError if fails
216 //let headers = new window.Headers();
217 //headers.set('content-type',type); Doesn't work, it ignores it
218 if (typeof opts === "function") { cb = opts; opts = {}; }
219 if (typeof httpurl !== "string") httpurl = httpurl.href; // Assume its a URL as no way to use "instanceof" on URL across node/browser
220 const retries = typeof opts.retries === "undefined" ? 0 : opts.retries;
221 let init = {
222 //https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
223 //https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name for headers tat cant be set
224 method: 'POST',
225 headers: {}, //headers,
226 //body: new Buffer(data),
227 body: opts.data,
228 mode: 'cors',
229 cache: 'default',
230 redirect: 'follow', // Chrome defaults to manual
231 keepalive: false // Keep alive - mostly we'll be going back to same places a lot
232 };
233 if (opts.contenttype) init.headers["Content-Type"] = opts.contenttype;
234 const prom = httptools.p_httpfetch(httpurl, init, {retries});
235 if (cb) { prom.then((res)=>cb(null,res)).catch((err) => cb(err)); } else { return prom; } // Unpromisify pattern v3
236}
237
238
239exports = module.exports = httptools;