cloudworker-proxy
Version:
An api gateway for cloudflare workers
1,686 lines (1,460 loc) • 41.8 kB
JavaScript
import { ReadableStream, TransformStream } from '@mattiasbuelens/web-streams-polyfill/ponyfill/es6';
import Stream, { PassThrough } from 'stream';
import Busboy from 'busboy';
import FormData from 'formdata-node';
import http, { STATUS_CODES } from 'http';
import { format, parse, resolve } from 'url';
import https from 'https';
import zlib from 'zlib';
// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js
// (MIT licensed)
const BUFFER = Symbol('buffer');
const TYPE = Symbol('type');
class Blob {
constructor() {
this[TYPE] = '';
const blobParts = arguments[0];
const options = arguments[1];
const buffers = [];
if (blobParts) {
const a = blobParts;
const length = Number(a.length);
for (let i = 0; i < length; i++) {
const element = a[i];
let buffer;
if (element instanceof Buffer) {
buffer = element;
} else if (ArrayBuffer.isView(element)) {
buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
} else if (element instanceof ArrayBuffer) {
buffer = Buffer.from(element);
} else if (element instanceof Blob) {
buffer = element[BUFFER];
} else {
buffer = Buffer.from(typeof element === 'string' ? element : String(element));
}
buffers.push(buffer);
}
}
this[BUFFER] = Buffer.concat(buffers);
let type = options && options.type !== undefined && String(options.type).toLowerCase();
if (type && !/[^\u0020-\u007E]/.test(type)) {
this[TYPE] = type;
}
}
get size() {
return this[BUFFER].length;
}
get type() {
return this[TYPE];
}
slice() {
const size = this.size;
const start = arguments[0];
const end = arguments[1];
let relativeStart, relativeEnd;
if (start === undefined) {
relativeStart = 0;
} else if (start < 0) {
relativeStart = Math.max(size + start, 0);
} else {
relativeStart = Math.min(start, size);
}
if (end === undefined) {
relativeEnd = size;
} else if (end < 0) {
relativeEnd = Math.max(size + end, 0);
} else {
relativeEnd = Math.min(end, size);
}
const span = Math.max(relativeEnd - relativeStart, 0);
const buffer = this[BUFFER];
const slicedBuffer = buffer.slice(relativeStart, relativeStart + span);
const blob = new Blob([], { type: arguments[2] });
blob[BUFFER] = slicedBuffer;
return blob;
}
}
Object.defineProperties(Blob.prototype, {
size: { enumerable: true },
type: { enumerable: true },
slice: { enumerable: true }
});
Object.defineProperty(Blob.prototype, Symbol.toStringTag, {
value: 'Blob',
writable: false,
enumerable: false,
configurable: true
});
/**
* fetch-error.js
*
* FetchError interface for operational errors
*/
/**
* Create FetchError instance
*
* @param String message Error message for human
* @param String type Error type for machine
* @param String systemError For Node.js system error
* @return FetchError
*/
function FetchError(message, type, systemError) {
Error.call(this, message);
this.message = message;
this.type = type;
// when err.type is `system`, err.code contains system error code
if (systemError) {
this.code = this.errno = systemError.code;
}
// hide custom error implementation details from end-users
Error.captureStackTrace(this, this.constructor);
}
FetchError.prototype = Object.create(Error.prototype);
FetchError.prototype.constructor = FetchError;
FetchError.prototype.name = 'FetchError';
let convert;
try {
convert = require('encoding').convert;
} catch (e) {}
const INTERNALS = Symbol('Body internals');
function getTypeOfBody(body) {
if (body == null) {
return "null";
} else if (typeof body === 'string') {
return "String";
} else if (isURLSearchParams(body)) {
return "URLSearchParams";
} else if (body instanceof Blob) {
return "Blob";
} else if (Buffer.isBuffer(body)) {
return "Buffer";
} else if (Object.prototype.toString.call(body) === '[object ArrayBuffer]') {
return "ArrayBuffer";
} else if (ArrayBuffer.isView(body)) {
return "ArrayBufferView";
} else if (body.toString() === '[object FormData]' || Object.prototype.toString.call(body) === '[object FormData]') {
return "FormData";
} else if (body instanceof Stream) {
return "Stream";
} else if (body instanceof ReadableStream ||
// Allow detecting a "ReadableStream" from a different install of web-streams-polyfill
body.constructor.name === "ReadableStream" && typeof body.getReader == "function") {
return "ReadableStream";
} else {
return "other";
}
}
function readableNodeToWeb(nodeStream, instance) {
return new ReadableStream({
start(controller) {
nodeStream.pause();
nodeStream.on('data', function (chunk) {
// TODO: Should we only do Buffer.from() if chunk is a UInt8Array?
// Potentially it makes more sense for down-stream consumers of fetch to cast to Buffer, instead?
// if(isUInt8Array(chunk)) {
controller.enqueue(new Uint8Array(Buffer.from(chunk)));
// HELP WANTED: The node-web-streams package pauses the nodeStream here, however,
// if we do that, then it gets permanently paused. Why?
// nodeStream.pause();
});
nodeStream.on('end', function () {
controller.close();
const pending = controller.byobRequest;
if (pending) {
pending.respond(0);
}
});
nodeStream.on('error', function (err) {
controller.error(new FetchError(`Invalid response body while trying to fetch ${instance.url}: ${err.message}`, 'system', err));
});
},
pull(controller) {
nodeStream.resume();
},
cancel(reason) {
nodeStream.pause();
},
type: "bytes"
});
}
function createReadableStream(instance) {
const body = getInstanceBody(instance);
const bodyType = getTypeOfBody(body);
if (bodyType === "null") {
return null;
}
if (bodyType === 'ReadableStream') {
return body.pipeThrough(new TransformStream({
transform(chunk, controller) {
// TODO: Should we only do Buffer.from() if chunk is a UInt8Array?
// Potentially it makes more sense for down-stream consumers of fetch to cast to Buffer, instead?
// if(isUInt8Array(chunk)) {
controller.enqueue(new Uint8Array(Buffer.from(chunk)));
}
}));
}
if (bodyType === "Stream") {
body.pause();
return readableNodeToWeb(body, instance);
}
const readable = new ReadableStream({
start(controller) {
switch (bodyType) {
case "String":
// body is a string:
controller.enqueue(new Uint8Array(Buffer.from(body)));
controller.close();
break;
case "URLSearchParams":
// body is a URLSearchParams
controller.enqueue(new Uint8Array(Buffer.from(body.toString())));
controller.close();
break;
case "Blob":
// body is blob
controller.enqueue(new Uint8Array(Buffer.from(body[BUFFER])));
controller.close();
break;
case "Buffer":
// body is Buffer
controller.enqueue(new Uint8Array(Buffer.from(body)));
controller.close();
break;
case "ArrayBuffer":
// body is ArrayBuffer
controller.enqueue(new Uint8Array(Buffer.from(body)));
controller.close();
break;
case "ArrayBufferView":
// body is ArrayBufferView
controller.enqueue(new Uint8Array(Buffer.from(body.buffer)));
controller.close();
break;
case "FormData":
controller.enqueue(new Uint8Array(Buffer.from(body.toString())));
controller.close();
break;
case "other":
controller.enqueue(new Uint8Array(Buffer.from(String(body))));
controller.close();
break;
default:
throw new Error("createReadableStream received an instance body that getTypeOfBody could not understand");
}
},
type: "bytes"
});
return readable;
}
/**
* Body mixin
*
* Ref: https://fetch.spec.whatwg.org/#body
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
function Body(body) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$size = _ref.size;
let size = _ref$size === undefined ? 0 : _ref$size;
var _ref$timeout = _ref.timeout;
let timeout = _ref$timeout === undefined ? 0 : _ref$timeout;
var _ref$name = _ref.name;
let name = _ref$name === undefined ? "Body" : _ref$name;
this.size = size;
this.timeout = timeout;
this[INTERNALS] = {
body: body,
readableStream: null,
disturbed: false,
name
};
const bodyType = getTypeOfBody(body);
if (bodyType !== 'ReadableStream') {
this[INTERNALS].readableStream = createReadableStream(this);
} else {
this[INTERNALS].readableStream = body;
}
}
Body.prototype = {
// NOTE: Firefox and Chrome return `undefined` if initial body is undefined, when looking at Request.body, they always return undefined.
get body() {
return getInstanceReadableStream(this);
},
get bodyUsed() {
return this[INTERNALS].disturbed;
},
/**
* Decode response as ArrayBuffer
*
* @return Promise
*/
arrayBuffer() {
return consumeBody.call(this).then(function (buf) {
var ab = new ArrayBuffer(buf.length);
var view = new Uint8Array(ab);
for (var i = 0; i < buf.length; ++i) {
view[i] = buf[i];
}
return ab;
});
},
/**
* Return raw response as Blob
*
* @return Promise
*/
blob() {
let ct = this.headers && this.headers.get('content-type') || '';
return consumeBody.call(this).then(function (buf) {
return Object.assign(
// Prevent copying
new Blob([], {
type: ct.toLowerCase()
}), {
[BUFFER]: buf
});
});
},
/**
* Decode response as json
*
* @return Promise
*/
json() {
var _this = this;
return consumeBody.call(this).then(function (buffer) {
try {
return JSON.parse(buffer.toString());
} catch (err) {
return Promise.reject(new FetchError(`invalid json response body at ${_this.url} reason: ${err.message}`, 'invalid-json'));
}
});
},
/**
* Decode response as text
*
* @return Promise
*/
text() {
return consumeBody.call(this).then(function (buffer) {
return buffer.toString();
});
},
/**
* Decode response as buffer (non-spec api)
*
* @return Promise
*/
buffer() {
return consumeBody.call(this);
},
/**
* Decode response as text, while automatically detecting the encoding and
* trying to decode to UTF-8 (non-spec api)
*
* @return Promise
*/
textConverted() {
var _this2 = this;
return consumeBody.call(this).then(function (buffer) {
return convertBody(buffer, _this2.headers);
});
},
formData() {
var _this3 = this;
return consumeBody.call(this).then(function (buffer) {
return new Promise(function (resolve$$1, reject) {
var formdata = new FormData();
var busboy = new Busboy({ headers: {
'content-type': _this3.headers.get('content-type')
} });
busboy.on('field', function (fieldname, val) {
return formdata.append(fieldname, val);
});
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
let val = "";
file.on('data', function (data) {
return val += data;
});
file.on('end', function () {
return formdata.append(fieldname, val, filename);
});
});
busboy.on('finish', function () {
return resolve$$1(formdata);
});
writeToStream(busboy, _this3);
});
});
}
};
// In browsers, all properties are enumerable.
Object.defineProperties(Body.prototype, {
body: { enumerable: true },
bodyUsed: { enumerable: true },
arrayBuffer: { enumerable: true },
blob: { enumerable: true },
json: { enumerable: true },
text: { enumerable: true }
});
Body.mixIn = function (proto) {
for (const name of Object.getOwnPropertyNames(Body.prototype)) {
// istanbul ignore else: future proof
if (!(name in proto)) {
const desc = Object.getOwnPropertyDescriptor(Body.prototype, name);
Object.defineProperty(proto, name, desc);
}
}
};
/**
* Consume and convert an entire Body to a Buffer.
*
* Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body
*
* @return Promise
*/
function consumeBody() {
const instance = this;
if (instance[INTERNALS].disturbed) {
return Promise.reject(new TypeError(`body used already for: ${instance.url}`));
}
instance[INTERNALS].disturbed = true;
let resTimeout;
const promise = new Promise(function (resolve$$1, reject) {
const readable = getInstanceReadableStream(instance);
if (readable == null) {
return resolve$$1(Buffer.alloc(0));
}
const reader = readable.getReader();
let timedOut = false;
// allow timeout on slow response body
if (instance.timeout) {
resTimeout = setTimeout(function () {
timedOut = true;
reject(new FetchError(`Response timeout while trying to fetch ${instance.url} (over ${instance.timeout}ms)`, 'body-timeout'));
}, instance.timeout);
}
let buffers = [];
let totalBytes = 0;
function push() {
reader.read().then(function (read) {
let bufferedData;
let chunk;
if (timedOut) {
return;
}
if (read.done) {
try {
bufferedData = Buffer.concat(buffers, totalBytes);
} catch (err) {
// handle streams that have accumulated too much data (issue #414)
reject(new FetchError(`Could not create Buffer from response body for ${instance.url}: ${err.message}`, 'system', err));
return;
}
resolve$$1(bufferedData);
return;
}
chunk = Buffer.from(read.value);
if (instance.size && totalBytes + chunk.length > instance.size) {
reject(new FetchError(`content size at ${instance.url} over limit: ${instance.size}`, 'max-size'));
return;
}
buffers.push(chunk);
totalBytes += chunk.length;
push();
}, function (err) {
reject(err);
});
}
push();
});
promise.then(function () {
return resTimeout && clearTimeout(resTimeout);
}, function () {
return resTimeout && clearTimeout(resTimeout);
});
return promise;
}
/**
* Detect buffer encoding and convert to target encoding
* ref: http://www.w3.org/TR/2011/WD-html5-20110113/parsing.html#determining-the-character-encoding
*
* @param Buffer buffer Incoming buffer
* @param String encoding Target encoding
* @return String
*/
function convertBody(buffer, headers) {
if (typeof convert !== 'function') {
throw new Error('The package `encoding` must be installed to use the textConverted() function');
}
const ct = headers.get('content-type');
let charset = 'utf-8';
let res, str;
// header
if (ct) {
res = /charset=([^;]*)/i.exec(ct);
}
// no charset in content type, peek at response body for at most 1024 bytes
str = buffer.slice(0, 1024).toString();
// html5
if (!res && str) {
res = /<meta.+?charset=(['"])(.+?)\1/i.exec(str);
}
// html4
if (!res && str) {
res = /<meta[\s]+?http-equiv=(['"])content-type\1[\s]+?content=(['"])(.+?)\2/i.exec(str);
if (res) {
res = /charset=(.*)/i.exec(res.pop());
}
}
// xml
if (!res && str) {
res = /<\?xml.+?encoding=(['"])(.+?)\1/i.exec(str);
}
// found charset
if (res) {
charset = res.pop();
// prevent decode issues when sites use incorrect encoding
// ref: https://hsivonen.fi/encoding-menu/
if (charset === 'gb2312' || charset === 'gbk') {
charset = 'gb18030';
}
}
// turn raw buffers into a single utf-8 buffer
return convert(buffer, 'UTF-8', charset).toString();
}
/**
* Detect a URLSearchParams object
* ref: https://github.com/bitinn/node-fetch/issues/296#issuecomment-307598143
*
* @param Object obj Object to detect by type or brand
* @return String
*/
function isURLSearchParams(obj) {
// Duck-typing as a necessary condition.
if (typeof obj !== 'object' || typeof obj.append !== 'function' || typeof obj.delete !== 'function' || typeof obj.get !== 'function' || typeof obj.getAll !== 'function' || typeof obj.has !== 'function' || typeof obj.set !== 'function') {
return false;
}
// Brand-checking and more duck-typing as optional condition.
return obj.constructor.name === 'URLSearchParams' || Object.prototype.toString.call(obj) === '[object URLSearchParams]' || typeof obj.sort === 'function';
}
/**
* Clone body given Res/Req instance
*
* @param Mixed instance Response or Request instance
* @return Mixed
*/
function cloneBody(instance) {
const body = getInstanceBody(instance);
const bodyType = getTypeOfBody(body);
// don't allow cloning a used body
if (instance.bodyUsed) {
throw new Error('cannot clone body after it is used');
}
// check that body is a stream and not form-data object
// note: we can't clone the form-data object without having it as a dependency
if (bodyType === "Stream") {
// tee instance body
let p1 = new PassThrough();
let p2 = new PassThrough();
// set instance body to teed body and return the other teed body
instance[INTERNALS].body = p1;
instance[INTERNALS].readableStream = createReadableStream(instance);
body.pipe(p1);
body.pipe(p2);
// body.resume();
return p2;
} else if (bodyType === "ReadableStream") {
var _body$tee = body.tee();
let p1 = _body$tee[0],
p2 = _body$tee[1];
// set instance body to teed body and return the other teed body
instance[INTERNALS].body = p1;
instance[INTERNALS].readableStream = p1;
return p2;
}
// Note the early returns
return body;
}
/**
* Performs the operation "extract a `Content-Type` value from |object|" as
* specified in the specification:
* https://fetch.spec.whatwg.org/#concept-bodyinit-extract
*
* This function assumes that instance.body is present.
*
* @param Mixed instance Response or Request instance
*/
function extractContentType(instance) {
const body = getInstanceBody(instance);
const bodyType = getTypeOfBody(body);
switch (bodyType) {
case "String":
case "other":
return 'text/plain;charset=UTF-8';
case "URLSearchParams":
return 'application/x-www-form-urlencoded;charset=UTF-8';
case "Blob":
return body.type || null;
case "FormData":
return `multipart/form-data;boundary=${body.getBoundary()}`;
default:
return null;
}
}
/**
* The Fetch Standard treats this as if "total bytes" is a property on the body.
* For us, we have to explicitly get it with a function.
*
* ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes
*
* @param Body instance Instance of Body
* @return Number? Number of bytes, or null if not possible
*/
function getTotalBytes(instance) {
const body = getInstanceBody(instance);
const bodyType = getTypeOfBody(body);
switch (bodyType) {
case "null":
return 0;
case "String":
return Buffer.byteLength(body);
case "URLSearchParams":
case "other":
return Buffer.byteLength(String(body));
case "Blob":
return body.size;
case "Buffer":
return body.length;
case "ArrayBuffer":
case "ArrayBufferView":
return body.byteLength;
case "FormData":
if (body._lengthRetrievers && body._lengthRetrievers.length == 0 || // 1.x
body.hasKnownLength && body.hasKnownLength()) {
// 2.x
return body.getLengthSync();
}
default:
return null;
}
}
/**
* Write a Body to a Node.j (e.g. http.Request) object.
*
* @param Body instance Instance of Body
* @return Void
*/
function writeToStream(dest, instance) {
const body = getInstanceBody(instance);
const bodyType = getTypeOfBody(body);
switch (bodyType) {
case "null":
dest.end();
break;
case "Stream":
body.pipe(dest);
break;
case "ReadableStream":
var _body$tee2 = body.tee();
const out1 = _body$tee2[0],
out2 = _body$tee2[1];
const reader = out2.getReader();
function push() {
reader.read().then(function (read) {
if (read.done) {
dest.end();
return;
}
// TODO: Should we only do Buffer.from() if chunk is a UInt8Array?
// if(isUInt8Array(chunk)) {
dest.write(Buffer.from(read.value));
push();
});
}
instance[INTERNALS].body = out1;
push();
break;
case "String":
dest.write(body);
dest.end();
break;
// case "URLSearchParams":
// dest.write(body.toString());
// dest.end();
// break;
case "Blob":
dest.write(body[BUFFER]);
dest.end();
break;
case "Buffer":
dest.write(body);
dest.end();
break;
case "ArrayBuffer":
dest.write(Buffer.from(body));
dest.end();
break;
case "ArrayBufferView":
dest.write(Buffer.from(body.buffer, body.byteOffset, body.byteLength));
dest.end();
break;
case "FormData":
body.pipe(dest);
break;
default:
dest.write(String(body));
dest.end();
break;
}
}
function getInstanceName(instance) {
return instance[INTERNALS].name;
}
function getInstanceBody(instance) {
return instance[INTERNALS].body;
}
function getInstanceReadableStream(instance) {
return instance[INTERNALS].readableStream;
}
/**
* headers.js
*
* Headers class offers convenient helpers
*/
const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/;
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
function validateName(name) {
name = `${name}`;
if (invalidTokenRegex.test(name)) {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
}
function validateValue(value) {
value = `${value}`;
if (invalidHeaderCharRegex.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
}
/**
* Find the key in the map object given a header name.
*
* Returns undefined if not found.
*
* @param String name Header name
* @return String|Undefined
*/
function find(map, name) {
name = name.toLowerCase();
for (const key in map) {
if (key.toLowerCase() === name) {
return key;
}
}
return undefined;
}
const MAP = Symbol('map');
class Headers {
/**
* Headers class
*
* @param Object headers Response headers
* @return Void
*/
constructor() {
let init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
this[MAP] = Object.create(null);
if (init instanceof Headers) {
const rawHeaders = init.raw();
const headerNames = Object.keys(rawHeaders);
for (const headerName of headerNames) {
for (const value of rawHeaders[headerName]) {
this.append(headerName, value);
}
}
return;
}
// We don't worry about converting prop to ByteString here as append()
// will handle it.
if (init == null) ; else if (typeof init === 'object') {
const method = init[Symbol.iterator];
if (method != null) {
if (typeof method !== 'function') {
throw new TypeError('Header pairs must be iterable');
}
// sequence<sequence<ByteString>>
// Note: per spec we have to first exhaust the lists then process them
const pairs = [];
for (const pair of init) {
if (typeof pair !== 'object' || typeof pair[Symbol.iterator] !== 'function') {
throw new TypeError('Each header pair must be iterable');
}
pairs.push(Array.from(pair));
}
for (const pair of pairs) {
if (pair.length !== 2) {
throw new TypeError('Each header pair must be a name/value tuple');
}
this.append(pair[0], pair[1]);
}
} else {
// record<ByteString, ByteString>
for (const key of Object.keys(init)) {
const value = init[key];
this.append(key, value);
}
}
} else {
throw new TypeError('Provided initializer must be an object');
}
}
/**
* Return combined header value given name
*
* @param String name Header name
* @return Mixed
*/
get(name) {
name = `${name}`;
validateName(name);
const key = find(this[MAP], name);
if (key === undefined) {
return null;
}
return this[MAP][key].join(', ');
}
/**
* Iterate over all headers
*
* @param Function callback Executed for each item with parameters (value, name, thisArg)
* @param Boolean thisArg `this` context for callback function
* @return Void
*/
forEach(callback) {
let thisArg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined;
let pairs = getHeaders(this);
let i = 0;
while (i < pairs.length) {
var _pairs$i = pairs[i];
const name = _pairs$i[0],
value = _pairs$i[1];
callback.call(thisArg, value, name, this);
pairs = getHeaders(this);
i++;
}
}
/**
* Overwrite header values given name
*
* @param String name Header name
* @param String value Header value
* @return Void
*/
set(name, value) {
name = `${name}`;
value = `${value}`;
validateName(name);
validateValue(value);
const key = find(this[MAP], name);
this[MAP][key !== undefined ? key : name] = [value];
}
/**
* Append a value onto existing header
*
* @param String name Header name
* @param String value Header value
* @return Void
*/
append(name, value) {
name = `${name}`;
value = `${value}`;
validateName(name);
validateValue(value);
const key = find(this[MAP], name);
if (key !== undefined) {
this[MAP][key].push(value);
} else {
this[MAP][name] = [value];
}
}
/**
* Check for header name existence
*
* @param String name Header name
* @return Boolean
*/
has(name) {
name = `${name}`;
validateName(name);
return find(this[MAP], name) !== undefined;
}
/**
* Delete all header values given name
*
* @param String name Header name
* @return Void
*/
delete(name) {
name = `${name}`;
validateName(name);
const key = find(this[MAP], name);
if (key !== undefined) {
delete this[MAP][key];
}
}
/**
* Return raw headers (non-spec api)
*
* @return Object
*/
raw() {
return this[MAP];
}
/**
* Get an iterator on keys.
*
* @return Iterator
*/
keys() {
return createHeadersIterator(this, 'key');
}
/**
* Get an iterator on values.
*
* @return Iterator
*/
values() {
return createHeadersIterator(this, 'value');
}
/**
* Get an iterator on entries.
*
* This is the default iterator of the Headers object.
*
* @return Iterator
*/
[Symbol.iterator]() {
return createHeadersIterator(this, 'key+value');
}
}
Headers.prototype.entries = Headers.prototype[Symbol.iterator];
Object.defineProperty(Headers.prototype, Symbol.toStringTag, {
value: 'Headers',
writable: false,
enumerable: false,
configurable: true
});
Object.defineProperties(Headers.prototype, {
get: { enumerable: true },
forEach: { enumerable: true },
set: { enumerable: true },
append: { enumerable: true },
has: { enumerable: true },
delete: { enumerable: true },
keys: { enumerable: true },
values: { enumerable: true },
entries: { enumerable: true }
});
function getHeaders(headers) {
let kind = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'key+value';
const keys = Object.keys(headers[MAP]).sort();
return keys.map(kind === 'key' ? function (k) {
return k.toLowerCase();
} : kind === 'value' ? function (k) {
return headers[MAP][k].join(', ');
} : function (k) {
return [k.toLowerCase(), headers[MAP][k].join(', ')];
});
}
const INTERNAL = Symbol('internal');
function createHeadersIterator(target, kind) {
const iterator = Object.create(HeadersIteratorPrototype);
iterator[INTERNAL] = {
target,
kind,
index: 0
};
return iterator;
}
const HeadersIteratorPrototype = Object.setPrototypeOf({
next() {
// istanbul ignore if
if (!this || Object.getPrototypeOf(this) !== HeadersIteratorPrototype) {
throw new TypeError('Value of `this` is not a HeadersIterator');
}
var _INTERNAL = this[INTERNAL];
const target = _INTERNAL.target,
kind = _INTERNAL.kind,
index = _INTERNAL.index;
const values = getHeaders(target, kind);
const len = values.length;
if (index >= len) {
return {
value: undefined,
done: true
};
}
this[INTERNAL].index = index + 1;
return {
value: values[index],
done: false
};
}
}, Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]())));
Object.defineProperty(HeadersIteratorPrototype, Symbol.toStringTag, {
value: 'HeadersIterator',
writable: false,
enumerable: false,
configurable: true
});
/**
* Export the Headers object in a form that Node.js can consume.
*
* @param Headers headers
* @return Object
*/
function exportNodeCompatibleHeaders(headers) {
const obj = Object.assign({ __proto__: null }, headers[MAP]);
// http.request() only supports string as Host header. This hack makes
// specifying custom Host header possible.
const hostHeaderKey = find(headers[MAP], 'Host');
if (hostHeaderKey !== undefined) {
obj[hostHeaderKey] = obj[hostHeaderKey][0];
}
return obj;
}
/**
* Create a Headers object from an object of headers, ignoring those that do
* not conform to HTTP grammar productions.
*
* @param Object obj Object of headers
* @return Headers
*/
function createHeadersLenient(obj) {
const headers = new Headers();
for (const name of Object.keys(obj)) {
if (invalidTokenRegex.test(name)) {
continue;
}
if (Array.isArray(obj[name])) {
for (const val of obj[name]) {
if (invalidHeaderCharRegex.test(val)) {
continue;
}
if (headers[MAP][name] === undefined) {
headers[MAP][name] = [val];
} else {
headers[MAP][name].push(val);
}
}
} else if (!invalidHeaderCharRegex.test(obj[name])) {
headers[MAP][name] = [obj[name]];
}
}
return headers;
}
const INTERNALS$1 = Symbol('Response internals');
/**
* Response class
*
* @param Stream body Readable stream
* @param Object opts Response options
* @return Void
*/
class Response {
constructor() {
let body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
let opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
if (!opts.name) {
opts.name = "Response";
}
Body.call(this, body, opts);
const status = opts.status || 200;
this[INTERNALS$1] = {
url: opts.url,
status,
statusText: opts.statusText || STATUS_CODES[status],
headers: new Headers(opts.headers)
};
}
get url() {
return this[INTERNALS$1].url;
}
get status() {
return this[INTERNALS$1].status;
}
/**
* Convenience property representing if the request ended normally
*/
get ok() {
return this[INTERNALS$1].status >= 200 && this[INTERNALS$1].status < 300;
}
get statusText() {
return this[INTERNALS$1].statusText;
}
get headers() {
return this[INTERNALS$1].headers;
}
/**
* Clone this response
*
* @return Response
*/
clone() {
return new Response(cloneBody(this), {
url: this.url,
status: this.status,
statusText: this.statusText,
headers: this.headers,
ok: this.ok,
name: `cloned(${getInstanceName(this)})`
});
}
}
Body.mixIn(Response.prototype);
Object.defineProperties(Response.prototype, {
url: { enumerable: true },
status: { enumerable: true },
ok: { enumerable: true },
statusText: { enumerable: true },
headers: { enumerable: true },
clone: { enumerable: true }
});
Object.defineProperty(Response.prototype, Symbol.toStringTag, {
value: 'Response',
writable: false,
enumerable: false,
configurable: true
});
const INTERNALS$2 = Symbol('Request internals');
/**
* Check if a value is an instance of Request.
*
* @param Mixed input
* @return Boolean
*/
function isRequest(input) {
return typeof input === 'object' && typeof input[INTERNALS$2] === 'object';
}
/**
* Request class
*
* @param Mixed input Url or Request instance
* @param Object init Custom options
* @return Void
*/
class Request {
constructor(input) {
let init = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
let parsedURL;
// normalize input
if (!isRequest(input)) {
if (input && input.href) {
// in order to support Node.js' Url objects; though WHATWG's URL objects
// will fall into this branch also (since their `toString()` will return
// `href` property anyway)
parsedURL = parse(input.href);
} else {
// coerce input to a string before attempting to parse
parsedURL = parse(`${input}`);
}
input = {};
} else {
parsedURL = parse(input.url);
}
let method = init.method || input.method || 'GET';
method = method.toUpperCase();
if ((init.body != null || isRequest(input) && getInstanceBody(input) !== null) && (method === 'GET' || method === 'HEAD')) {
throw new TypeError('Request with GET/HEAD method cannot have body');
}
let inputBody = null;
if (init.body != null) {
inputBody = init.body;
} else if (isRequest(input) && getInstanceBody(input) != null) {
inputBody = cloneBody(input);
}
Body.call(this, inputBody, {
timeout: init.timeout || input.timeout || 0,
size: init.size || input.size || 0,
parent: "Request"
});
const headers = new Headers(init.headers || input.headers || {});
if (inputBody !== null) {
const contentType = extractContentType(this);
if (contentType !== null && !headers.has('Content-Type')) {
headers.append('Content-Type', contentType);
}
}
this[INTERNALS$2] = {
method,
redirect: init.redirect || input.redirect || 'follow',
headers,
parsedURL
};
// node-fetch-only options
this.follow = init.follow !== undefined ? init.follow : input.follow !== undefined ? input.follow : 20;
this.compress = init.compress !== undefined ? init.compress : input.compress !== undefined ? input.compress : true;
this.counter = init.counter || input.counter || 0;
this.agent = init.agent || input.agent;
}
get method() {
return this[INTERNALS$2].method;
}
get url() {
return format(this[INTERNALS$2].parsedURL);
}
get headers() {
return this[INTERNALS$2].headers;
}
get redirect() {
return this[INTERNALS$2].redirect;
}
/**
* Clone this request
*
* @return Request
*/
clone() {
return new Request(this);
}
}
Body.mixIn(Request.prototype);
Object.defineProperty(Request.prototype, Symbol.toStringTag, {
value: 'Request',
writable: false,
enumerable: false,
configurable: true
});
Object.defineProperties(Request.prototype, {
method: { enumerable: true },
url: { enumerable: true },
headers: { enumerable: true },
redirect: { enumerable: true },
clone: { enumerable: true }
});
/**
* Convert a Request to Node.js http request options.
*
* @param Request A Request instance
* @return Object The options object to be passed to http.request
*/
function getNodeRequestOptions(request) {
const parsedURL = request[INTERNALS$2].parsedURL;
const headers = new Headers(request[INTERNALS$2].headers);
const body = getInstanceBody(request);
// fetch step 1.3
if (!headers.has('Accept')) {
headers.set('Accept', '*/*');
}
// Basic fetch
if (!parsedURL.protocol || !parsedURL.hostname) {
throw new TypeError('Only absolute URLs are supported');
}
if (!/^https?:$/.test(parsedURL.protocol)) {
throw new TypeError('Only HTTP(S) protocols are supported');
}
// HTTP-network-or-cache fetch steps 2.4-2.7
let contentLengthValue = null;
if (body == null && /^(POST|PUT)$/i.test(request.method)) {
contentLengthValue = '0';
}
if (body != null) {
const totalBytes = getTotalBytes(request);
if (typeof totalBytes === 'number') {
contentLengthValue = String(totalBytes);
}
}
if (contentLengthValue) {
headers.set('Content-Length', contentLengthValue);
}
// HTTP-network-or-cache fetch step 2.11
if (!headers.has('User-Agent')) {
headers.set('User-Agent', 'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)');
}
// HTTP-network-or-cache fetch step 2.15
if (request.compress) {
headers.set('Accept-Encoding', 'gzip,deflate');
}
if (!headers.has('Connection') && !request.agent) {
headers.set('Connection', 'close');
}
// HTTP-network fetch step 4.2
// chunked encoding is handled by Node.js
return Object.assign({}, parsedURL, {
method: request.method,
headers: exportNodeCompatibleHeaders(headers),
agent: request.agent
});
}
/**
* Fetch function
*
* @param Mixed url Absolute url or Request instance
* @param Object opts Fetch options
* @return Promise
*/
function fetch(url, opts) {
// wrap http.request into fetch
return new Promise(function (resolve$$1, reject) {
// build request object
const request = new Request(url, opts);
const options = getNodeRequestOptions(request);
const send = (options.protocol === 'https:' ? https : http).request;
// send request
const req = send(options);
let reqTimeout;
function finalize() {
req.abort();
clearTimeout(reqTimeout);
}
if (request.timeout) {
req.once('socket', function (socket) {
reqTimeout = setTimeout(function () {
reject(new FetchError(`network timeout at: ${request.url}`, 'request-timeout'));
finalize();
}, request.timeout);
});
}
req.on('error', function (err) {
reject(new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err));
finalize();
});
req.on('response', function (res) {
clearTimeout(reqTimeout);
const headers = createHeadersLenient(res.headers);
// HTTP fetch step 5
if (fetch.isRedirect(res.statusCode)) {
// HTTP fetch step 5.2
const location = headers.get('Location');
// HTTP fetch step 5.3
const locationURL = location === null ? null : resolve(request.url, location);
// HTTP fetch step 5.5
switch (request.redirect) {
case 'error':
reject(new FetchError(`redirect mode is set to error: ${request.url}`, 'no-redirect'));
finalize();
return;
case 'manual':
// node-fetch-specific step: make manual redirect a bit easier to use by setting the Location header value to the resolved URL.
if (locationURL !== null) {
headers.set('Location', locationURL);
}
break;
case 'follow':
// HTTP-redirect fetch step 2
if (locationURL === null) {
break;
}
// HTTP-redirect fetch step 5
if (request.counter >= request.follow) {
reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect'));
finalize();
return;
}
const requestBody = getInstanceBody(request);
// HTTP-redirect fetch step 6 (counter increment)
// Create a new Request object.
const requestOpts = {
headers: new Headers(request.headers),
follow: request.follow,
counter: request.counter + 1,
agent: request.agent,
compress: request.compress,
method: request.method,
body: requestBody
};
// HTTP-redirect fetch step 9
if (res.statusCode !== 303 && requestBody && getTotalBytes(request) === null) {
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
finalize();
return;
}
// HTTP-redirect fetch step 11
if (res.statusCode === 303 || (res.statusCode === 301 || res.statusCode === 302) && request.method === 'POST') {
requestOpts.method = 'GET';
requestOpts.body = undefined;
requestOpts.headers.delete('content-length');
}
// HTTP-redirect fetch step 15
resolve$$1(fetch(new Request(locationURL, requestOpts)));
finalize();
return;
}
}
// prepare response
// const { readable, writable } = new TransformStream();
// const writer = writable.getWriter();
let body = res.pipe(new PassThrough());
// res.on("data", (data) => writer.write(data));
// res.on("end", () => writer.close());
const response_options = {
url: request.url,
status: res.statusCode,
statusText: res.statusMessage,
headers: headers,
size: request.size,
timeout: request.timeout
};
// HTTP-network fetch step 12.1.1.3
const codings = headers.get('Content-Encoding');
// HTTP-network fetch step 12.1.1.4: handle content codings
// in following scenarios we ignore compression support
// 1. compression support is disabled
// 2. HEAD request
// 3. no Content-Encoding header
// 4. no content response (204)
// 5. content not modified response (304)
if (!request.compress || request.method === 'HEAD' || codings === null || res.statusCode === 204 || res.statusCode === 304) {
resolve$$1(new Response(body, response_options));
return;
}
// For Node v6+
// Be less strict when decoding compressed responses, since sometimes
// servers send slightly invalid responses that are still accepted
// by common browsers.
// Always using Z_SYNC_FLUSH is what cURL does.
const zlibOptions = {
flush: zlib.Z_SYNC_FLUSH,
finishFlush: zlib.Z_SYNC_FLUSH
};
// for gzip
if (codings == 'gzip' || codings == 'x-gzip') {
body = body.pipe(zlib.createGunzip(zlibOptions));
resolve$$1(new Response(body, response_options));
// for deflate
} else if (codings == 'deflate' || codings == 'x-deflate') {
// handle the infamous raw deflate response from old servers
// a hack for old IIS and Apache servers
const raw = res.pipe(new PassThrough());
raw.once('data', function (chunk) {
// see http://stackoverflow.com/questions/37519828
if ((chunk[0] & 0x0F) === 0x08) {
body = body.pipe(zlib.createInflate());
} else {
body = body.pipe(zlib.createInflateRaw());
}
resolve$$1(new Response(body, response_options));
});
// otherwise, use response as-is
} else {
resolve$$1(new Response(body, response_options));
}
});
writeToStream(req, request);
});
}
/**
* Redirect code matching
*
* @param Number code Status code
* @return Boolean
*/
fetch.isRedirect = function (code) {
return code === 301 || code === 302 || code === 303 || code === 307 || code === 308;
};
export default fetch;
export { Headers, Request, Response, FetchError };