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

Rewrite Got #1051

Merged
merged 29 commits into from
Apr 12, 2020
Merged

Rewrite Got #1051

merged 29 commits into from
Apr 12, 2020

Conversation

szmarczak
Copy link
Collaborator

@szmarczak szmarczak commented Feb 6, 2020

Changelog

Bug fixes

  1. Fixes Got doesn't pass query-string when calling via unix-socket #1036 (tested)
  2. Fixes beforeRequest hook is not called before the actual request #994 (tested)
  3. Fixes got.stream() swallows errors when used to upload small files with stream.pipeline() #1026 (tested)
  4. Fixes Using got.post with both body and cache options causes an exception #1021 (tested)
  5. Fixes Got doesn't throw on leading slashes #1057 (tested)
  6. Fixes Got throws if passed options are already frozen #1050 (tested)
  7. Fixes TypeScript cannot figure out which Got type to use #954 (tested)
  8. Fixes mergeOptions isn't merging URLSearchParams instances #1011 (tested)
  9. Fixes HTTP authentication leak in redirects #1090 (tested)
  10. Fixes Pagination should ignore resolveBodyOnly option #1140 (tested)
  11. Fixes Cannot reuse an options object #1118 (tested)

Breaking changes

API

  • options.encoding will no longer affect streams (reverting back to the Got 9 behavior), use got.stream(...).setEncoding(encoding) instead (to prevent confusion)
  • renamed GotError to RequestError
  • options.agent can no longer be an instance of http.Agent - instead you need to pass:
{
	http: new http.Agent(...),
	https: new https.Agent(...),
	http2: new http2wrapper.Agent(...)
}
  • options.useElectronNet has been removed
  • dnsCache is now enabled by default
  • dnsCache no longer can be a Map or a Keyv instance, instead you need to pass a CacheableLookup instance
  • errors thrown in init hooks will be converted to RequestErrors (why: RequestError provides much more useful information (e.g. Got options) than the usual TypeError etc.)
  • options._pagination has been renamed to options.pagination
  • the options.url property will no longer be visible in init hooks (design limitation)
  • the error.request property is no longer a ClientRequest instance - instead, it gives a Got stream

Types

  • renamed the Defaults type to InstanceDefaults
  • renamed the DefaultOptions type to Defaults
  • renamed the DefaultRetryOptions type to RequiredRetryOptions
  • renamed the GotOptions type to Options
  • the ResponseObject type has been merged into the Response type
  • renamed the GotRequestMethod type to GotRequestFunction

Enhancements

  1. Fixes The merge function is slow #1016 (benchmarked)
  2. Fixes Use error.code instead of error.message to compare errors #981 (refactored)
  3. Fixes The retry logic should be moved into as-promise.ts #932 (refactored)
  4. Fixes Pass error thrown in the init hook to beforeError hook #929 (tested)
  5. Fixes A way to customize cacheable-lookup #1058 (tested)
  6. Fixes HTTP2 support #167 (tested, but there's only 1 test as of now, blocked by Use fastify in tests instead of express #1093)
  7. Fixes Errors have undefined body when using streams #1138 (tested)
  8. Fixes Spaces should be normalized as + in query strings #1113 (tested)
  9. Fixes Modify response headers while using got.stream(...) #1129 (tested)
  10. The errors now have a request property (it's a Got stream).

Miscellaneous

  1. Fixes got actually need nodejs >= 10.9, not >= 10 #1130

Known bugs

  1. The timings indicate that the request was successful (even though it errored).
  2. The downloadProgress object may show incorrect data if the request has errored.

TODO

FEEL FREE TO TEST THIS

Note that this has yet undocumented breaking changes, but everything should work as expected - all tests pass. TypeScript recommended.

git clone https://github.com/szmarczak/got.git
git checkout rework
npm install
npm run build
const got = require('./dist/source');

(async () => {
	console.log(await got('https://httpbin.org/anything'));
})();

IssueHunt Summary

Referenced issues

This pull request has been submitted to:


IssueHunt has been backed by the following sponsors. Become a sponsor

@sindresorhus
Copy link
Owner

I got chills down my spine when I read the PR title 😅

I hope you plan to add lots of tests. I'm very paranoid that we'll end up breaking a lot of stuff that we're not currently testing.

@szmarczak
Copy link
Collaborator Author

I hope you plan to add lots of tests.

Yup. Every fix & every change in behavior is gonna be tested.

I'm very paranoid that we'll end up breaking a lot of stuff that we're not currently testing.

Don't worry! There's gonna be a very detailed changelog. I'll explain why things are done like that and not in another way.

// got - stream x 4,313 ops/sec ±5.61% (72 runs sampled)
// got - promise core x 6,756 ops/sec ±5.32% (80 runs sampled)
// got - stream core x 6,863 ops/sec ±4.68% (76 runs sampled)
// got - stream core - normalized options x 7,960 ops/sec ±3.83% (81 runs sampled)
Copy link
Collaborator Author

@szmarczak szmarczak Mar 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normalizing cuts out ~1k req/s but the time is needed to duplicate the options, so the original ones won't be altered if you use hooks to modify the request. Hooks are our main adventage while being compared to other HTTP clients.

Comment on lines +186 to +187
// got - promise x 3,092 ops/sec ±5.25% (73 runs sampled)
// got - stream x 4,313 ops/sec ±5.61% (72 runs sampled)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why, but the got function cuts out 3k-4k req/s.

@@ -10,7 +10,7 @@
"node": ">=10"
},
"scripts": {
"test": "xo && tsc --noEmit && nyc ava",
"test": "xo && tsc --noEmit && nyc --reporter=html --reporter=text ava",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very useful to debug later. Generates both HTML output and text output as usual.

import Request, {knownHookEvents, RequestError, HTTPError} from '../core';

if (!knownHookEvents.includes('beforeRetry' as any)) {
knownHookEvents.push('beforeRetry' as any, 'afterResponse' as any);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normalizing will be done via Request.normalizeArguments so we just tell Request that it needs to normalize beforeRetry too.

Comment on lines +133 to +141
if (this._throwHttpErrors && !isHTTPError) {
this.destroy(error);
} else {
this.emit('error', error);
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot destroy this if the retry functionality is enabled. This is the responsibility of the Got promise.


// Download body
try {
body = await getStream.buffer(request);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is request, not response. If we were downloading directly from the response there would be no progress events emitted.

@@ -0,0 +1,149 @@
import PCancelable = require('p-cancelable');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file just adds promise-specific options.

@@ -1,144 +0,0 @@
import duplexer3 = require('duplexer3');
Copy link
Collaborator Author

@szmarczak szmarczak Mar 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got is based directly on Duplex streams.

}

if (options.encoding) {
proxy.setEncoding(options.encoding);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change: options.encoding will no longer affect streams (it's the same behavior as in Got 9), people should do got.stream(...).setEncoding(encoding) instead. It should be mentioned in the docs that this option is for promises only.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also be documented as a doc comment on the options.encoding type.

// We cannot use `stream.pipeline(...)` here,
// because if we did then `output` would throw
// the original error before throwing `ReadError`.
response.pipe(output);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new implementation doesn't do this. It just proxies the _read calls (basically a Duplex stream).

@@ -0,0 +1,73 @@
/* istanbul ignore file: deprecated */
Copy link
Collaborator Author

@szmarczak szmarczak Mar 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: searchParams is handled via the Request.normalizeArguments, so it won't be present here (as well as options.username and options.password).

import {TimeoutError as TimedOutError} from './utils/timed-out';
import {Response, NormalizedOptions} from './types';

export class GotError extends Error {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking: renamed GotError to RequestError

Comment on lines -223 to -234
options.decompress = Boolean(options.decompress);
options.isStream = Boolean(options.isStream);
options.throwHttpErrors = Boolean(options.throwHttpErrors);
options.ignoreInvalidCookies = Boolean(options.ignoreInvalidCookies);
options.cache = options.cache ?? false;
options.responseType = options.responseType ?? 'text';
options.resolveBodyOnly = Boolean(options.resolveBodyOnly);
options.followRedirect = Boolean(options.followRedirect);
options.dnsCache = options.dnsCache ?? false;
options.useElectronNet = Boolean(options.useElectronNet);
options.methodRewriting = Boolean(options.methodRewriting);
options.allowGetBody = Boolean(options.allowGetBody);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been dropped to get a 1.5k req/s increase. It will be inherited from defaults so it's unnecessary either because the defaults define these booleans.

import {Transform as TransformStream} from 'stream';
import is from '@sindresorhus/is';

export function createProgressStream(name: 'downloadProgress' | 'uploadProgress', emitter: EventEmitter, totalBytes?: number | string): TransformStream {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been removed in favor of the Duplex stream (yeah, we can do things like this there).

// `request.aborted` is a boolean since v11.0.0: https://github.com/nodejs/node/commit/4b00c4fafaa2ae8c41c1f78823c0feb810ae4723#diff-e3bc37430eb078ccbafe3aa3b570c91a
const isAborted = (): boolean => typeof currentRequest.aborted === 'number' || (currentRequest.aborted as unknown as boolean);

const emitError = async (error: GeneralError): Promise<void> => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been renamed to request._beforeError.


isPiped = true;

await pipeline(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to use pipeline if we do Duplex.

}
};

emitter.retry = error => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been moved to as-promise/index.ts.


Note: Not available when the response is cached. This is hopefully a temporary limitation, see [lukechilds/cacheable-request#86](https://github.com/lukechilds/cacheable-request/issues/86).
*/
ip: string;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been corrected to ip?: string;

Comment on lines -91 to -93
export interface RetryOptions extends Partial<DefaultRetryOptions> {
retries?: number;
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it even here? It has been removed in Got 10...

export interface AgentByProtocol {
http?: http.Agent;
https?: https.Agent;
}
Copy link
Collaborator Author

@szmarczak szmarczak Mar 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking change: options.agent can no longer be an instance of http.Agent. Instead, you need to pass an object like:

{
	http: new http.Agent(...),
	https: new https.Agent(...),
	http2: new http2wrapper.Agent(...)
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you mention all the breaking changes in the PR description?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do! :D

@szmarczak
Copy link
Collaborator Author

Can you double-check that you have ported all commits landed on master since you opened this PR into this branch?

Done. Nothing's missing as of now.

Also double-checked the types and the logic in general. Didn't notice any bugs other than those in the PR description (just nitpicks, nothing breaking).

@sindresorhus sindresorhus merged commit 286e31e into sindresorhus:master Apr 12, 2020
@sindresorhus
Copy link
Owner

Wow! This includes a lot of fixes and improvements. Really nice work, @szmarczak! 🎉

@joebowbeer
Copy link

You might want to list the fix for #1090 as a breaking change in case someone was depending on forwarded authorization headers.

@@ -7,10 +7,10 @@
"funding": "https://github.com/sindresorhus/got?sponsor=1",
"main": "dist/source",
"engines": {
"node": ">=10"
"node": ">=10.19.0"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@szmarczak Sorry to be commenting on a merged PR, but is there a reason you had to bump this? This should probably be mentioned as a breaking change?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment