node.js - Express 文件上传无论如何都会出现“表单意外结束”
我正在尝试为我的 Express+no-stress+Typescript 应用添加一个简单的文件上传 API 端点,所以我从看起来比较简单的方式开始,也就是使用 express-fileupload。
我几乎是在当场就辞职了,一旦我意识到它和 Typescript 配合不好,因为这种访问上传文件的方式,Typescript 似乎非常不喜欢(以我的 <input> HTML 元素命名的变量对 Typescript 来说是未知的,因此代码根本无法编译)。关于这个问题,已经存在另一个提问,但没有得到任何合适的答案,而被接受的那个实际上没有回答问题的答案建议使用 multer,我也照做了。
所以我运行了npm uninstall express-fileupload,然后运行了npm install multer。我尝试了multer中间件的几种组合(甚至没有任何中间件,而是在/upload POST回调中直接调用multer),但我总是遇到“表单意外结束”错误。一直是这样,直到我发现这个未解决的问题并尝试将multer降级到1.4.3,这在某种程度上确实有效(我开始收到不同的错误),但最终这对我来说不是一个可接受的解决方案,因为降级带来的安全隐患。
然后我改用了 busboy。我仍然遇到了同样的“表单意外结束”错误。用“busboy”一起在谷歌搜索这个问题,把我带到了一个相当新的未回答的问题,在那里你能找到的最好回复是一个评论,指出 multer 和 express-fileupload 都在后台使用 busboy,并建议改用其中一个……
所以我又回到了 multer,因为显然不是 multer 的问题,而且 multer 看起来是大多数开发者实际上使用的。下面是我编写的相关代码。这是我从零生成的一个最小的 Express+no-stress+Typescript 应用的一部分,用于提供一个最小可复现的例子。然而,生成的脚手架不够简化,无法在这里贴出全部内容,所以我这里只放了我修改的部分,其余的内容可以通过生成一个新的 Express+no-stress+Typescript 项目或者克隆我的示例仓库获得。
/server/api/controllers/examples/router.ts 文件:
/* eslint-disable prettier/prettier */
import express from 'express';
import controller from './controller';
export default express
.Router()
.post('/:dir', controller.post.bind(controller));
/server/api/controllers/examples/controller.ts 文件:
/* eslint-disable prettier/prettier */
import { Request, Response } from 'express';
import path from 'path';
import multer from 'multer';
export class Controller {
filename(_req: Request, file: Express.Multer.File, cb: (error: Error|null, name: string) => void) {
console.log("file will be named " + file.originalname);
cb(null, file.originalname);
}
post(req: Request, res: Response): void {
console.log("Received POST request");
const storage = multer.diskStorage({
destination: path.join('/tmp', req.params.dir), // Trusting req.params.dir is insecure, but this is only an example
filename: this.filename
});
const upload = multer(
{ storage: storage,
limits: { fieldNameSize: 1000000,
fieldSize: 1000000
}
}).single('doc');
console.log("Multer is set to receive a single 'doc' upload");
upload(req, res, (err: any) => {
if (err) {
console.log("Upload failed with error: " + err.message);
res.status(422);
res.json(err.message);
res.end();
}
});
}
}
export default new Controller();
以及 /server/common/api.yml 文件,对于 Multer 或 Express 并不真正需要,但它让你可以使用 Swagger (http://localhost:3000) 测试文件上传端点
openapi: 3.0.1
info:
title: multertest
description: Multer Test
version: 1.0.0
servers:
- url: /api/v1
tags:
- name: Examples
description: Simple example endpoints
- name: Specification
description: The swagger API specification
paths:
/examples/{dir}:
post:
tags:
- Examples
description: Uploads a file
parameters:
- name: dir
in: path
description: the subdirectory where the uploaded file will be stored
required: true
schema:
type: string
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
doc:
type: string
format: binary
required: true
responses:
200:
description: ok
content: {}
/spec:
get:
tags:
- Specification
responses:
200:
description: Return the API specification
content: {}
最后,这里是 /server/common/server.ts 文件,我没有修改过它,但有人建议它可能是问题的原因。我确实尝试过设置更高的上传限制,并上传远小于设置限制的文件,但情况没有改变。
import express, { Application } from 'express';
import path from 'path';
import bodyParser from 'body-parser';
import http from 'http';
import os from 'os';
import cookieParser from 'cookie-parser';
import l from './logger';
import errorHandler from '../api/middlewares/error.handler';
import * as OpenApiValidator from 'express-openapi-validator';
const app = express();
export default class ExpressServer {
private routes: (app: Application) => void;
constructor() {
const root = path.normalize(__dirname + '/../..');
app.use(bodyParser.json({ limit: process.env.REQUEST_LIMIT || '100kb' }));
app.use(
bodyParser.urlencoded({
extended: true,
limit: process.env.REQUEST_LIMIT || '100kb',
})
);
app.use(bodyParser.text({ limit: process.env.REQUEST_LIMIT || '100kb' }));
app.use(cookieParser(process.env.SESSION_SECRET));
app.use(express.static(`${root}/public`));
const apiSpec = path.join(__dirname, 'api.yml');
const validateResponses = !!(
process.env.OPENAPI_ENABLE_RESPONSE_VALIDATION &&
process.env.OPENAPI_ENABLE_RESPONSE_VALIDATION.toLowerCase() === 'true'
);
app.use(process.env.OPENAPI_SPEC || '/spec', express.static(apiSpec));
app.use(
OpenApiValidator.middleware({
apiSpec,
validateResponses,
ignorePaths: /.*\/spec(\/|$)/,
})
);
}
router(routes: (app: Application) => void): ExpressServer {
routes(app);
app.use(errorHandler);
return this;
}
listen(port: number): Application {
const welcome = (p: number) => (): void =>
l.info(
`up and running in ${
process.env.NODE_ENV || 'development'
} @: ${os.hostname()} on port: ${p}}`
);
http.createServer(app).listen(port, welcome(port));
return app;
}
}
所以问题是:我如何让一个简单的文件上传在我的“Express no stress typescript”应用程序中工作?
答案#1
你的代码问题在于在处理 formdata 时,你的 express-openapi-validator 中间件和 multer 之间发生了冲突。这种冲突产生的原因是 express-openapi-validator 中间件使用 multer 来处理 formdata 内容,而你的代码中也在使用 multer 来处理相同的数据。
然而,好消息是,在你的 express 路由中,上传的文件已经可以在 req.body.files 中使用,因为 express-openapi-validator 使用 multer 来处理这些文件。
为了解决这个问题,你可以简单地使用 multer 的磁盘存储来处理上传的文件。磁盘存储用于指定上传文件的目标目录和文件名。下面的代码是你示例仓库中控制器的可运行版本:
/* eslint-disable prettier/prettier */
import {
Request,
Response
} from 'express';
import path from 'path';
import multer from 'multer';
import stream from 'stream';
export class Controller {
filename(
_req: Request,
file: Express.Multer.File,
cb: (error: Error | null, name: string) => void
) {
console.log('file will be named ' + file.originalname);
cb(null, file.originalname);
}
post(req: Request, res: Response): void {
console.log('Received POST request');
const storage = multer.diskStorage({
destination: path.join('tmp', req.params.dir), // Trusting req.params.dir is insecure, but this is only an example
filename: this.filename,
});
(req.files as Array < Express.Multer.File > ).forEach(
(file: Express.Multer.File) => {
storage._handleFile(
req, {
...file,
stream: new stream.PassThrough(),
},
(err: any) => {
if (err) {
console.log('Upload failed with error: ' + err.message);
res.status(422);
res.json(err.message);
res.end();
}
}
);
}
);
res.status(200);
res.json('OK');
res.end();
}
}
export default new Controller();答案#2
我找到了一种变通方法,例如,禁用上传端点的 express-openapi-validator(在我上面的问题中名为 /examples,但在我的实际项目中名为 /upload)。
在 server/common/server.ts 文件中,我修改了这一生成的代码行:
ignorePaths: /.*\/spec(\/|$)/,
进入
ignorePaths: (path: string) => path.endsWith('/spec') || path.indexOf('/upload') >= 0,
这基本上告诉 OpenApiValidator 对 /upload 端点完全跳过验证。虽然这不是问题的真正解决方案,但我实际上可以接受,它也回答了我的问题:“我如何才能让它工作?”。因此,这里以答案的形式给你,而不是评论。
