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