UNPKG

3.29 kBJavaScriptView Raw
1// Native
2const server = require('http').Server
3
4// Packages
5const getRawBody = require('raw-body')
6const typer = require('media-typer')
7const isStream = require('isstream')
8const Promise = require('bluebird')
9
10const {resolve} = Promise
11
12const DEV = process.env.NODE_ENV === 'development'
13const TESTING = process.env.NODE_ENV === 'test'
14
15const serve = fn => server((req, res) => exports.run(req, res, fn))
16
17module.exports = exports = serve
18
19exports.send = send
20exports.sendError = sendError
21exports.createError = createError
22
23exports.run = (req, res, fn) =>
24 resolve(fn(req, res))
25 .then(val => {
26 if (val === null) {
27 send(res, 204, null)
28 }
29
30 // Return a undefined-null value -> send
31 if (undefined !== val) {
32 send(res, res.statusCode || 200, val)
33 }
34 })
35 .catch(err => sendError(req, res, err))
36
37// maps requests to buffered raw bodies so that
38// multiple calls to `json` work as expected
39const rawBodyMap = new WeakMap()
40
41const returnJSON = (resolve, reject, str) => {
42 try {
43 resolve(JSON.parse(str))
44 } catch (err) {
45 reject(createError(400, 'Invalid JSON', err))
46 }
47}
48
49exports.json = (req, {limit = '1mb'} = {}) => new Promise((resolve, reject) => {
50 const type = req.headers['content-type']
51 const length = req.headers['content-length']
52 const encoding = typer.parse(type).parameters.charset
53
54 let str = rawBodyMap.get(req)
55
56 if (str) {
57 returnJSON(resolve, reject, str)
58 return
59 }
60
61 getRawBody(req, {limit, length, encoding}).then(buf => {
62 str = buf
63 rawBodyMap.set(req, str)
64
65 returnJSON(resolve, reject, str)
66 }).catch(err => {
67 if (err.type === 'entity.too.large') {
68 throw createError(413, `Body exceeded ${limit} limit`, err)
69 } else {
70 throw createError(400, 'Invalid body', err)
71 }
72 })
73})
74
75function send(res, code, obj = null) {
76 res.statusCode = code
77
78 if (obj === null) {
79 res.end()
80 return
81 }
82
83 if (Buffer.isBuffer(obj)) {
84 if (!res.getHeader('Content-Type')) {
85 res.setHeader('Content-Type', 'application/octet-stream')
86 }
87
88 res.setHeader('Content-Length', obj.length)
89 res.end(obj)
90 return
91 }
92
93 if (isStream(obj)) {
94 if (!res.getHeader('Content-Type')) {
95 res.setHeader('Content-Type', 'application/octet-stream')
96 }
97
98 obj.pipe(res)
99 return
100 }
101
102 let str = obj
103
104 if (typeof obj === 'object') {
105 // we stringify before setting the header
106 // in case `JSON.stringify` throws and a
107 // 500 has to be sent instead
108
109 // the `JSON.stringify` call is split into
110 // two cases as `JSON.stringify` is optimized
111 // in V8 if called with only one argument
112 if (DEV) {
113 str = JSON.stringify(obj, null, 2)
114 } else {
115 str = JSON.stringify(obj)
116 }
117 res.setHeader('Content-Type', 'application/json')
118 }
119
120 res.setHeader('Content-Length', Buffer.byteLength(str))
121 res.end(str)
122}
123
124function sendError(req, res, {statusCode, status, message, stack}) {
125 if (statusCode) {
126 send(res, statusCode || status, DEV ? stack : message)
127 } else {
128 send(res, 500, DEV ? stack : 'Internal Server Error')
129 }
130
131 if (!TESTING) {
132 console.error(stack)
133 }
134}
135
136function createError(code, msg, orig) {
137 const err = new Error(msg)
138 err.statusCode = code
139 err.originalError = orig
140 return err
141}