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 | */
|
5 | const 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
|
6 | const errors = require('./Errors'); // Standard Dweb Errors
|
7 | const TransportError = errors.TransportError;
|
8 | const debug = require('debug')('dweb-transports:httptools');
|
9 | const queue = require('async/queue');
|
10 |
|
11 |
|
12 | //var fetch,Headers,Request;
|
13 | //if (typeof(Window) === "undefined") {
|
14 | if (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 |
|
30 | httptools = {};
|
31 | let httpTaskQueue;
|
32 |
|
33 | function 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 | }
|
71 | queueSetup({concurrency: 6});
|
72 |
|
73 | function 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 |
|
86 | async 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 | */
|
128 | httptools.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 | */
|
183 | httptools.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 | }
|
210 | httptools.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 |
|
239 | exports = module.exports = httptools;
|