Skip to content

Instantly share code, notes, and snippets.

@3imed-jaberi
Last active May 9, 2020 00:19
Show Gist options
  • Select an option

  • Save 3imed-jaberi/d73ef042f5c1e30678b621df0aad2a3d to your computer and use it in GitHub Desktop.

Select an option

Save 3imed-jaberi/d73ef042f5c1e30678b621df0aad2a3d to your computer and use it in GitHub Desktop.
Lightweight HTTP/HTTPS client for Node.js πŸ”₯. Make request easy with pure modules ✨.
// ************************************************
// I was thinking about publish this package to
// npm, but I saw that's not necessary to do
// that, especially we have many wonderful pckgs
// that do the same thing.
// Like: Axios / GOT / Fetch implementations ..
// So, i want to share this code with community.
// ************************************************
import * as path from 'path';
import * as http from 'http';
import * as https from 'https';
import { URL } from 'url';
// Types Dec. ..
type HttpRequestMethod =
'GET' | 'get' |
'POST' | 'post' |
'PUT' | 'put' |
'PATCH' | 'patch' |
'HEAD' | 'head' |
'DELETE'| 'delete';
interface ModuleOptions {
method?: HttpRequestMethod,
usePath?: boolean
}
interface ResponseOptions extends https.RequestOptions {
maxBuffer?: number
}
// Globale Var. ..
const support = {
shortConetntType: ['json', 'form', 'buffer'],
compression: ['gzip', 'deflate'],
protocols: ['http:', 'https:']
};
// Prepare the output result ..
class Response {
private _response: http.IncomingMessage;
private _responseOptions: ResponseOptions;
private _body: Buffer;
private _headers: http.OutgoingHttpHeaders;
private _statusCode: number | undefined;
constructor (response: http.IncomingMessage, responseOptions: ResponseOptions) {
this._response = response;
this._responseOptions = responseOptions;
this._body = Buffer.alloc(0)
this._headers = response.headers
this._statusCode = response.statusCode
}
addChunk (chunk: Uint8Array): void {
this._body = Buffer.concat([this._body, chunk])
}
async json (): Promise<JSON> {
return JSON.parse(this._body.toString());
}
async text (): Promise<string> {
return this._body.toString();
}
buffer () {
return this._body;
}
}
class LightRequest {
private baseUrl: URL;
private method: HttpRequestMethod;
private usePath: boolean;
private data: unknown;
private timeoutTime: number | null;
private streamEnabled: boolean;
private requestHeaders: http.OutgoingHttpHeaders;
private responseOptions: ResponseOptions
// https://nodejs.org/dist/latest-v10.x/docs/api/http.html#http_http_request_url_options_callback ..
private requestOptions: http.ClientRequestArgs;
constructor(url: string | URL, Options?: ModuleOptions) {
Options = Options || {};
this.baseUrl = typeof url === 'string' ? new URL(url) : url;
this.method = Options.method || 'GET';
this.usePath = Options.usePath || false;
this.data = null;
this.timeoutTime = null;
this.streamEnabled = false;
this.requestHeaders = {};
this.requestOptions = {};
this.responseOptions = {
maxBuffer: 50 * 1000000 // 50 MB
}
return this;
}
query(queryKey: string | object, queryValue?: string): this {
if(typeof queryKey === 'string') {
if(!queryValue) throw new Error('Please add value for your key ...');
this.baseUrl.searchParams.append(queryKey, queryValue);
}else{
// here queryKey as queryObject ..
Object.keys(queryKey).forEach((queryParamKey) => {
this.baseUrl.searchParams.append(queryParamKey, queryKey[queryParamKey]);
});
}
return this;
}
path (pathName: string): this {
this.baseUrl.pathname = this.usePath ? pathName : path.join(this.baseUrl.pathname, pathName);
return this;
}
body (data: object): this {
this.data = JSON.stringify(data);
return this;
}
headers(headerKey: string | object, headerValue?: string): this {
if(typeof headerKey === 'string') {
if(!headerValue) throw new Error('Please add value for your key ...');
this.requestHeaders[headerKey.toLowerCase()] = headerValue;
}else{
// here headerKey as requestHeaders ..
Object.keys(headerKey).forEach((headerObjKey) => {
this.requestHeaders[headerObjKey.toLowerCase()] = headerKey[headerObjKey];
});
}
return this;
}
timeout(timeout: number): this {
this.timeoutTime = timeout;
return this;
}
option (name: string, value: string): this {
this.requestOptions[name] = value;
return this;
}
stream (): this {
this.streamEnabled = true;
return this;
}
send(): Promise<any>{
return new Promise((resolve, reject) => {
if(!support.protocols.includes(this.baseUrl.protocol)) throw new Error(`Bad URL protocol: ${this.baseUrl.protocol}, Http and Https only supported ..`);
if (this.data) {
if (!this.requestHeaders.hasOwnProperty('content-type')) this.requestHeaders['content-type'] = 'application/json';
}
const options = Object.assign({
'protocol': this.baseUrl.protocol,
'host': this.baseUrl.hostname,
'port': this.baseUrl.port,
'path': `${this.baseUrl.pathname}${this.baseUrl.search}`,
'method': this.method,
'headers': this.requestHeaders
}, this.requestOptions);
const requestHandler = (response: http.IncomingMessage) => {
let stream: http.IncomingMessage = response;
if (this.streamEnabled) return resolve(stream);
let responseOutput = new Response(response, this.responseOptions);
stream.on('data', (chunk) => {
responseOutput.addChunk(chunk);
if (this.responseOptions.maxBuffer && responseOutput.buffer().length > this.responseOptions.maxBuffer) {
stream.destroy();
return reject(`Received a response which was longer than acceptable when buffering. (${this.body.length} bytes)`);
}
})
.on('error', (error) => reject(error))
.on('end', () => resolve(responseOutput));
}
let request: http.ClientRequest = this.baseUrl.protocol === 'http:' ? http.request(options, requestHandler) : https.request(options, requestHandler);
if (this.timeoutTime) {
request.setTimeout(this.timeoutTime, () => {
request.abort();
// if (!this.streamEnabled) {
// reject(new Error('Timeout reached'))
// }
})
}
request.on('error', (error) => reject(error));
if (this.data) request.write(this.data)
request.end();
})
}
}
export default (baseUrl: string | URL, opts?: ModuleOptions): LightRequest => new LightRequest(baseUrl, opts);
// ============================================================================================================= //
// ***************** TEST ************************
// LightRequest.spec.ts
// ************************************************
// I was thinking about publish this package to
// npm, but I saw that's not necessary to do
// that, especially we have many wonderful pckgs
// that do the same thing.
// Like: Axios / GOT / Fetch implementations ..
// So, i want to share this code with community.
// ************************************************
// Pkgs
// yarn add -D typescript@3.8.3 ts-node@8.10.1 rimraf@3.0.2
// nyc@15.0.1 @types/node@13.13.5 mocha@7.1.2 @types/mocha@7.0.2
// chai@4.2.0 @types/chai@4.2.11 nock@12.0.3 @types/nock@11.1.0
// cross-env@7.0.2
// .mocharc.json
// {
// "extension": [
// "ts"
// ],
// "require": "ts-node/register",
// "reporter": "spec",
// "timeout": false,
// "watch-files": ["test/**/*.ts"]
// }
// tsconfig.json
// {
// "compilerOptions": {
// "suppressImplicitAnyIndexErrors": true,
// "module": "commonjs",
// "target": "es2016",
// "rootDir": "<YOURS>",
// "outDir": "<YOURS>,
// "strict": true,
// "allowSyntheticDefaultImports": true,
// "esModuleInterop": true,
// "forceConsistentCasingInFileNames": true
// }
// }
// scripts ..
// "prebuild": "rimraf dist",
// "build": "tsc -d",
// "pretest": "npm run build",
// "test": "cross-env NODE_ENV=test mocha",
// "precoverage": "rimraf coverage .nyc_output",
// "coverage": "nyc npm run test"
// ********************************************
// ** TEST: it's nto the best way but OK ^^. **
// ********************************************
import 'mocha'
import { expect } from 'chai'
import nock from 'nock'
import request from './LightRequest'
const baseUrl = 'https://www.example.com';
const toJSON = (input: object): JSON => JSON.parse(JSON.stringify(input));
describe('LightRequest Test', () => {
describe('Method verbs.', () => {
it('GET request', async () => {
const responseData = toJSON([
{ id: 1, username: 'Imed Jaberi' },
{ id: 2, username: 'Jawher Jaberi' }
]);
nock(baseUrl).get('/user').reply(200, responseData);
const data = await request(baseUrl).path('/user').send();
expect(await data.json()).to.deep.equal(responseData);
});
it('POST request', async () => {
const bodyData = {
id: 3,
username: 'Cha9chou9'
}
const responseData = toJSON({ created: true });
nock(baseUrl).post('/user', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(baseUrl, { method: 'POST' })
.path('/user')
.headers('content-type', 'application/json')
.body(bodyData)
.send();
expect(await data.json()).to.deep.equal(responseData);
});
it('PUT request', async () => {
const bodyData = {
id: 3,
username: 'Chaouki Ben Fredj'
}
const responseData = toJSON({ updated: true, data: { ...bodyData, username: 'Chaouki Ben Fredj' } });
nock(baseUrl).put('/user', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(baseUrl, { method: 'PUT' })
.path('/user')
.headers('content-type', 'application/json')
.body(bodyData)
.send();
expect(await data.json()).to.deep.equal(responseData);
});
it('DELETE request', async () => {
const bodyData = {
id: 1
}
const responseData = toJSON({ success: true });
nock(baseUrl).delete('/user', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(baseUrl, { method: 'DELETE' })
.path('/user')
.headers('content-type', 'application/json')
.body(bodyData)
.send();
expect(await data.json()).to.deep.equal(responseData);
});
});
describe('Headers', () => {
// by default the content type support json
it('Headers as Key Value string', async () => {
const bodyData = {
id: 3,
username: 'Cha9chou9'
}
const responseData = toJSON({ created: true });
nock(baseUrl).post('/user', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(baseUrl, { method: 'POST' })
.path('/user')
.headers('content-type', 'application/json')
.headers('x-author', 'imed jaberi')
.body(bodyData)
.send();
expect(await data.json()).to.deep.equal(responseData);
});
it('Headers as Key Value object', async () => {
const bodyData = {
id: 3,
username: 'Cha9chou9'
}
const responseData = toJSON({ created: true });
nock(baseUrl).post('/user', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(baseUrl, { method: 'POST' })
.path('/user')
.headers({
'content-type': 'application/json',
'x-author': 'imed jaberi'
})
.body(bodyData)
.send();
expect(await data.json()).to.deep.equal(responseData);
});
});
describe('Query/Search params', () => {
it('Query/Search as Key Value string', async () => {
const responseData = toJSON(
{
id: 1,
username: 'Imed Jaberi'
}
);
nock(baseUrl).get('/user?id=1').reply(200, responseData);
const data = await request(baseUrl).path('/user').query('id', '1').send();
expect(await data.json()).to.deep.equal(responseData);
});
it('Query/Search as Key Value object', async () => {
const responseData = toJSON(
{
id: 1,
username: 'Imed Jaberi'
}
);
nock(baseUrl).get('/user?id=1&anyOtherQuery=someValueQuery').reply(200, responseData);
const data = await request(baseUrl).path('/user').query({ 'id': '1', 'anyOtherQuery': 'someValueQuery' }).send();
expect(await data.json()).to.deep.equal(responseData);
});
});
describe('Path', () => {
it('Path as relative path', async () => {
const bodyData = {
id: 1,
techName: 'React JS'
}
const responseData = toJSON({ created: true });
nock(baseUrl).post('/tech', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(`${baseUrl}/user`, { method: 'POST' }) // default is false ..
.path('../tech')
.body(bodyData)
.send();
expect(await data.json()).to.deep.equal(responseData);
});
it('Path as independent path', async () => {
const bodyData = {
id: 1,
techName: 'React JS'
}
const responseData = toJSON({ created: true });
nock(baseUrl).post('/tech', JSON.stringify(bodyData)).reply(200, responseData);
const data = await request(`${baseUrl}/user`, { method: 'POST', usePath: true })
.path('/tech')
.headers( 'content-type', 'application/json')
.body(bodyData)
.send();
expect(await data.text()).to.equal(JSON.stringify(responseData));
});
});
describe('Body', () => {
it('Body as JSON', () => {
// all the other test use json as default.
expect(true).to.equal(true);
});
});
describe('Timeout', () => {
it('Timeout with pass request --stream', async () => {
const responseData = toJSON([
{ id: 1, username: 'Imed Jaberi'},
{ id: 2, username: 'Jawher Jaberi' }
]);
nock(baseUrl).get('/user').reply(200, responseData);
const stream = await request(baseUrl)
.path('/user')
.headers('content-type', 'application/json')
.stream()
.timeout(60000)
.send();
let streamData = Buffer.alloc(0);
stream.on('data', (chunk: Uint8Array) => {
streamData = Buffer.concat([streamData, chunk]);
});
stream.on('end', () => {
let expectPayload = JSON.parse(streamData.toString())
expect(expectPayload).to.deep.equal(responseData)
});
});
it('Timeout with stop request --stream', async () => {
const responseData = toJSON([
{ id: 1, username: 'Imed Jaberi'},
{ id: 2, username: 'Jawher Jaberi' }
]);
nock(baseUrl).get('/user').socketDelay(1).reply(200, responseData);
expect(
async () => await request(baseUrl)
.path('/user')
.headers('content-type', 'application/json')
.stream()
.timeout(1)
.send()
).to.Throw
});
});
describe('Stream', () => {
it('Stream .......', async () => {
const responseData = toJSON([
{ id: 1, username: 'Imed Jaberi'},
{ id: 2, username: 'Jawher Jaberi' }
]);
nock(baseUrl).get('/user').reply(200, responseData);
const stream = await request(baseUrl).path('/user').headers('content-type', 'application/json').stream().send();
let streamData = Buffer.alloc(0);
stream.on('data', (chunk: Uint8Array) => {
streamData = Buffer.concat([streamData, chunk]);
});
stream.on('end', () => {
let expectPayload = JSON.parse(streamData.toString())
expect(expectPayload).to.deep.equal(responseData)
});
});
});
describe('Option', () => {
it('Basic Opt ....', async () => {
const responseData = toJSON({ id: 1, username: 'Imed Jaberi' });
nock(baseUrl).get('/user/1').reply(200, responseData);
const data = await request(baseUrl).path('/user/1').headers('content-type', 'application/json').option('protocol ', 'https:').send();
expect(await data.json()).to.deep.equal(responseData);
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment