mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-24 23:09:10 -05:00
A lot of Net functions returned Promises that would not reliably resolve/reject if a Net request failed. This fixes it so they should now all reliably reject on request failure. (Yes, this fixes the ladder issues and memory leak in Main.)
234 lines
6.3 KiB
TypeScript
234 lines
6.3 KiB
TypeScript
/**
|
|
* Net - abstraction layer around Node's HTTP/S request system.
|
|
* Advantages:
|
|
* - easier acquiring of data
|
|
* - mass disabling of outgoing requests via Config.
|
|
*/
|
|
|
|
import * as https from 'https';
|
|
import * as http from 'http';
|
|
import * as url from 'url';
|
|
import * as Streams from './streams';
|
|
|
|
export interface PostData {
|
|
[key: string]: string | number;
|
|
}
|
|
export interface NetRequestOptions extends https.RequestOptions {
|
|
body?: string | PostData;
|
|
writable?: boolean;
|
|
query?: PostData;
|
|
}
|
|
|
|
export class HttpError extends Error {
|
|
statusCode?: number;
|
|
body: string;
|
|
constructor(message: string, statusCode: number | undefined, body: string) {
|
|
super(message);
|
|
this.name = 'HttpError';
|
|
this.statusCode = statusCode;
|
|
this.body = body;
|
|
Error.captureStackTrace(this, HttpError);
|
|
}
|
|
}
|
|
|
|
export class NetStream extends Streams.ReadWriteStream {
|
|
opts: NetRequestOptions | null;
|
|
uri: string;
|
|
request: http.ClientRequest;
|
|
/** will be a Promise before the response is received, and the response itself after */
|
|
response: Promise<http.IncomingMessage | null> | http.IncomingMessage | null;
|
|
statusCode: number | null;
|
|
/** response headers */
|
|
headers: http.IncomingHttpHeaders | null;
|
|
state: 'pending' | 'open' | 'timeout' | 'success' | 'error';
|
|
|
|
constructor(uri: string, opts: NetRequestOptions | null = null) {
|
|
super();
|
|
this.statusCode = null;
|
|
this.headers = null;
|
|
this.uri = uri;
|
|
this.opts = opts;
|
|
// make request
|
|
this.response = null;
|
|
this.state = 'pending';
|
|
this.request = this.makeRequest(opts);
|
|
}
|
|
makeRequest(opts: NetRequestOptions | null) {
|
|
if (!opts) opts = {};
|
|
let body = opts.body;
|
|
if (body && typeof body !== 'string') {
|
|
if (!opts.headers) opts.headers = {};
|
|
if (!opts.headers['Content-Type']) {
|
|
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
}
|
|
body = NetStream.encodeQuery(body);
|
|
}
|
|
|
|
if (opts.query) {
|
|
this.uri += (this.uri.includes('?') ? '&' : '?') + NetStream.encodeQuery(opts.query);
|
|
}
|
|
|
|
if (body) {
|
|
if (!opts.headers) opts.headers = {};
|
|
if (!opts.headers['Content-Length']) {
|
|
opts.headers['Content-Length'] = Buffer.byteLength(body);
|
|
}
|
|
}
|
|
|
|
const protocol = url.parse(this.uri).protocol as string;
|
|
const net = protocol === 'https:' ? https : http;
|
|
|
|
let resolveResponse: ((value: http.IncomingMessage | null) => void) | null;
|
|
this.response = new Promise(resolve => {
|
|
resolveResponse = resolve;
|
|
});
|
|
|
|
const request = net.request(this.uri, opts, response => {
|
|
this.state = 'open';
|
|
this.nodeReadableStream = response;
|
|
this.response = response;
|
|
this.statusCode = response.statusCode || null;
|
|
this.headers = response.headers;
|
|
|
|
response.setEncoding('utf-8');
|
|
resolveResponse!(response);
|
|
resolveResponse = null;
|
|
|
|
response.on('data', data => {
|
|
this.push(data);
|
|
});
|
|
response.on('end', () => {
|
|
if (this.state === 'open') this.state = 'success';
|
|
this.pushEnd();
|
|
});
|
|
});
|
|
request.on('close', () => {
|
|
if (!this.atEOF) {
|
|
this.state = 'error';
|
|
this.pushError(new Error("Unexpected connection close"));
|
|
}
|
|
if (resolveResponse) {
|
|
this.response = null;
|
|
resolveResponse(null);
|
|
resolveResponse = null;
|
|
}
|
|
});
|
|
request.on('error', error => {
|
|
if (!this.atEOF) this.pushError(error, true);
|
|
});
|
|
if (opts.timeout || opts.timeout === undefined) {
|
|
request.setTimeout(opts.timeout || 5000, () => {
|
|
this.state = 'timeout';
|
|
this.pushError(new Error("Request timeout"));
|
|
request.abort();
|
|
});
|
|
}
|
|
|
|
if (body) {
|
|
request.write(body);
|
|
request.end();
|
|
if (opts.writable) {
|
|
throw new Error(`options.body is what you would have written to a NetStream - you must choose one or the other`);
|
|
}
|
|
} else if (opts.writable) {
|
|
this.nodeWritableStream = request;
|
|
} else {
|
|
request.end();
|
|
}
|
|
|
|
return request;
|
|
}
|
|
static encodeQuery(data: PostData) {
|
|
let out = '';
|
|
for (const key in data) {
|
|
if (out) out += `&`;
|
|
out += `${key}=${encodeURIComponent('' + data[key])}`;
|
|
}
|
|
return out;
|
|
}
|
|
_write(data: string | Buffer): Promise<void> | void {
|
|
if (!this.nodeWritableStream) {
|
|
throw new Error("You must specify opts.writable to write to a request.");
|
|
}
|
|
const result = this.nodeWritableStream.write(data);
|
|
if (result !== false) return undefined;
|
|
if (!this.drainListeners.length) {
|
|
this.nodeWritableStream.once('drain', () => {
|
|
for (const listener of this.drainListeners) listener();
|
|
this.drainListeners = [];
|
|
});
|
|
}
|
|
return new Promise(resolve => {
|
|
this.drainListeners.push(resolve);
|
|
});
|
|
}
|
|
_read() {
|
|
this.nodeReadableStream?.resume();
|
|
}
|
|
_pause() {
|
|
this.nodeReadableStream?.pause();
|
|
}
|
|
}
|
|
export class NetRequest {
|
|
uri: string;
|
|
constructor(uri: string) {
|
|
this.uri = uri;
|
|
}
|
|
/**
|
|
* Makes a http/https get request to the given link and returns a stream.
|
|
* The request data itself can be read with ReadStream#readAll().
|
|
* The NetStream class also holds headers and statusCode as a property.
|
|
*
|
|
* @param opts request opts - headers, etc.
|
|
* @param body POST body
|
|
*/
|
|
getStream(opts: NetRequestOptions = {}) {
|
|
if (typeof Config !== 'undefined' && Config.noNetRequests) {
|
|
throw new Error(`Net requests are disabled.`);
|
|
}
|
|
const stream = new NetStream(this.uri, opts);
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* Makes a basic http/https request to the URI.
|
|
* Returns the response data.
|
|
*
|
|
* Will throw if the response code isn't 200 OK.
|
|
*
|
|
* @param opts request opts - headers, etc.
|
|
*/
|
|
async get(opts: NetRequestOptions = {}): Promise<string> {
|
|
const stream = this.getStream(opts);
|
|
const response = await stream.response;
|
|
if (response && response.statusCode !== 200) {
|
|
throw new HttpError(response.statusMessage || "Connection error", response.statusCode, await stream.readAll());
|
|
}
|
|
return stream.readAll();
|
|
}
|
|
|
|
/**
|
|
* Makes a http/https POST request to the given link.
|
|
* @param opts request opts - headers, etc.
|
|
* @param body POST body
|
|
*/
|
|
post(opts: Omit<NetRequestOptions, 'body'>, body: PostData | string): Promise<string>;
|
|
/**
|
|
* Makes a http/https POST request to the given link.
|
|
* @param opts request opts - headers, etc.
|
|
*/
|
|
post(opts?: NetRequestOptions): Promise<string>;
|
|
post(opts: NetRequestOptions = {}, body?: PostData | string) {
|
|
if (!body) body = opts.body;
|
|
return this.get({
|
|
...opts,
|
|
method: 'POST',
|
|
body,
|
|
});
|
|
}
|
|
}
|
|
|
|
export function Net(uri: string) {
|
|
return new NetRequest(uri);
|
|
}
|