Last active
May 9, 2020 00:19
-
-
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 β¨.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ************************************************ | |
| // 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