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 DEV = process.env.NODE_ENV === 'development'
11const TESTING = process.env.NODE_ENV === 'test'
12
13const serve = fn => server((req, res) => exports.run(req, res, fn))
14
15module.exports = serve
16exports = serve
17
18exports.send = send
19exports.sendError = sendError
20exports.createError = createError
21
22exports.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// Maps requests to buffered raw bodies so that
35// multiple calls to `json` work as expected
36const rawBodyMap = new WeakMap()
37
38const parseJSON = str => {
39 try {
40 return JSON.parse(str)
41 } catch (err) {
42 throw createError(400, 'Invalid JSON', err)
43 }
44}
45
46exports.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
71function 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 // We stringify before setting the header
102 // in case `JSON.stringify` throws and a
103 // 500 has to be sent instead
104
105 // the `JSON.stringify` call is split into
106 // two cases as `JSON.stringify` is optimized
107 // in V8 if called with only one argument
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
123function 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
137function createError(code, msg, orig) {
138 const err = new Error(msg)
139 err.statusCode = code
140 err.originalError = orig
141 return err
142}