-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add request and response utils (#15)
* feat: add request and response utils * chore: use MIME.json * chore: stringify json body * chore: use dummy host for URL parser * test: add test * chore: types * improve utils * use use * update tests * move destr to devDependencies * refactor: improve error and body utils Co-authored-by: Pooya Parsa <pyapar@gmail.com>
- Loading branch information
Showing
10 changed files
with
595 additions
and
500 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import type { IncomingMessage } from 'http' | ||
|
||
import destr from 'destr' | ||
|
||
type Encoding = false | 'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex' | ||
|
||
export function useBody (req: IncomingMessage, encoding: Encoding = 'utf-8'): Encoding extends false ? Buffer : Promise<string> { | ||
// @ts-ignore | ||
if (req.rawBody) { | ||
// @ts-ignore | ||
return Promise.resolve(encoding ? req.rawBody.toString(encoding) : req.rawBody) | ||
} | ||
|
||
return new Promise<string>((resolve, reject) => { | ||
const bodyData: any[] = [] | ||
req | ||
.on('error', (err) => { reject(err) }) | ||
.on('data', (chunk) => { bodyData.push(chunk) }) | ||
.on('end', () => { | ||
// @ts-ignore | ||
req.rawBody = Buffer.concat(bodyData) | ||
// @ts-ignore | ||
resolve(encoding ? req.rawBody.toString(encoding) : req.rawBody) | ||
}) | ||
}) | ||
} | ||
|
||
export async function useBodyJSON<T> (req: IncomingMessage): Promise<T> { | ||
// @ts-ignore | ||
if (req.jsonBody) { | ||
// @ts-ignore | ||
return req.jsonBody | ||
} | ||
|
||
const body = await useBody(req) | ||
const json = destr(body) | ||
|
||
// @ts-ignore | ||
req.jsonBody = json | ||
|
||
return json | ||
} |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import type { ServerResponse } from 'http' | ||
import { MIMES } from './utils' | ||
|
||
export class H2Error extends Error { | ||
statusCode: number = 500 | ||
statusMessage: string = 'Internal Error' | ||
data?: any | ||
internal: boolean = false | ||
} | ||
|
||
export function createError (input: Partial<H2Error>): H2Error { | ||
if (input instanceof H2Error) { | ||
return input | ||
} | ||
const err = new H2Error() | ||
if (input.statusCode) { | ||
err.statusCode = input.statusCode | ||
} | ||
if (input.statusMessage) { | ||
err.statusMessage = input.statusMessage | ||
} | ||
if (input.data) { | ||
err.data = input.data | ||
} | ||
if (input.internal) { | ||
err.internal = input.internal | ||
} | ||
return err | ||
} | ||
|
||
export function sendError (res: ServerResponse, error: Error | H2Error, debug: boolean) { | ||
const h2Error = createError(error) | ||
|
||
// @ts-ignore | ||
if (h2Error.internal) { | ||
console.error(h2Error) // eslint-disable-line no-console | ||
} | ||
|
||
res.statusCode = h2Error.statusCode | ||
res.statusMessage = h2Error.statusMessage | ||
|
||
const responseBody = { | ||
statusCode: res.statusCode, | ||
statusMessage: res.statusMessage, | ||
stack: [] as string[], | ||
data: h2Error.data | ||
} | ||
|
||
if (debug) { | ||
responseBody.stack = (h2Error.stack || '').split('\n').map(l => l.trim()) | ||
} | ||
|
||
res.setHeader('Content-Type', MIMES.json) | ||
res.end(JSON.stringify(responseBody, null, 2)) | ||
} |
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import supertest, { SuperTest, Test } from 'supertest' | ||
import { createApp, App, useBody, useBodyJSON } from '../src' | ||
|
||
describe('', () => { | ||
let app: App | ||
let request: SuperTest<Test> | ||
|
||
beforeEach(() => { | ||
app = createApp({ debug: false }) | ||
request = supertest(app) | ||
}) | ||
|
||
describe('useBody', () => { | ||
it('can handle raw string', async () => { | ||
app.use('/', async (request) => { | ||
const body = await useBody(request) | ||
expect(body).toEqual('{"bool":true,"name":"string","number":1}') | ||
return '200' | ||
}) | ||
const result = await request.post('/api/test').send(JSON.stringify({ | ||
bool: true, | ||
name: 'string', | ||
number: 1 | ||
})) | ||
|
||
expect(result.text).toBe('200') | ||
}) | ||
}) | ||
|
||
describe('useJSON', () => { | ||
it('can parse json payload', async () => { | ||
app.use('/', async (request) => { | ||
const body = await useBodyJSON(request) | ||
expect(body).toMatchObject({ | ||
bool: true, | ||
name: 'string', | ||
number: 1 | ||
}) | ||
return '200' | ||
}) | ||
const result = await request.post('/api/test').send({ | ||
bool: true, | ||
name: 'string', | ||
number: 1 | ||
}) | ||
|
||
expect(result.text).toBe('200') | ||
}) | ||
}) | ||
}) |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
import supertest, { SuperTest, Test } from 'supertest' | ||
import { createApp, App, createError } from '../src' | ||
|
||
; (global.console.error as any) = jest.fn() | ||
|
||
describe('error', () => { | ||
let app: App | ||
let request: SuperTest<Test> | ||
|
||
beforeEach(() => { | ||
app = createApp({ debug: false }) | ||
request = supertest(app) | ||
}) | ||
|
||
describe('sendError', () => { | ||
it('logs errors', async () => { | ||
app.use((_req) => { | ||
throw createError({ statusMessage: 'Unprocessable', statusCode: 422 }) | ||
}) | ||
const result = await request.get('/') | ||
|
||
expect(result.status).toBe(422) | ||
}) | ||
|
||
it('returns errors', async () => { | ||
app.use((_req) => { | ||
throw createError({ statusMessage: 'Unprocessable', statusCode: 422 }) | ||
}) | ||
const result = await request.get('/') | ||
|
||
expect(result.status).toBe(422) | ||
}) | ||
}) | ||
|
||
describe('createError', () => { | ||
it('can send internal error', async () => { | ||
app.use('/', () => { | ||
throw createError({ | ||
statusCode: 500, | ||
statusMessage: 'Internal Error', | ||
data: 'oops', | ||
internal: true | ||
}) | ||
}) | ||
const result = await request.get('/api/test') | ||
|
||
expect(result.status).toBe(500) | ||
// eslint-disable-next-line | ||
expect(console.error).toBeCalled() | ||
|
||
expect(JSON.parse(result.text)).toMatchObject({ | ||
statusCode: 500, | ||
statusMessage: 'Internal Error' | ||
}) | ||
}) | ||
|
||
it('can send runtime error', async () => { | ||
jest.clearAllMocks() | ||
|
||
app.use('/', () => { | ||
throw createError({ | ||
statusCode: 400, | ||
statusMessage: 'Bad Request', | ||
data: { | ||
message: 'Invalid Input' | ||
} | ||
}) | ||
}) | ||
|
||
const result = await request.get('/api/test') | ||
|
||
expect(result.status).toBe(400) | ||
expect(result.type).toMatch('application/json') | ||
// eslint-disable-next-line | ||
expect(console.error).not.toBeCalled() | ||
|
||
expect(JSON.parse(result.text)).toMatchObject({ | ||
statusCode: 400, | ||
statusMessage: 'Bad Request', | ||
data: { | ||
message: 'Invalid Input' | ||
} | ||
}) | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.