网站搜索

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 端点完全跳过验证。虽然这不是问题的真正解决方案,但我实际上可以接受,它也回答了我的问题:“我如何才能让它工作?”。因此,这里以答案的形式给你,而不是评论。

相关文章