Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Major refactoring #921

Merged
merged 35 commits into from
Nov 16, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c29d4d3
init
szmarczak Nov 5, 2019
fcc18f8
Big big changes
szmarczak Nov 6, 2019
b803076
fixes
szmarczak Nov 7, 2019
3ceba12
Merge with master
szmarczak Nov 7, 2019
29fee34
remove unnecessary semicolon
szmarczak Nov 7, 2019
d5a2ff2
enhancements
szmarczak Nov 7, 2019
5236d84
rename stream to isStream
szmarczak Nov 7, 2019
c7dbe1e
throw on legacy url input
szmarczak Nov 7, 2019
f1f203a
enhancements
szmarczak Nov 10, 2019
461e8d9
bug fixes
szmarczak Nov 10, 2019
b044037
fixes
szmarczak Nov 10, 2019
22c36bf
fix option merge
szmarczak Nov 11, 2019
aedac8c
more bug fixes
szmarczak Nov 11, 2019
d7c7d53
fixes
szmarczak Nov 11, 2019
507a3cc
make tests pass
szmarczak Nov 11, 2019
9255df7
remove todo
szmarczak Nov 11, 2019
778cf67
Remove got.create() & update docs
szmarczak Nov 12, 2019
e8ff08b
update docs
szmarczak Nov 12, 2019
0841642
another fix
szmarczak Nov 12, 2019
fe76e8c
nitpick
szmarczak Nov 12, 2019
3def303
types
szmarczak Nov 12, 2019
695ebaf
generic cookiejar object
szmarczak Nov 12, 2019
52496df
make tests pass
szmarczak Nov 12, 2019
c6b22e1
Refactor the got() function, aka: fix bugs
szmarczak Nov 14, 2019
ef32a0e
Throw on null value headers
szmarczak Nov 14, 2019
dde0b76
bug fixes
szmarczak Nov 14, 2019
a0850bd
Improve is usage
szmarczak Nov 14, 2019
0d9baf1
remove useless line
szmarczak Nov 14, 2019
42b2605
comments
szmarczak Nov 14, 2019
3b313a7
call beforeRetry hook when retrying in afterResponse hook
szmarczak Nov 14, 2019
518c00d
nitpicks
szmarczak Nov 15, 2019
7f2f477
nitpicks
szmarczak Nov 15, 2019
5bc7319
nitpicks
szmarczak Nov 15, 2019
3ff5ebb
no unnecessary escape
szmarczak Nov 15, 2019
a7e73f2
Update readme.md
sindresorhus Nov 16, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,15 @@ Cookie support. You don't have to care about parsing or how to store them. [Exam

###### cookieJar.setCookie

The function takes two arguments: `rawCookie` (`string`) and `url` (`string`). It needs to be an async function or some callback-style function.
Type: `Function<Promise>`

The function takes two arguments: `rawCookie` (`string`) and `url` (`string`).

###### cookieJar.getCookieString

The function takes one argument: `url` (`string`). It needs to be an async function or some callback-style function.
Type: `Function<Promise>`

The function takes one argument: `url` (`string`).

###### ignoreInvalidCookies

Expand Down
114 changes: 55 additions & 59 deletions source/as-promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,32 @@ import is from '@sindresorhus/is';
import PCancelable = require('p-cancelable');
import {NormalizedOptions, Response, CancelableRequest} from './utils/types';
import {ParseError, ReadError, HTTPError} from './errors';
import requestAsEventEmitter from './request-as-event-emitter';
import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
import {normalizeArguments, mergeOptions} from './normalize-arguments';

type ResponseReturn = Response | Buffer | string | any;
const parseBody = (body: Response['body'], responseType: NormalizedOptions['responseType'], statusCode: Response['statusCode']) => {
if (responseType === 'json') {
return statusCode === 204 ? '' : JSON.parse(body);
}

export const isProxiedSymbol: unique symbol = Symbol('proxied');
if (responseType === 'buffer') {
return Buffer.from(body);
}

export default function asPromise(options: NormalizedOptions): CancelableRequest<Response> {
const proxy = new EventEmitter();
if (responseType === 'text') {
return body.toString();
}

const parseBody = (response: Response): void => {
if (options.responseType === 'json') {
response.body = response.statusCode === 204 ? '' : JSON.parse(response.body);
} else if (options.responseType === 'buffer') {
response.body = Buffer.from(response.body);
} else if (options.responseType !== 'text' && !is.falsy(options.responseType)) {
throw new Error(`Failed to parse body of type '${options.responseType}'`);
}
};
if (responseType === '') {
return body;
}

throw new Error(`Failed to parse body of type '${responseType}'`);
};

export default function asPromise(options: NormalizedOptions) {
const proxy = new EventEmitter();
let finalResponse: Pick<Response, 'body' | 'statusCode'>;

// @ts-ignore `.json()`, `.buffer()` and `.text()` are added later
const promise = new PCancelable<IncomingMessage>((resolve, reject, onCancel) => {
Expand All @@ -46,11 +53,10 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
emitter.on('response', async (response: Response) => {
proxy.emit('response', response);

const stream = is.null_(options.encoding) ? getStream.buffer(response) : getStream(response, {encoding: options.encoding});
const streamAsPromise = is.null_(options.encoding) ? getStream.buffer(response) : getStream(response, {encoding: options.encoding});

let data: Buffer | string;
try {
data = await stream;
response.body = await streamAsPromise;
} catch (error) {
emitError(new ReadError(error, options));
return;
Expand All @@ -61,10 +67,6 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest
return;
}

const limitStatusCode = options.followRedirect ? 299 : 399;

response.body = data;

try {
for (const [index, hook] of options.hooks.afterResponse.entries()) {
// eslint-disable-next-line no-await-in-loop
Expand Down Expand Up @@ -93,18 +95,22 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest

const {statusCode} = response;

if (response.body) {
try {
parseBody(response);
} catch (error) {
if (statusCode >= 200 && statusCode < 300) {
const parseError = new ParseError(error, response, options);
emitError(parseError);
return;
}
finalResponse = {
body: response.body,
statusCode
};

try {
response.body = parseBody(response.body, options.responseType, response.statusCode);
} catch (error) {
if (statusCode >= 200 && statusCode < 300) {
const parseError = new ParseError(error, response, options);
emitError(parseError);
return;
}
}

const limitStatusCode = options.followRedirect ? 299 : 399;
if (statusCode !== 304 && (statusCode < 200 || statusCode > limitStatusCode)) {
const error = new HTTPError(response, options);
if (emitter.retry(error) === false) {
Expand All @@ -124,44 +130,34 @@ export default function asPromise(options: NormalizedOptions): CancelableRequest

emitter.once('error', reject);

const events = [
'request',
'redirect',
'uploadProgress',
'downloadProgress'
];

for (const event of events) {
emitter.on(event, (...args: unknown[]) => {
proxy.emit(event, ...args);
});
}
}) as CancelableRequest<ResponseReturn>;

promise[isProxiedSymbol] = true;
proxyEvents(proxy, emitter);
}) as CancelableRequest<any>;

promise.on = (name, fn) => {
promise.on = (name: string, fn: (...args: any[]) => void) => {
proxy.on(name, fn);
return promise;
};

promise.json = () => {
options.responseType = 'json';
options.resolveBodyOnly = true;
return promise;
};
const shortcut = (responseType: NormalizedOptions['responseType']): CancelableRequest<any> => {
// eslint-disable-next-line promise/prefer-await-to-then
const newPromise = promise.then(() => parseBody(finalResponse.body, responseType, finalResponse.statusCode));

promise.buffer = () => {
options.responseType = 'buffer';
options.resolveBodyOnly = true;
return promise;
Object.defineProperties(newPromise, Object.getOwnPropertyDescriptors(promise));

// @ts-ignore The missing properties are added above
return newPromise;
};

promise.text = () => {
options.responseType = 'text';
options.resolveBodyOnly = true;
return promise;
promise.json = () => {
if (is.undefined(options.headers.accept)) {
options.headers.accept = 'application/json';
}

return shortcut('json');
};

promise.buffer = () => shortcut('buffer');
promise.text = () => shortcut('text');

return promise;
}
17 changes: 3 additions & 14 deletions source/as-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {PassThrough as PassThroughStream, Duplex as DuplexStream} from 'stream';
import stream = require('stream');
import {IncomingMessage} from 'http';
import duplexer3 = require('duplexer3');
import requestAsEventEmitter from './request-as-event-emitter';
import requestAsEventEmitter, {proxyEvents} from './request-as-event-emitter';
import {HTTPError, ReadError} from './errors';
import {NormalizedOptions, Response, GotEvents} from './utils/types';

Expand Down Expand Up @@ -101,19 +101,8 @@ export default function asStream(options: NormalizedOptions): ProxyStream {
proxy.emit('response', response);
});

const events = [
'error',
'request',
'redirect',
'uploadProgress',
'downloadProgress'
];

for (const event of events) {
emitter.on(event, (...args) => {
proxy.emit(event, ...args);
});
}
proxyEvents(proxy, emitter);
emitter.on('error', (error: Error) => proxy.emit('error', error));

const pipe = proxy.pipe.bind(proxy);
const unpipe = proxy.unpipe.bind(proxy);
Expand Down
9 changes: 6 additions & 3 deletions source/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const defaultHandler: HandlerFunction = (options, next) => next(options);

const create = (defaults: Defaults): Got => {
// Proxy properties from next handlers
defaults._rawHandlers = defaults.handlers;
defaults.handlers = defaults.handlers.map(fn => ((options, next) => {
let root: GotReturn;

Expand Down Expand Up @@ -110,18 +111,18 @@ const create = (defaults: Defaults): Got => {

got.extend = (...instancesOrOptions) => {
const optionsArray: Options[] = [defaults.options];
const handlers: HandlerFunction[] = [...defaults.handlers];
let handlers: HandlerFunction[] = [...defaults._rawHandlers];
let mutableDefaults: boolean;

for (const value of instancesOrOptions) {
if (Reflect.has(value, 'defaults')) {
optionsArray.push((value as Got).defaults.options);

handlers.push(...(value as Got).defaults.handlers.filter(handler => handler !== defaultHandler));
handlers.push(...(value as Got).defaults._rawHandlers);

mutableDefaults = (value as Got).defaults.mutableDefaults;
} else {
optionsArray.push(value as Options);
optionsArray.push(value as ExtendedOptions);

if (Reflect.has(value, 'handlers')) {
handlers.push(...(value as ExtendedOptions).handlers);
Expand All @@ -131,6 +132,8 @@ const create = (defaults: Defaults): Got => {
}
}

handlers = handlers.filter(handler => handler !== defaultHandler);

if (handlers.length === 0) {
handlers.push(defaultHandler);
}
Expand Down
5 changes: 4 additions & 1 deletion source/get-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import {IncomingMessage} from 'http';
import EventEmitter = require('events');
import stream = require('stream');
import decompressResponse = require('decompress-response');
import mimicResponse = require('mimic-response');
import {NormalizedOptions, Response} from './utils/types';
import {downloadProgress} from './progress';

export default (response: IncomingMessage, options: NormalizedOptions, emitter: EventEmitter) => {
const downloadBodySize = Number(response.headers['content-length']) || undefined;
const progressStream = downloadProgress(response, emitter, downloadBodySize);
const progressStream = downloadProgress(emitter, downloadBodySize);

mimicResponse(response, progressStream);

const newResponse = (
options.decompress === true &&
Expand Down
2 changes: 1 addition & 1 deletion source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const defaults: Defaults = {
cache: false,
dnsCache: false,
useElectronNet: false,
responseType: 'text',
responseType: '',
resolveBodyOnly: false,
maxRedirects: 10
},
Expand Down
41 changes: 27 additions & 14 deletions source/normalize-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import getBodySize from './utils/get-body-size';
import isFormData from './utils/is-form-data';
import supportsBrotli from './utils/supports-brotli';

// TODO: Add this to documentation:
// `preNormalizeArguments` normalizes these options: `headers`, `prefixUrl`, `hooks`, `timeout`, `retry` and `method`.
// `normalizeArguments` is *only* called on `got(...)`. It normalizes the URL and performs `mergeOptions(...)`.
// `normalizeRequestArguments` converts Got options into HTTP options.
Expand Down Expand Up @@ -146,12 +145,16 @@ export const preNormalizeArguments = (options: Options, defaults?: NormalizedOpt
if (options.cookieJar) {
let {setCookie, getCookieString} = options.cookieJar;

if (setCookie.length !== 2) {
setCookie = promisify(options.cookieJar.setCookie.bind(options.cookieJar));
}

if (setCookie.length !== 2) {
getCookieString = promisify(options.cookieJar.getCookieString.bind(options.cookieJar));
// Horrible `tough-cookie` check
if (setCookie.length === 4 && getCookieString.length === 0) {
if (!Reflect.has(setCookie, promisify.custom)) {
setCookie = promisify(setCookie.bind(options.cookieJar));
getCookieString = promisify(getCookieString.bind(options.cookieJar));
}
} else if (setCookie.length !== 2) {
throw new TypeError('`options.cookieJar.setCookie` needs to be an async function with 2 arguments');
} else if (getCookieString.length !== 1) {
throw new TypeError('`options.cookieJar.getCookieString` needs to be an async function with 1 argument');
}

options.cookieJar = {setCookie, getCookieString};
Expand Down Expand Up @@ -195,7 +198,7 @@ export const mergeOptions = (...sources: Options[]): NormalizedOptions => {
export const normalizeArguments = (url: URLOrOptions, options?: Options, defaults?: Defaults): NormalizedOptions => {
// Merge options
if (typeof url === 'undefined') {
throw new TypeError('Missing `url` argument.');
throw new TypeError('Missing `url` argument');
}

if (typeof options === 'undefined') {
Expand Down Expand Up @@ -241,7 +244,7 @@ export const normalizeArguments = (url: URLOrOptions, options?: Options, default
let prefixUrl = options.prefixUrl as string;
Object.defineProperty(options, 'prefixUrl', {
set: (value: string) => {
if (normalizedOptions.url.href.startsWith(value)) {
if (!normalizedOptions.url.href.startsWith(value)) {
throw new Error(`Cannot change \`prefixUrl\` from ${prefixUrl} to ${value}: ${normalizedOptions.url.href}`);
}

Expand Down Expand Up @@ -274,7 +277,7 @@ export const normalizeArguments = (url: URLOrOptions, options?: Options, default

const withoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);

type NormalizedRequestArguments = https.RequestOptions & {
export type NormalizedRequestArguments = https.RequestOptions & {
body: Pick<NormalizedOptions, 'body'>;
url: Pick<NormalizedOptions, 'url'>;
};
Expand All @@ -293,11 +296,11 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro
throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
}

if (isBody) {
if (isForm || isJSON) {
throw new TypeError('The `body` option cannot be used with the `json` option or `form` option');
}
if ([isBody, isForm, isJSON].filter(isTrue => isTrue).length > 1) {
throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
}

if (isBody) {
if (is.object(options.body) && isFormData(options.body)) {
// Special case for https://github.com/form-data/form-data
if (!Reflect.has(headers, 'content-type')) {
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -405,8 +408,18 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro
options.request = electron.net.request ?? electron.remote.net.request;
}

// We're not compatible
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
delete options.timeout;

// Set cookies
if (options.cookieJar) {
const cookieString = await options.cookieJar.getCookieString(options.url.toString());

if (is.nonEmptyString(cookieString)) {
options.headers.cookie = cookieString;
}
}

// `http-cache-semantics` check this
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
delete options.url;

Expand Down