快速设置多部分/表单数据错误:“身体必须是对象”

发布于 2025-02-13 14:04:44 字数 2880 浏览 0 评论 0原文

我正在使用Fastify-multer和JSON模式提交可能包含文件的多部分表单数据。不管我做什么,factify都会给我一个不好的响应错误:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body must be object"
}

这是我的index.ts

const server = fastify();
server.register(require("@fastify/cors"));
server.register(multer.contentParser).after(() => {
    if (!isProdEnv) {
        server.register(require("@fastify/swagger"), {
            /* ... */
        });
    }
    server.register(require("@fastify/auth")).after(() => {
        server.decorate("authenticateRequest", authenticateRequest);
        server.decorate("requireAuthentication", requireAuthentication);
        server.addHook("preHandler", server.auth([server.authenticateRequest]));
        server.register(indexRouter);
        server.register(authRouter, { prefix: "/auth" });
        server.register(usersRouter, { prefix: "/users" });
        server.register(listsRouter, { prefix: "/lists" });
        server.register(postsRouter, { prefix: "/posts" });
        server.register(searchRouter, { prefix: "/search" });
        server.register(settingsRouter, { prefix: "/settings" });
    });
});
server.setErrorHandler((err, req, res) => {
    req.log.error(err.toString());
    res.status(500).send(err);
});

/posts/create/create endpoint:

const postsRouter = (server: FastifyInstance, options: FastifyPluginOptions, next: HookHandlerDoneFunction) => {
    server.post(
        "/create",
        {
            schema: {
                consumes: ["multipart/form-data"],
                body: {
                    content: {
                        type: "string"
                    },
                    media: {
                        type: "string",
                        format: "binary"
                    },
                    "media-description": {
                        type: "string"
                    }
                }
            },
            preHandler: [server.auth([server.requireAuthentication]), uploadMediaFileToCloud]
        },
        postsController.createPost
    );
    next();
};

export default postsRouter;

request curl:

curl -X 'POST' \
  'http://localhost:3072/posts/create' \
  -H 'accept: */*' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoYW5kbGUiOiJ1bGtrYSIsInVzZXJJZCI6IjYyNGQ5NmY4NzFhOTI2OGY2YzNjZWExZCIsImlhdCI6MTY1NzEwNTg5NCwiZXhwIjoxNjU3NDA1ODk0fQ.A5WO3M-NhDYGWkILQLVCPfv-Ve-e_Dlm1UYD2vj5UrQ' \
  -H 'Content-Type: multipart/form-data' \
  -F 'content=Test.' \
  -F '[email protected];type=image/png' \
  -F 'media-description=' \

为什么是这样不起作用?

I'm using fastify-multer and JSON Schema to submit multipart form data that may include a file. No matter what I do, Fastify keeps giving me a bad response error:

{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body must be object"
}

Here is my index.ts:

const server = fastify();
server.register(require("@fastify/cors"));
server.register(multer.contentParser).after(() => {
    if (!isProdEnv) {
        server.register(require("@fastify/swagger"), {
            /* ... */
        });
    }
    server.register(require("@fastify/auth")).after(() => {
        server.decorate("authenticateRequest", authenticateRequest);
        server.decorate("requireAuthentication", requireAuthentication);
        server.addHook("preHandler", server.auth([server.authenticateRequest]));
        server.register(indexRouter);
        server.register(authRouter, { prefix: "/auth" });
        server.register(usersRouter, { prefix: "/users" });
        server.register(listsRouter, { prefix: "/lists" });
        server.register(postsRouter, { prefix: "/posts" });
        server.register(searchRouter, { prefix: "/search" });
        server.register(settingsRouter, { prefix: "/settings" });
    });
});
server.setErrorHandler((err, req, res) => {
    req.log.error(err.toString());
    res.status(500).send(err);
});

And the /posts/create endpoint:

const postsRouter = (server: FastifyInstance, options: FastifyPluginOptions, next: HookHandlerDoneFunction) => {
    server.post(
        "/create",
        {
            schema: {
                consumes: ["multipart/form-data"],
                body: {
                    content: {
                        type: "string"
                    },
                    media: {
                        type: "string",
                        format: "binary"
                    },
                    "media-description": {
                        type: "string"
                    }
                }
            },
            preHandler: [server.auth([server.requireAuthentication]), uploadMediaFileToCloud]
        },
        postsController.createPost
    );
    next();
};

export default postsRouter;

Request CURL:

curl -X 'POST' \
  'http://localhost:3072/posts/create' \
  -H 'accept: */*' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJoYW5kbGUiOiJ1bGtrYSIsInVzZXJJZCI6IjYyNGQ5NmY4NzFhOTI2OGY2YzNjZWExZCIsImlhdCI6MTY1NzEwNTg5NCwiZXhwIjoxNjU3NDA1ODk0fQ.A5WO3M-NhDYGWkILQLVCPfv-Ve-e_Dlm1UYD2vj5UrQ' \
  -H 'Content-Type: multipart/form-data' \
  -F 'content=Test.' \
  -F '[email protected];type=image/png' \
  -F 'media-description=' \

Why is this not working?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

忆梦 2025-02-20 14:04:44

编辑2:显然,有一个非常简单的解决方案:在prevalidation挂钩中使用multer,而不是prehandler。因此,一个工作代码看起来像这样:

server.register(multer.contentParser).after(() => {
    server.register(
        (instance: FastifyInstance, options: FastifyPluginOptions, next: HookHandlerDoneFunction) => {
            instance.post(
                "/create",
                {
                    schema: {
                        consumes: ["multipart/form-data"],
                        body: {
                            type: "object",
                            properties: {
                                content: {
                                    type: "string"
                                },
                                media: {
                                    type: "string",
                                    format: "binary"
                                }
                            }
                        }
                    },
                    preValidation: multer({
                        limits: {
                            fileSize: 1024 * 1024 * 5
                        },
                        storage: multer.memoryStorage()
                    }).single("media")
                },
                (request: FastifyRequest, reply: FastifyReply) => {
                    const content = (request.body as any).content as string;
                    const file = (request as any).file as File;
                    if (file) {
                        delete file.buffer;
                    }
                    reply.send({
                        content,
                        file: JSON.stringify(file) || "No file selected"
                    });
                }
            );
            next();
        },
        { prefix: "/posts" }
    );
});

编辑:在下面发布答案后,我可以找到解决方案。为可能遇到同一问题的其他人更新我的答案。

首先,我从fastify-multer切换到@fastify/multipart。然后,我从媒体字段中删除了type属性。

media: {
    format: "binary"
}

之后,我在注册@fastify/multipart时添加了Option {AddTobody:true}

import fastifyMultipart from "@fastify/multipart";

server.register(fastifyMultipart, { addToBody: true }).after(() => { ... });

这些更改后,字段媒体request.body中可用。


旧答案:

似乎如今,我必须在这里回答自己的问题。无论如何,我弄清楚了发生了什么。 Fastify的内置模式验证与Multipart/form-data相关。我玩了架构规范,以确保情况如此。因此,我从所有路线中删除了模式验证。我的用例是从Expressjs移植API以进行快速化,所以我使用 Express-Oas-Generator 躺在周围。我用它来产生Swagger UI,一切正常。我希望Fastify将其行为共同解决,并解决这个问题。

EDIT 2: Apparently, there is a really easy solution for this: Use multer in the preValidation hook instead of preHandler. So, a piece of working code will look like this:

server.register(multer.contentParser).after(() => {
    server.register(
        (instance: FastifyInstance, options: FastifyPluginOptions, next: HookHandlerDoneFunction) => {
            instance.post(
                "/create",
                {
                    schema: {
                        consumes: ["multipart/form-data"],
                        body: {
                            type: "object",
                            properties: {
                                content: {
                                    type: "string"
                                },
                                media: {
                                    type: "string",
                                    format: "binary"
                                }
                            }
                        }
                    },
                    preValidation: multer({
                        limits: {
                            fileSize: 1024 * 1024 * 5
                        },
                        storage: multer.memoryStorage()
                    }).single("media")
                },
                (request: FastifyRequest, reply: FastifyReply) => {
                    const content = (request.body as any).content as string;
                    const file = (request as any).file as File;
                    if (file) {
                        delete file.buffer;
                    }
                    reply.send({
                        content,
                        file: JSON.stringify(file) || "No file selected"
                    });
                }
            );
            next();
        },
        { prefix: "/posts" }
    );
});

EDIT: After I posted the answer below, I was able to find a solution for this. Updating my answer for anyone else who might encounter the same issue.

First, I switched to @fastify/multipart from fastify-multer. Then I removed the type property from the media field.

media: {
    format: "binary"
}

After this, I added the option{ addToBody: true } when registering @fastify/multipart.

import fastifyMultipart from "@fastify/multipart";

server.register(fastifyMultipart, { addToBody: true }).after(() => { ... });

After these changes, the field media became available in request.body.


OLD ANSWER:

Seems like these days I have to answer my own questions here. Anyway, I figured out what's happening. Fastify's built-in schema validation doesn't play well with multipart/form-data. I played around with the schema specification to make sure that this is the case. So I removed schema validation from all routes. My use case here was porting an API from ExpressJS to Fastify, so I had a nice Swagger JSON spec generated using express-oas-generator lying around. I used that to generate Swagger UI and everything worked fine. I hope Fastify gets its act together and sorts out this issue.

¢好甜 2025-02-20 14:04:44

您现在可以使用@fastify/multipart来完成此操作,要求它将所有多部分表单字段连接到对象上,就像是JSON一样,使用魔术值“ keyValues”:

server.register(multipart, {
  attachFieldsToBody: "keyValues",
  ...
});

将文件标记为文件(s)在架构中键入“对象”:

const SCHEMA_API_UPLOAD = {
  consumes: [ "multipart/form-data" ],
  body: {
    type: "object",
    properties: {
      file: { type: "object" },
      description: { type: "string" },
    }
  }
};

然后,每个表单字段将是request.body对象的字段:

server.post("/upload", { schema: SCHEMA_API_UPLOAD }, async (request, reply) => {
    console.log(request.body);
});

您可以通过curl:

$ curl -v localhost:8080/upload -F description="something good" -F [email protected]

...

[20250109-20:02:50.501] INF (uy33kj8cjzw6) [127.0.0.1:48898] POST /upload
[20250109-20:02:50.502] DEB (uy33kj8cjzw6) starting multipart parsing
[20250109-20:02:50.502] TRA (uy33kj8cjzw6) Providing options to busboy
{
  description: 'something good',
  file: <Buffer ff d8 ff e0 ... 61483 more bytes>
}

You can now do this with @fastify/multipart by asking it to attach all multipart form fields to an object as if it was JSON, using the magic value "keyValues":

server.register(multipart, {
  attachFieldsToBody: "keyValues",
  ...
});

Mark the file(s) as type "object" in the schema:

const SCHEMA_API_UPLOAD = {
  consumes: [ "multipart/form-data" ],
  body: {
    type: "object",
    properties: {
      file: { type: "object" },
      description: { type: "string" },
    }
  }
};

Then each form field will be a field on the request.body object:

server.post("/upload", { schema: SCHEMA_API_UPLOAD }, async (request, reply) => {
    console.log(request.body);
});

And you can see the object via curl:

$ curl -v localhost:8080/upload -F description="something good" -F [email protected]

...

[20250109-20:02:50.501] INF (uy33kj8cjzw6) [127.0.0.1:48898] POST /upload
[20250109-20:02:50.502] DEB (uy33kj8cjzw6) starting multipart parsing
[20250109-20:02:50.502] TRA (uy33kj8cjzw6) Providing options to busboy
{
  description: 'something good',
  file: <Buffer ff d8 ff e0 ... 61483 more bytes>
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文