Skip to content

Commit

Permalink
feat: add patchNode option to support patching ctx.req.body
Browse files Browse the repository at this point in the history
  • Loading branch information
3imed-jaberi committed Apr 16, 2023
1 parent 118546a commit 5a78111
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 32 deletions.
15 changes: 4 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,14 @@ app.use((ctx) => {

## Options

- **patchNode**: patch request body to Node's `ctx.req`, default is `false`.
- **enableTypes**: parser will only parse when request type hits enableTypes, support `json/form/text/xml`, default is `['json', 'form']`.
- **encoding**: requested encoding. Default is `utf-8` by `co-body`.
- **formLimit**: limit of the `urlencoded` body. If the body ends up being larger than this limit, a 413 error code is returned. Default is `56kb`.
- **jsonLimit**: limit of the `json` body. Default is `1mb`.
- **textLimit**: limit of the `text` body. Default is `1mb`.
- **xmlLimit**: limit of the `xml` body. Default is `1mb`.
- **strict**: when set to true, JSON parser will only accept arrays and objects. Default is `true`. See [strict mode](https://github.com/cojs/co-body#options) in `co-body`. In strict mode, `ctx.request.body` will always be an object(or array), this avoid lots of type judging. But text body will always return string type.
- **jsonStrict**: when set to true, JSON parser will only accept arrays and objects. Default is `true`. See [strict mode](https://github.com/cojs/co-body#options) in `co-body`. In strict mode, `ctx.request.body` will always be an object(or array), this avoid lots of type judging. But text body will always return string type.
- **detectJSON**: custom json request detect function. Default is `null`.

```js
Expand All @@ -77,12 +78,12 @@ app.use((ctx) => {
);
```

- **onerror**: support custom error handle, if `koa-bodyparser` throw an error, you can customize the response like:
- **onError**: support custom error handle, if `koa-bodyparser` throw an error, you can customize the response like:

```js
app.use(
bodyParser({
onerror(err, ctx) {
onError(err, ctx) {
ctx.throw(422, "body parse error");
},
})
Expand All @@ -91,14 +92,6 @@ app.use((ctx) => {

- **enableRawChecking**: support the already parsed body on the raw request by override and prioritize the parsed value over the sended payload. (default is `false`)

```js
app.use(
bodyParser({
enableRawChecking: true,
})
);
```

- **disableBodyParser**: you can dynamic disable body parser by set `ctx.disableBodyParser = true`.

```js
Expand Down
24 changes: 19 additions & 5 deletions src/body-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare module 'http' {
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface IncomingMessage {
body?: any;
rawBody: string;
}
}
/**
Expand All @@ -26,10 +27,11 @@ declare module 'http' {
export function bodyParserWrapper(opts: BodyParserOptions = {}) {
const {
detectJSON,
onerror,
onError,
enableTypes = ['json', 'form'],
extendTypes = {} as NonNullable<BodyParserOptions['extendTypes']>,
enableRawChecking = false,
patchNode = false,
...restOpts
} = opts;
const isEnabledBodyAs = getIsEnabledBodyAs(enableTypes);
Expand All @@ -55,7 +57,7 @@ export function bodyParserWrapper(opts: BodyParserOptions = {}) {
const parserOptions = {
// force co-body return raw body
returnRawBody: true,
strict: restOpts.strict,
strict: shouldParseBodyAs('json') ? restOpts.jsonStrict : undefined,
[`${bodyType}Types`]: mimeTypes[bodyType],
limit: restOpts[`${shouldParseBodyAs('xml') ? 'xml' : bodyType}Limit`],
};
Expand All @@ -66,7 +68,12 @@ export function bodyParserWrapper(opts: BodyParserOptions = {}) {
}

return async function (ctx: Koa.Context, next: Koa.Next) {
if (ctx.request.body !== undefined || ctx.disableBodyParser) return next();
if (
(patchNode && ctx.req.body !== undefined) ||
ctx.request.body !== undefined ||
ctx.disableBodyParser
)
return next();
// raw request parsed and contain 'body' values and it's enabled to override the koa request
if (enableRawChecking && ctx.req.body !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Expand All @@ -76,11 +83,18 @@ export function bodyParserWrapper(opts: BodyParserOptions = {}) {

try {
const response = await parseBody(ctx);
// patch node
if (patchNode) {
ctx.req.body = 'parsed' in response ? response.parsed : {};
if (ctx.req.rawBody === undefined) ctx.req.rawBody = response.raw;
}

// patch koa
ctx.request.body = 'parsed' in response ? response.parsed : {};
if (ctx.request.rawBody === undefined) ctx.request.rawBody = response.raw;
} catch (err: unknown) {
if (!onerror) throw err;
onerror(err as Error, ctx);
if (!onError) throw err;
onError(err as Error, ctx);
}

return next();
Expand Down
32 changes: 19 additions & 13 deletions src/body-parser.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,22 @@ export const supportedBodyTypes = ['json', 'form', 'text', 'xml'] as const;
export type BodyType = (typeof supportedBodyTypes)[number];

export type BodyParserOptions = {
/**
* Patch request body to Node's 'ctx.req'
* @default false
*/
patchNode?: boolean;
/**
* json detector function, can help to detect request json type based on custom logic
*/
detectJSON?: (ctx: Koa.Context) => boolean;
/**
* error handler, can help to customize the response on error case
*/
onerror?: (error: Error, ctx: Koa.Context) => void;
onError?: (error: Error, ctx: Koa.Context) => void;
/**
* False to disable the raw request body checking to prevent koa request override
* false to disable the raw request body checking to prevent koa request override
* @default false
*/
enableRawChecking?: boolean;
/**
Expand All @@ -32,15 +38,22 @@ export type BodyParserOptions = {
[K in BodyType]?: string[];
};
/**
* limit of the `urlencoded` body
* @default '56kb'
* When set to true, JSON parser will only accept arrays and objects.
* When false will accept anything JSON.parse accepts.
*
* @default true
*/
formLimit?: CoBodyOptions['limit'];
jsonStrict?: CoBodyOptions['strict'];
/**
* limit of the `json` body
* @default '1mb'
*/
jsonLimit?: CoBodyOptions['limit'];
/**
* limit of the `urlencoded` body
* @default '56kb'
*/
formLimit?: CoBodyOptions['limit'];
/**
* limit of the `text` body
* @default '1mb'
Expand All @@ -57,12 +70,5 @@ export type BodyParserOptions = {
* requested encoding.
* @default 'utf-8' by 'co-body'.
*/
| 'encoding'
/**
* When set to true, JSON parser will only accept arrays and objects.
* When false will accept anything JSON.parse accepts.
*
* @default true
*/
| 'strict'
'encoding'
>;
4 changes: 4 additions & 0 deletions src/body-parser.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import {
type BodyType,
} from './body-parser.types';

/**
* UnsupportedBodyTypeError
*/
export class UnsupportedBodyTypeError extends Error {
constructor(wrongType: string) {
super();
Expand Down Expand Up @@ -61,6 +64,7 @@ export function getMimeTypes(
'application/json-patch+json',
'application/vnd.api+json',
'application/csp-report',
'application/reports+json',
'application/scim+json',
],
// default form mime types
Expand Down
45 changes: 42 additions & 3 deletions test/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe("test/body-parser.test.ts", () => {
});

it("should json body ok with string not in strict mode", (done) => {
const app = createApp({ jsonLimit: 100, strict: false });
const app = createApp({ jsonLimit: 100, jsonStrict: false });
app.use(async (ctx) => {
expect(ctx.request.rawBody).toEqual('"valid"');
ctx.body = ctx.request.body;
Expand Down Expand Up @@ -315,6 +315,45 @@ describe("test/body-parser.test.ts", () => {
});
});

describe("patchNode", () => {
it("should patch Node raw request with supported type", (done) => {
const app = createApp({ patchNode: true });

app.use(async (ctx) => {
expect(ctx.request.body).toEqual({ foo: "bar" });
expect(ctx.request.rawBody).toEqual('{"foo":"bar"}');
expect(ctx.req.body).toEqual({ foo: "bar" });
expect(ctx.req.rawBody).toEqual('{"foo":"bar"}');

ctx.body = ctx.req.body;
});
server = app.listen();
request(server)
.post("/")
.send({ foo: "bar" })
.expect({ foo: "bar" }, done);
});

it("should patch Node raw request with unsupported type", (done) => {
const app = createApp({ patchNode: true });

app.use(async (ctx) => {
expect(ctx.request.body).toEqual({});
expect(ctx.request.rawBody).toEqual(undefined);
expect(ctx.req.body).toEqual({});
expect(ctx.req.rawBody).toEqual(undefined);

ctx.body = ctx.req.body;
});
server = app.listen();
request(server)
.post("/")
.type("application/x-unsupported-type")
.send("x-unsupported-type")
.expect({}, done);
});
});

describe("extend type", () => {
it("should extend json ok", (done) => {
const app = createApp({
Expand Down Expand Up @@ -449,9 +488,9 @@ describe("test/body-parser.test.ts", () => {
});
});

describe("onerror", () => {
describe("onError", () => {
const app = createApp({
onerror({}, ctx) {
onError({}, ctx) {
ctx.throw("custom parse error", 422);
},
});
Expand Down

0 comments on commit 5a78111

Please sign in to comment.