error_view.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. 'use strict';
  2. // modify from https://github.com/poppinss/youch/blob/develop/src/Youch/index.js
  3. const fs = require('fs');
  4. const path = require('path');
  5. const cookie = require('cookie');
  6. const Mustache = require('mustache');
  7. const stackTrace = require('stack-trace');
  8. const util = require('util');
  9. const { detectErrorMessage } = require('./utils');
  10. const startingSlashRegex = /\\|\//;
  11. class ErrorView {
  12. constructor(ctx, error, template) {
  13. this.codeContext = 5;
  14. this._filterHeaders = [ 'cookie', 'connection' ];
  15. this.ctx = ctx;
  16. this.error = error;
  17. this.request = ctx.request;
  18. this.app = ctx.app;
  19. this.assets = new Map();
  20. this.viewTemplate = template;
  21. }
  22. /**
  23. * get html error page
  24. *
  25. * @return {String} html page
  26. *
  27. * @memberOf ErrorView
  28. */
  29. toHTML() {
  30. const stack = this.parseError();
  31. const data = this.serializeData(stack, (frame, index) => {
  32. const serializedFrame = this.serializeFrame(frame);
  33. serializedFrame.classes = this.getFrameClasses(frame, index);
  34. return serializedFrame;
  35. });
  36. data.request = this.serializeRequest();
  37. data.appInfo = this.serializeAppInfo();
  38. return this.complieView(this.viewTemplate, data);
  39. }
  40. /**
  41. * compile view
  42. *
  43. * @param {String} tpl - template
  44. * @param {Object} locals - data used by template
  45. *
  46. * @return {String} html
  47. *
  48. * @memberOf ErrorView
  49. */
  50. complieView(tpl, locals) {
  51. return Mustache.render(tpl, locals);
  52. }
  53. /**
  54. * check if the frame is node native file.
  55. *
  56. * @param {Frame} frame - current frame
  57. * @return {Boolean} bool
  58. *
  59. * @memberOf ErrorView
  60. */
  61. isNode(frame) {
  62. if (frame.isNative()) {
  63. return true;
  64. }
  65. const filename = frame.getFileName() || '';
  66. return !path.isAbsolute(filename) && filename[0] !== '.';
  67. }
  68. /**
  69. * check if the frame is app modules.
  70. *
  71. * @param {Object} frame - current frame
  72. * @return {Boolean} bool
  73. *
  74. * @memberOf ErrorView
  75. */
  76. isApp(frame) {
  77. if (this.isNode(frame)) {
  78. return false;
  79. }
  80. const filename = frame.getFileName() || '';
  81. return !filename.includes('node_modules' + path.sep);
  82. }
  83. /**
  84. * cache file asserts
  85. *
  86. * @param {String} key - assert key
  87. * @param {String} value - assert content
  88. *
  89. * @memberOf ErrorView
  90. */
  91. setAssets(key, value) {
  92. this.assets.set(key, value);
  93. }
  94. /**
  95. * get cache file asserts
  96. *
  97. * @param {String} key - assert key
  98. *
  99. * @memberOf ErrorView
  100. */
  101. getAssets(key) {
  102. this.assets.get(key);
  103. }
  104. /**
  105. * get frame source
  106. *
  107. * @param {Object} frame - current frame
  108. * @return {Object} frame source
  109. *
  110. * @memberOf ErrorView
  111. */
  112. getFrameSource(frame) {
  113. const filename = frame.getFileName();
  114. const lineNumber = frame.getLineNumber();
  115. let contents = this.getAssets(filename);
  116. if (!contents) {
  117. contents = fs.existsSync(filename) ? fs.readFileSync(filename, 'utf8') : '';
  118. this.setAssets(filename, contents);
  119. }
  120. const lines = contents.split(/\r?\n/);
  121. return {
  122. pre: lines.slice(Math.max(0, lineNumber - (this.codeContext + 1)), lineNumber - 1),
  123. line: lines[lineNumber - 1],
  124. post: lines.slice(lineNumber, lineNumber + this.codeContext),
  125. };
  126. }
  127. /**
  128. * parse error and return frame stack
  129. *
  130. * @return {Array} frame
  131. *
  132. * @memberOf ErrorView
  133. */
  134. parseError() {
  135. const stack = stackTrace.parse(this.error);
  136. return stack.map(frame => {
  137. if (!this.isNode(frame)) {
  138. frame.context = this.getFrameSource(frame);
  139. }
  140. return frame;
  141. });
  142. }
  143. /**
  144. * get stack context
  145. *
  146. * @param {Object} frame - current frame
  147. * @return {Object} context
  148. *
  149. * @memberOf ErrorView
  150. */
  151. getContext(frame) {
  152. if (!frame.context) {
  153. return {};
  154. }
  155. return {
  156. start: frame.getLineNumber() - (frame.context.pre || []).length,
  157. pre: frame.context.pre.join('\n'),
  158. line: frame.context.line,
  159. post: frame.context.post.join('\n'),
  160. };
  161. }
  162. /**
  163. * get frame classes, let view identify the frame
  164. *
  165. * @param {any} frame - current frame
  166. * @param {any} index - current index
  167. * @return {String} classes
  168. *
  169. * @memberOf ErrorView
  170. */
  171. getFrameClasses(frame, index) {
  172. const classes = [];
  173. if (index === 0) {
  174. classes.push('active');
  175. }
  176. if (!this.isApp(frame)) {
  177. classes.push('native-frame');
  178. }
  179. return classes.join(' ');
  180. }
  181. /**
  182. * serialize frame and return meaningful data
  183. *
  184. * @param {Object} frame - current frame
  185. * @return {Object} frame result
  186. *
  187. * @memberOf ErrorView
  188. */
  189. serializeFrame(frame) {
  190. const filename = frame.getFileName();
  191. const relativeFileName = filename.includes(process.cwd())
  192. ? filename.replace(process.cwd(), '').replace(startingSlashRegex, '')
  193. : filename;
  194. const extname = path.extname(filename).replace('.', '');
  195. return {
  196. extname,
  197. file: relativeFileName,
  198. method: frame.getFunctionName(),
  199. line: frame.getLineNumber(),
  200. column: frame.getColumnNumber(),
  201. context: this.getContext(frame),
  202. };
  203. }
  204. /**
  205. * serialize base data
  206. *
  207. * @param {Object} stack - frame stack
  208. * @param {Function} frameFomatter - frame fomatter function
  209. * @return {Object} data
  210. *
  211. * @memberOf ErrorView
  212. */
  213. serializeData(stack, frameFomatter) {
  214. const code = this.error.code || this.error.type;
  215. let message = detectErrorMessage(this.ctx, this.error);
  216. if (code) {
  217. message = `${message} (code: ${code})`;
  218. }
  219. return {
  220. code,
  221. message,
  222. name: this.error.name,
  223. status: this.error.status,
  224. frames: stack instanceof Array ? stack.filter(frame => frame.getFileName()).map(frameFomatter) : [],
  225. };
  226. }
  227. /**
  228. * serialize request object
  229. *
  230. * @return {Object} request object
  231. *
  232. * @memberOf ErrorView
  233. */
  234. serializeRequest() {
  235. const headers = [];
  236. Object.keys(this.request.headers).forEach(key => {
  237. if (this._filterHeaders.includes(key)) {
  238. return;
  239. }
  240. headers.push({
  241. key,
  242. value: this.request.headers[key],
  243. });
  244. });
  245. const parsedCookies = cookie.parse(this.request.headers.cookie || '');
  246. const cookies = Object.keys(parsedCookies).map(key => {
  247. return { key, value: parsedCookies[key] };
  248. });
  249. return {
  250. url: this.request.url,
  251. httpVersion: this.request.httpVersion,
  252. method: this.request.method,
  253. connection: this.request.headers.connection,
  254. headers,
  255. cookies,
  256. };
  257. }
  258. /**
  259. * serialize app info object
  260. *
  261. * @return {Object} egg app info
  262. *
  263. * @memberOf ErrorView
  264. */
  265. serializeAppInfo() {
  266. return {
  267. baseDir: this.app.config.baseDir,
  268. config: util.inspect(this.app.config),
  269. };
  270. }
  271. }
  272. module.exports = ErrorView;