Spaces:
Running
Running
| import Koa from 'koa'; | |
| import KoaRouter from 'koa-router'; | |
| import koaRange from 'koa-range'; | |
| import koaCors from "koa2-cors"; | |
| import koaBody from 'koa-body'; | |
| import _ from 'lodash'; | |
| import Exception from './exceptions/Exception.ts'; | |
| import Request from './request/Request.ts'; | |
| import Response from './response/Response.js'; | |
| import FailureBody from './response/FailureBody.ts'; | |
| import EX from './consts/exceptions.ts'; | |
| import logger from './logger.ts'; | |
| import config from './config.ts'; | |
| class Server { | |
| app; | |
| router; | |
| constructor() { | |
| this.app = new Koa(); | |
| this.app.use(koaCors()); | |
| // 范围请求支持 | |
| this.app.use(koaRange); | |
| this.router = new KoaRouter({ prefix: config.service.urlPrefix }); | |
| // 前置处理异常拦截 | |
| this.app.use(async (ctx: any, next: Function) => { | |
| if(ctx.request.type === "application/xml" || ctx.request.type === "application/ssml+xml") | |
| ctx.req.headers["content-type"] = "text/xml"; | |
| try { await next() } | |
| catch (err) { | |
| logger.error(err); | |
| const failureBody = new FailureBody(err); | |
| new Response(failureBody).injectTo(ctx); | |
| } | |
| }); | |
| // 自定义 JSON 解析中间件 | |
| this.app.use(async (ctx: any, next: Function) => { | |
| if (ctx.is('application/json') && ['POST', 'PUT', 'PATCH'].includes(ctx.method)) { | |
| logger.debug('开始自定义 JSON 解析'); | |
| const chunks: Buffer[] = []; | |
| await new Promise((resolve, reject) => { | |
| ctx.req.on('data', (chunk: Buffer) => { | |
| chunks.push(chunk); | |
| }); | |
| ctx.req.on('end', () => { | |
| resolve(null); | |
| }); | |
| ctx.req.on('error', reject); | |
| }); | |
| const body = Buffer.concat(chunks).toString('utf8'); | |
| // 清理问题字符 | |
| let cleanedBody = body | |
| .replace(/\r\n/g, '\n') | |
| .replace(/\r/g, '\n') | |
| .replace(/\u00A0/g, ' ') | |
| .replace(/[\u2000-\u200B]/g, ' ') | |
| .replace(/\uFEFF/g, '') | |
| .trim(); | |
| // 预先处理尾随逗号问题 | |
| cleanedBody = cleanedBody.replace(/,(\s*[\r\n]*\s*[}\]])/g, '$1'); | |
| // 安全的JSON解析 | |
| let parsedBody; | |
| try { | |
| parsedBody = JSON.parse(cleanedBody); | |
| } catch (parseError) { | |
| logger.error(`JSON解析失败: ${parseError.message}`); | |
| logger.debug(`原始请求体: ${body.substring(0, 200)}...`); | |
| logger.debug(`清理后请求体: ${cleanedBody.substring(0, 200)}...`); | |
| // 尝试修复常见的JSON格式问题 | |
| let fixedBody = cleanedBody; | |
| try { | |
| // 修复单引号问题 | |
| fixedBody = fixedBody.replace(/'/g, '"'); | |
| // 修复属性名没有引号的问题 | |
| fixedBody = fixedBody.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":'); | |
| // 修复尾随逗号问题 - 更强的正则表达式 | |
| fixedBody = fixedBody.replace(/,(\s*[\r\n]*\s*[}\]])/g, '$1'); | |
| // 修复多个连续逗号 | |
| fixedBody = fixedBody.replace(/,+/g, ','); | |
| parsedBody = JSON.parse(fixedBody); | |
| logger.debug('JSON修复成功'); | |
| } catch (fixError) { | |
| logger.error(`JSON修复也失败: ${fixError.message}`); | |
| logger.debug(`修复后的JSON: ${fixedBody.substring(0, 200)}...`); | |
| throw new Error(`无效的JSON格式: ${parseError.message}`); | |
| } | |
| } | |
| logger.debug('JSON 解析成功,跳过 koa-body'); | |
| ctx.request.body = parsedBody; | |
| ctx.request.rawBody = cleanedBody; | |
| // 标记已处理,避免 koa-body 再次处理 | |
| ctx._jsonProcessed = true; | |
| } | |
| await next(); | |
| }); | |
| // 载荷解析器支持(只处理未被自定义解析器处理的请求) | |
| this.app.use(async (ctx: any, next: Function) => { | |
| if (!ctx._jsonProcessed) { | |
| await koaBody(Object.assign(_.clone(config.system.requestBody), { | |
| multipart: true, // 开启multipart文件上传 | |
| formidable: { | |
| maxFileSize: 100 * 1024 * 1024, // 限制最大100MB | |
| }, | |
| enableTypes: ['json', 'form', 'text', 'xml'] // 确保form类型被启用 | |
| }))(ctx, next); | |
| } else { | |
| await next(); | |
| } | |
| }); | |
| this.app.on("error", (err: any) => { | |
| // 忽略连接重试、中断、管道、取消错误 | |
| if (["ECONNRESET", "ECONNABORTED", "EPIPE", "ECANCELED"].includes(err.code)) return; | |
| logger.error(err); | |
| }); | |
| logger.success("Server initialized"); | |
| } | |
| /** | |
| * 附加路由 | |
| * | |
| * @param routes 路由列表 | |
| */ | |
| attachRoutes(routes: any[]) { | |
| routes.forEach((route: any) => { | |
| const prefix = route.prefix || ""; | |
| for (let method in route) { | |
| if(method === "prefix") continue; | |
| if (!_.isObject(route[method])) { | |
| logger.warn(`Router ${prefix} ${method} invalid`); | |
| continue; | |
| } | |
| for (let uri in route[method]) { | |
| this.router[method](`${prefix}${uri}`, async ctx => { | |
| const { request, response } = await this.#requestProcessing(ctx, route[method][uri]); | |
| if(response != null && config.system.requestLog) { | |
| if (ctx.request.url.endsWith('/ping')) { | |
| logger.debug(`<- ${request.method} ${request.url} ${response.time - request.time}ms`); | |
| } else { | |
| logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| logger.info(`Route ${config.service.urlPrefix || ""}${prefix} attached`); | |
| }); | |
| this.app.use(this.router.routes()); | |
| this.app.use((ctx: any) => { | |
| const request = new Request(ctx); | |
| logger.debug(`-> ${ctx.request.method} ${ctx.request.url} request is not supported - ${request.remoteIP || "unknown"}`); | |
| // const failureBody = new FailureBody(new Exception(EX.SYSTEM_NOT_ROUTE_MATCHING, "Request is not supported")); | |
| // const response = new Response(failureBody); | |
| const message = `[请求有误]: 请检查API文档确认正确的请求路径,当前请求为 ${ctx.request.method} -> ${ctx.request.url} 请纠正`; | |
| logger.warn(message); | |
| const failureBody = new FailureBody(new Error(message)); | |
| const response = new Response(failureBody); | |
| response.injectTo(ctx); | |
| if(config.system.requestLog) | |
| logger.info(`<- ${request.method} ${request.url} ${response.time - request.time}ms`); | |
| }); | |
| } | |
| /** | |
| * 请求处理 | |
| * | |
| * @param ctx 上下文 | |
| * @param routeFn 路由方法 | |
| */ | |
| #requestProcessing(ctx: any, routeFn: Function): Promise<any> { | |
| return new Promise(resolve => { | |
| const request = new Request(ctx); | |
| try { | |
| if(config.system.requestLog) { | |
| if (request.url === '/ping') { | |
| logger.debug(`-> ${request.method} ${request.url}`); | |
| } else { | |
| logger.info(`-> ${request.method} ${request.url}`); | |
| } | |
| } | |
| routeFn(request) | |
| .then(response => { | |
| try { | |
| if(!Response.isInstance(response)) { | |
| const _response = new Response(response); | |
| _response.injectTo(ctx); | |
| return resolve({ request, response: _response }); | |
| } | |
| response.injectTo(ctx); | |
| resolve({ request, response }); | |
| } | |
| catch(err) { | |
| logger.error(err); | |
| const failureBody = new FailureBody(err); | |
| const response = new Response(failureBody); | |
| response.injectTo(ctx); | |
| resolve({ request, response }); | |
| } | |
| }) | |
| .catch(err => { | |
| try { | |
| logger.error(err); | |
| const failureBody = new FailureBody(err); | |
| const response = new Response(failureBody); | |
| response.injectTo(ctx); | |
| resolve({ request, response }); | |
| } | |
| catch(err) { | |
| logger.error(err); | |
| const failureBody = new FailureBody(err); | |
| const response = new Response(failureBody); | |
| response.injectTo(ctx); | |
| resolve({ request, response }); | |
| } | |
| }); | |
| } | |
| catch(err) { | |
| logger.error(err); | |
| const failureBody = new FailureBody(err); | |
| const response = new Response(failureBody); | |
| response.injectTo(ctx); | |
| resolve({ request, response }); | |
| } | |
| }); | |
| } | |
| /** | |
| * 监听端口 | |
| */ | |
| async listen() { | |
| const host = config.service.host; | |
| const port = config.service.port; | |
| await Promise.all([ | |
| new Promise((resolve, reject) => { | |
| if(host === "0.0.0.0" || host === "localhost" || host === "127.0.0.1") | |
| return resolve(null); | |
| this.app.listen(port, "localhost", err => { | |
| if(err) return reject(err); | |
| resolve(null); | |
| }); | |
| }), | |
| new Promise((resolve, reject) => { | |
| this.app.listen(port, host, err => { | |
| if(err) return reject(err); | |
| resolve(null); | |
| }); | |
| }) | |
| ]); | |
| logger.success(`Server listening on port ${port} (${host})`); | |
| } | |
| } | |
| export default new Server(); | |