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

[WIP] HTTP2 support #832

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"decompress-response": "^5.0.0",
"duplexer3": "^0.1.4",
"get-stream": "^5.0.0",
"http2-wrapper": "^1.0.0-beta.3",
"lowercase-keys": "^2.0.0",
"mimic-response": "^2.0.0",
"p-cancelable": "^2.0.0",
Expand Down
23 changes: 12 additions & 11 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ For browser usage, we recommend [Ky](https://github.com/sindresorhus/ky) by the
## Highlights

- [Promise & stream API](#api)
- [HTTP2 support](#http2-support)
- [Request cancelation](#aborting-the-request)
- [RFC compliant caching](#cache-adapters)
- [Follows redirects](#followredirect)
Expand Down Expand Up @@ -445,10 +446,10 @@ Default: `false`

###### request

Type: `Function`\
Default: `http.request | https.request` *(Depending on the protocol)*
Type: `Function | AsyncFunction`\
Default: `http2wrapper.auto`

Custom request function. The main purpose of this is to [support HTTP2 using a wrapper](#experimental-http2-support).
Custom request function. The main purpose of this is to [support HTTP2 using a wrapper](#http2-support).
szmarczak marked this conversation as resolved.
Show resolved Hide resolved

###### useElectronNet

Expand Down Expand Up @@ -1395,19 +1396,19 @@ const custom = got.extend({
})();
```

### Experimental HTTP2 support
### HTTP2 support

Got provides an experimental support for HTTP2 using the [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper) package:
Got supports HTTP2 via the [`http2-wrapper`](https://github.com/szmarczak/http2-wrapper) package.

**Note:** Overriding `options.request` will disable HTTP2 support.

```js
const got = require('got');
const {request} = require('http2-wrapper');

const h2got = got.extend({request});

(async () => {
const {body} = await h2got('https://nghttp2.org/httpbin/headers');
console.log(body);
const {headers} = await got('https://nghttp2.org/httpbin/anything');
console.log(headers.via);
//=> '2 nghttpx'
})();
```

Expand All @@ -1421,7 +1422,7 @@ Got was created because the popular [`request`](https://github.com/request/reque

| | `got` | [`request`][r0] | [`node-fetch`][n0] | [`ky`][k0] | [`axios`][a0] | [`superagent`][s0] |
|-----------------------|:----------------:|:---------------:|:--------------------:|:-----------------:|:----------------:|:--------------------:|
| HTTP/2 support | | ❌ | ❌ | ❌ | ❌ | ✔️\*\* |
| HTTP/2 support | ✔️ | ❌ | ❌ | ❌ | ❌ | ✔️\*\* |
| Browser support | ❌ | ❌ | ✔️\* | ✔️ | ✔️ | ✔️ |
| Electron support | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Promise API | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
Expand Down
17 changes: 5 additions & 12 deletions source/normalize-arguments.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {promisify} from 'util';
import CacheableRequest = require('cacheable-request');
import http = require('http');
// @ts-ignore Missing types
import http2 = require('http2-wrapper');
import https = require('https');
import Keyv = require('keyv');
import lowercaseKeys = require('lowercase-keys');
Expand All @@ -18,12 +19,10 @@ import merge from './utils/merge';
import optionsToUrl from './utils/options-to-url';
import supportsBrotli from './utils/supports-brotli';
import {
AgentByProtocol,
Defaults,
Method,
NormalizedOptions,
Options,
RequestFunction,
URLOrOptions
} from './utils/types';

Expand All @@ -39,8 +38,6 @@ const nonEnumerableProperties: NonEnumerableProperty[] = [
'form'
];

const isAgentByProtocol = (agent: Options['agent']): agent is AgentByProtocol => is.object(agent);

// TODO: `preNormalizeArguments` should merge `options` & `defaults`
export const preNormalizeArguments = (options: Options, defaults?: NormalizedOptions): NormalizedOptions => {
// `options.headers`
Expand Down Expand Up @@ -302,8 +299,8 @@ const withoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);

export type NormalizedRequestArguments = Merge<https.RequestOptions, {
body?: stream.Readable;
request: RequestFunction;
url: Pick<NormalizedOptions, 'url'>;
request: NormalizedOptions['request'];
url: NormalizedOptions['url'];
}>;

export const normalizeRequestArguments = async (options: NormalizedOptions): Promise<NormalizedRequestArguments> => {
Expand Down Expand Up @@ -402,7 +399,7 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro

// Normalize request function
if (!is.function_(options.request)) {
options.request = options.url.protocol === 'https:' ? https.request : http.request;
options.request = http2.auto;
}

// UNIX sockets
Expand All @@ -421,10 +418,6 @@ export const normalizeRequestArguments = async (options: NormalizedOptions): Pro
}
}

if (isAgentByProtocol(options.agent)) {
options.agent = options.agent[options.url.protocol.slice(0, -1) as keyof AgentByProtocol] ?? options.agent;
}

if (options.dnsCache) {
options.lookup = options.dnsCache.lookup;
}
Expand Down
40 changes: 38 additions & 2 deletions source/request-as-event-emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import stream = require('stream');
import {promisify} from 'util';
import is from '@sindresorhus/is';
import timer from '@szmarczak/http-timer';
import {Promisable} from 'type-fest';
import {ProxyStream} from './as-stream';
import calculateRetryDelay from './calculate-retry-delay';
import {CacheError, GotError, MaxRedirectsError, RequestError, TimeoutError} from './errors';
Expand Down Expand Up @@ -207,6 +208,35 @@ export default (options: NormalizedOptions): RequestAsEventEmitter => {
...urlToOptions(options.url)
};

const request = httpOptions.request!;
httpOptions.request = (url, options, callback) => {
// @ts-ignore Type URL is not assignable to type URL
const result = request(url, options, callback);

if (is.promise(result)) {
const emitter = new EventEmitter();

// @ts-ignore `cacheable-request` doesn't support async request function
result.once = emitter.once.bind(emitter);

// @ts-ignore This is a TS bug, because `result` is `Promise<http.ClientRequest>`
Copy link
Collaborator Author

@szmarczak szmarczak Dec 1, 2019

Choose a reason for hiding this comment

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

It complains that request is unknown I have typed it badly

// eslint-disable-next-line promise/prefer-await-to-then
result.then((request: http.ClientRequest) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

return result.then(...);

request.once('abort', () => {
emitter.emit('abort');
});

request.once('error', error => {
emitter.emit('error', error);
});

return request;
});
}

return result;
};

// @ts-ignore ResponseLike missing socket field, should be fixed upstream
const cacheRequest = options.cacheableRequest!(httpOptions, handleResponse);

Expand All @@ -218,12 +248,18 @@ export default (options: NormalizedOptions): RequestAsEventEmitter => {
}
});

cacheRequest.once('request', handleRequest);
cacheRequest.once('request', async (request: Promisable<http.ClientRequest>) => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Promisable -> ClientRequest | Promise<ClientRequest>

try {
handleRequest(await request);
} catch (error) {
emitError(new RequestError(error, options));
}
});
} else {
// Catches errors thrown by calling `requestFn(…)`
try {
// @ts-ignore URLSearchParams does not equal URLSearchParams
handleRequest(httpOptions.request(options.url, httpOptions, handleResponse));
handleRequest(await httpOptions.request(options.url, httpOptions, handleResponse));
} catch (error) {
emitError(new RequestError(error, options));
}
Expand Down
4 changes: 2 additions & 2 deletions source/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export interface RetryOptions extends Partial<DefaultRetryOptions> {
retries?: number;
}

export type RequestFunction = typeof http.request;
export type RequestFunction = (url: string | URL, options: http.RequestOptions, callback?: (res: http.IncomingMessage) => void) => http.ClientRequest | Promise<http.ClientRequest>;

export interface AgentByProtocol {
http?: http.Agent;
Expand Down Expand Up @@ -175,7 +175,7 @@ export interface GotOptions {
throwHttpErrors?: boolean;
cookieJar?: ToughCookieJar | PromiseCookieJar;
ignoreInvalidCookies?: boolean;
request?: RequestFunction;
request?: RequestFunction | typeof http.request;
agent?: http.Agent | https.Agent | boolean | AgentByProtocol;
cache?: string | CacheableRequest.StorageAdapter | false;
headers?: Headers;
Expand Down
4 changes: 3 additions & 1 deletion test/timeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import EventEmitter = require('events');
import {PassThrough as PassThroughStream} from 'stream';
import stream = require('stream');
import http = require('http');
import https = require('https');
import net = require('net');
import getStream = require('get-stream');
import test from 'ava';
Expand Down Expand Up @@ -285,7 +286,8 @@ test.serial('secureConnect timeout', withServerAndLolex, async (t, _server, got,
return socket;
},
timeout: {secureConnect: 0},
retry: 0
retry: 0,
request: https.request
}).on('request', (request: http.ClientRequest) => {
request.on('socket', () => {
clock.runAll();
Expand Down