@wooksjs/http-body 0.6.6 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -52
- package/dist/index.cjs +111 -109
- package/dist/index.d.ts +10 -11
- package/dist/index.mjs +112 -110
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**!!! This is work-in-progress library, breaking changes are expected !!!**
|
|
1
|
+
# @wooksjs/http-body
|
|
4
2
|
|
|
5
3
|
<p align="center">
|
|
6
4
|
<img src="../../wooks-logo.png" width="450px"><br>
|
|
@@ -9,60 +7,18 @@
|
|
|
9
7
|
</a>
|
|
10
8
|
</p>
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Supported content types:
|
|
15
|
-
|
|
16
|
-
- ✅ application/json
|
|
17
|
-
- ✅ text/\*
|
|
18
|
-
- ✅ multipart/form-data
|
|
19
|
-
- ✅ application/x-www-form-urlencoded
|
|
20
|
-
|
|
21
|
-
Body parser does not parse every request's body. The parsing happens only when you call `parseBody` function.
|
|
10
|
+
Composable body parser for [@wooksjs/event-http](https://github.com/wooksjs/wooksjs/tree/main/packages/event-http). Supports JSON, text, multipart/form-data, and URL-encoded content types. Parsing is lazy — the body is only read when you call `parseBody()`.
|
|
22
11
|
|
|
23
12
|
## Installation
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { useBody } from '@wooksjs/http-body'
|
|
31
|
-
app.post('test', async () => {
|
|
32
|
-
const { parseBody } = useBody()
|
|
33
|
-
const data = await parseBody()
|
|
34
|
-
})
|
|
14
|
+
```sh
|
|
15
|
+
npm install @wooksjs/http-body
|
|
35
16
|
```
|
|
36
17
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```ts
|
|
40
|
-
import { useBody } from '@wooksjs/http-body'
|
|
41
|
-
app.post('test', async () => {
|
|
42
|
-
const {
|
|
43
|
-
isJson, // checks if content-type is "application/json" : () => boolean;
|
|
44
|
-
isHtml, // checks if content-type is "text/html" : () => boolean;
|
|
45
|
-
isXml, // checks if content-type is "application/xml" : () => boolean;
|
|
46
|
-
isText, // checks if content-type is "text/plain" : () => boolean;
|
|
47
|
-
isBinary, // checks if content-type is binary : () => boolean;
|
|
48
|
-
isFormData, // checks if content-type is "multipart/form-data" : () => boolean;
|
|
49
|
-
isUrlencoded, // checks if content-type is "application/x-www-form-urlencoded" : () => boolean;
|
|
50
|
-
isCompressed, // checks content-encoding : () => boolean | undefined;
|
|
51
|
-
contentEncodings, // returns an array of encodings : () => string[];
|
|
52
|
-
parseBody, // parses body according to content-type : <T = unknown>() => Promise<T>;
|
|
53
|
-
rawBody, // returns raw body Buffer : () => Promise<Buffer>;
|
|
54
|
-
} = useBody()
|
|
55
|
-
|
|
56
|
-
// the handler got the control, but the body isn't loaded yet
|
|
57
|
-
//...
|
|
58
|
-
|
|
59
|
-
console.log(await parseBody())
|
|
18
|
+
## Documentation
|
|
60
19
|
|
|
61
|
-
|
|
62
|
-
// ...
|
|
63
|
-
})
|
|
64
|
-
```
|
|
20
|
+
For full documentation, visit [wooks.moost.org/webapp/body](https://wooks.moost.org/webapp/body).
|
|
65
21
|
|
|
66
|
-
##
|
|
22
|
+
## License
|
|
67
23
|
|
|
68
|
-
|
|
24
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
21
21
|
}) : target, mod));
|
|
22
22
|
|
|
23
23
|
//#endregion
|
|
24
|
+
const __wooksjs_event_core = __toESM(require("@wooksjs/event-core"));
|
|
24
25
|
const __wooksjs_event_http = __toESM(require("@wooksjs/event-http"));
|
|
25
26
|
|
|
26
27
|
//#region packages/http-body/src/utils/safe-json.ts
|
|
@@ -29,137 +30,138 @@ const ILLEGAL_KEYS = [
|
|
|
29
30
|
"constructor",
|
|
30
31
|
"prototype"
|
|
31
32
|
];
|
|
32
|
-
const illigalKeySet = new Set(ILLEGAL_KEYS);
|
|
33
33
|
function safeJsonParse(src) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
34
|
+
const parsed = JSON.parse(src);
|
|
35
|
+
assertNoProtoKeys(parsed);
|
|
36
|
+
return parsed;
|
|
38
37
|
}
|
|
39
|
-
function
|
|
40
|
-
if (
|
|
38
|
+
function assertNoProtoKeys(obj) {
|
|
39
|
+
if (obj === null || typeof obj !== "object") return;
|
|
40
|
+
if (Array.isArray(obj)) {
|
|
41
|
+
for (const item of obj) assertNoProtoKeys(item);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const record = obj;
|
|
45
|
+
for (const key of Object.keys(record)) {
|
|
46
|
+
if (key === ILLEGAL_KEYS[0] || key === ILLEGAL_KEYS[1] || key === ILLEGAL_KEYS[2]) throw new __wooksjs_event_http.HttpError(400, `Illegal key name "${key}"`);
|
|
47
|
+
assertNoProtoKeys(record[key]);
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
//#endregion
|
|
44
52
|
//#region packages/http-body/src/body.ts
|
|
53
|
+
const CONTENT_TYPE_MAP = {
|
|
54
|
+
json: "application/json",
|
|
55
|
+
html: "text/html",
|
|
56
|
+
xml: "text/xml",
|
|
57
|
+
text: "text/plain",
|
|
58
|
+
binary: "application/octet-stream",
|
|
59
|
+
"form-data": "multipart/form-data",
|
|
60
|
+
urlencoded: "application/x-www-form-urlencoded"
|
|
61
|
+
};
|
|
62
|
+
const contentIsSlot = (0, __wooksjs_event_core.cachedBy)((type, ctx) => {
|
|
63
|
+
const contentType = (0, __wooksjs_event_http.useHeaders)(ctx)["content-type"] || "";
|
|
64
|
+
const mime = CONTENT_TYPE_MAP[type] || type;
|
|
65
|
+
return contentType.includes(mime);
|
|
66
|
+
});
|
|
67
|
+
const parsedBodySlot = (0, __wooksjs_event_core.cached)(async (ctx) => {
|
|
68
|
+
const { rawBody } = (0, __wooksjs_event_http.useRequest)(ctx);
|
|
69
|
+
const contentType = (0, __wooksjs_event_http.useHeaders)(ctx)["content-type"] || "";
|
|
70
|
+
const contentIs = (type) => contentType.includes(type);
|
|
71
|
+
const body = await rawBody();
|
|
72
|
+
const sBody = body.toString();
|
|
73
|
+
if (contentIs("application/json")) return jsonParser(sBody);
|
|
74
|
+
else if (contentIs("multipart/form-data")) return formDataParser(sBody, contentType);
|
|
75
|
+
else if (contentIs("application/x-www-form-urlencoded")) return urlEncodedParser(sBody);
|
|
76
|
+
else return sBody;
|
|
77
|
+
});
|
|
78
|
+
function jsonParser(v) {
|
|
79
|
+
try {
|
|
80
|
+
return safeJsonParse(v);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw new __wooksjs_event_http.HttpError(400, error.message);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function formDataParser(v, contentType) {
|
|
86
|
+
const MAX_PARTS = 255;
|
|
87
|
+
const MAX_KEY_LENGTH = 100;
|
|
88
|
+
const MAX_VALUE_LENGTH = 100 * 1024;
|
|
89
|
+
const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`;
|
|
90
|
+
if (!boundary) throw new __wooksjs_event_http.HttpError(__wooksjs_event_http.EHttpStatusCode.BadRequest, "form-data boundary not recognized");
|
|
91
|
+
const parts = v.trim().split(boundary);
|
|
92
|
+
const result = Object.create(null);
|
|
93
|
+
let key = "";
|
|
94
|
+
let partContentType = "text/plain";
|
|
95
|
+
let partCount = 0;
|
|
96
|
+
for (const part of parts) {
|
|
97
|
+
parsePart();
|
|
98
|
+
key = "";
|
|
99
|
+
partContentType = "text/plain";
|
|
100
|
+
if (!part.trim() || part.trim() === "--") continue;
|
|
101
|
+
partCount++;
|
|
102
|
+
if (partCount > MAX_PARTS) throw new __wooksjs_event_http.HttpError(413, "Too many form fields");
|
|
103
|
+
let valueMode = false;
|
|
104
|
+
const lines = part.trim().split(/\n/u).map((l) => l.trim());
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (valueMode) {
|
|
107
|
+
if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new __wooksjs_event_http.HttpError(413, `Field "${key}" is too large`);
|
|
108
|
+
result[key] = (result[key] ? `${result[key]}\n` : "") + line;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!line) {
|
|
112
|
+
valueMode = !!key;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (line.toLowerCase().startsWith("content-disposition: form-data;")) {
|
|
116
|
+
key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? "";
|
|
117
|
+
if (!key) throw new __wooksjs_event_http.HttpError(400, `Could not read multipart name: ${line}`);
|
|
118
|
+
if (key.length > MAX_KEY_LENGTH) throw new __wooksjs_event_http.HttpError(413, "Field name too long");
|
|
119
|
+
if ([
|
|
120
|
+
"__proto__",
|
|
121
|
+
"constructor",
|
|
122
|
+
"prototype"
|
|
123
|
+
].includes(key)) throw new __wooksjs_event_http.HttpError(400, `Illegal key name "${key}"`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (line.toLowerCase().startsWith("content-type:")) {
|
|
127
|
+
partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? "";
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
parsePart();
|
|
133
|
+
return result;
|
|
134
|
+
function parsePart() {
|
|
135
|
+
if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function urlEncodedParser(v) {
|
|
139
|
+
return new __wooksjs_event_http.WooksURLSearchParams(v.trim()).toJson();
|
|
140
|
+
}
|
|
45
141
|
/**
|
|
46
142
|
* Composable that provides request body parsing utilities for various content types.
|
|
47
143
|
*
|
|
48
144
|
* @example
|
|
49
145
|
* ```ts
|
|
50
146
|
* app.post('/api/data', async () => {
|
|
51
|
-
* const {
|
|
52
|
-
* if (
|
|
147
|
+
* const { is, parseBody } = useBody()
|
|
148
|
+
* if (is('json')) {
|
|
53
149
|
* const data = await parseBody<{ name: string }>()
|
|
54
150
|
* return { received: data.name }
|
|
55
151
|
* }
|
|
56
152
|
* })
|
|
57
153
|
* ```
|
|
58
154
|
*
|
|
59
|
-
* @returns Object with
|
|
155
|
+
* @returns Object with `is(type)` checker, `parseBody` function, and `rawBody` accessor.
|
|
60
156
|
*/
|
|
61
|
-
|
|
62
|
-
const {
|
|
63
|
-
const { init } = store("request");
|
|
64
|
-
const { rawBody } = (0, __wooksjs_event_http.useRequest)();
|
|
65
|
-
const { "content-type": contentType } = (0, __wooksjs_event_http.useHeaders)();
|
|
66
|
-
function contentIs(type) {
|
|
67
|
-
return (contentType || "").includes(type);
|
|
68
|
-
}
|
|
69
|
-
const isJson = () => init("isJson", () => contentIs("application/json"));
|
|
70
|
-
const isHtml = () => init("isHtml", () => contentIs("text/html"));
|
|
71
|
-
const isXml = () => init("isXml", () => contentIs("text/xml"));
|
|
72
|
-
const isText = () => init("isText", () => contentIs("text/plain"));
|
|
73
|
-
const isBinary = () => init("isBinary", () => contentIs("application/octet-stream"));
|
|
74
|
-
const isFormData = () => init("isFormData", () => contentIs("multipart/form-data"));
|
|
75
|
-
const isUrlencoded = () => init("isUrlencoded", () => contentIs("application/x-www-form-urlencoded"));
|
|
76
|
-
const parseBody = () => init("parsed", async () => {
|
|
77
|
-
const body = await rawBody();
|
|
78
|
-
const sBody = body.toString();
|
|
79
|
-
if (isJson()) return jsonParser(sBody);
|
|
80
|
-
else if (isFormData()) return formDataParser(sBody);
|
|
81
|
-
else if (isUrlencoded()) return urlEncodedParser(sBody);
|
|
82
|
-
else if (isBinary()) return textParser(sBody);
|
|
83
|
-
else return textParser(sBody);
|
|
84
|
-
});
|
|
85
|
-
function jsonParser(v) {
|
|
86
|
-
try {
|
|
87
|
-
return safeJsonParse(v);
|
|
88
|
-
} catch (error) {
|
|
89
|
-
throw new __wooksjs_event_http.HttpError(400, error.message);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
function textParser(v) {
|
|
93
|
-
return v;
|
|
94
|
-
}
|
|
95
|
-
function formDataParser(v) {
|
|
96
|
-
const MAX_PARTS = 255;
|
|
97
|
-
const MAX_KEY_LENGTH = 100;
|
|
98
|
-
const MAX_VALUE_LENGTH = 100 * 1024;
|
|
99
|
-
const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`;
|
|
100
|
-
if (!boundary) throw new __wooksjs_event_http.HttpError(__wooksjs_event_http.EHttpStatusCode.BadRequest, "form-data boundary not recognized");
|
|
101
|
-
const parts = v.trim().split(boundary);
|
|
102
|
-
const result = Object.create(null);
|
|
103
|
-
let key = "";
|
|
104
|
-
let partContentType = "text/plain";
|
|
105
|
-
let partCount = 0;
|
|
106
|
-
for (const part of parts) {
|
|
107
|
-
parsePart();
|
|
108
|
-
key = "";
|
|
109
|
-
partContentType = "text/plain";
|
|
110
|
-
if (!part.trim() || part.trim() === "--") continue;
|
|
111
|
-
partCount++;
|
|
112
|
-
if (partCount > MAX_PARTS) throw new __wooksjs_event_http.HttpError(413, "Too many form fields");
|
|
113
|
-
let valueMode = false;
|
|
114
|
-
const lines = part.trim().split(/\n/u).map((l) => l.trim());
|
|
115
|
-
for (const line of lines) {
|
|
116
|
-
if (valueMode) {
|
|
117
|
-
if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new __wooksjs_event_http.HttpError(413, `Field "${key}" is too large`);
|
|
118
|
-
result[key] = (result[key] ? `${result[key]}\n` : "") + line;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (!line) {
|
|
122
|
-
valueMode = !!key;
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (line.toLowerCase().startsWith("content-disposition: form-data;")) {
|
|
126
|
-
key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? "";
|
|
127
|
-
if (!key) throw new __wooksjs_event_http.HttpError(400, `Could not read multipart name: ${line}`);
|
|
128
|
-
if (key.length > MAX_KEY_LENGTH) throw new __wooksjs_event_http.HttpError(413, "Field name too long");
|
|
129
|
-
if ([
|
|
130
|
-
"__proto__",
|
|
131
|
-
"constructor",
|
|
132
|
-
"prototype"
|
|
133
|
-
].includes(key)) throw new __wooksjs_event_http.HttpError(400, `Illegal key name "${key}"`);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (line.toLowerCase().startsWith("content-type:")) {
|
|
137
|
-
partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? "";
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
parsePart();
|
|
143
|
-
return result;
|
|
144
|
-
function parsePart() {
|
|
145
|
-
if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function urlEncodedParser(v) {
|
|
149
|
-
return new __wooksjs_event_http.WooksURLSearchParams(v.trim()).toJson();
|
|
150
|
-
}
|
|
157
|
+
const useBody = (0, __wooksjs_event_core.defineWook)((ctx) => {
|
|
158
|
+
const { rawBody } = (0, __wooksjs_event_http.useRequest)(ctx);
|
|
151
159
|
return {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
isXml,
|
|
155
|
-
isText,
|
|
156
|
-
isBinary,
|
|
157
|
-
isFormData,
|
|
158
|
-
isUrlencoded,
|
|
159
|
-
parseBody,
|
|
160
|
+
is: (type) => contentIsSlot(type, ctx),
|
|
161
|
+
parseBody: () => ctx.get(parsedBodySlot),
|
|
160
162
|
rawBody
|
|
161
163
|
};
|
|
162
|
-
}
|
|
164
|
+
});
|
|
163
165
|
|
|
164
166
|
//#endregion
|
|
165
167
|
exports.useBody = useBody;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
+
import { EventContext } from '@wooksjs/event-core';
|
|
2
|
+
|
|
3
|
+
/** Short names for common Content-Type values. */
|
|
4
|
+
type KnownContentType = 'json' | 'html' | 'xml' | 'text' | 'binary' | 'form-data' | 'urlencoded';
|
|
1
5
|
/**
|
|
2
6
|
* Composable that provides request body parsing utilities for various content types.
|
|
3
7
|
*
|
|
4
8
|
* @example
|
|
5
9
|
* ```ts
|
|
6
10
|
* app.post('/api/data', async () => {
|
|
7
|
-
* const {
|
|
8
|
-
* if (
|
|
11
|
+
* const { is, parseBody } = useBody()
|
|
12
|
+
* if (is('json')) {
|
|
9
13
|
* const data = await parseBody<{ name: string }>()
|
|
10
14
|
* return { received: data.name }
|
|
11
15
|
* }
|
|
12
16
|
* })
|
|
13
17
|
* ```
|
|
14
18
|
*
|
|
15
|
-
* @returns Object with
|
|
19
|
+
* @returns Object with `is(type)` checker, `parseBody` function, and `rawBody` accessor.
|
|
16
20
|
*/
|
|
17
|
-
declare
|
|
18
|
-
|
|
19
|
-
isHtml: () => boolean;
|
|
20
|
-
isXml: () => boolean;
|
|
21
|
-
isText: () => boolean;
|
|
22
|
-
isBinary: () => boolean;
|
|
23
|
-
isFormData: () => boolean;
|
|
24
|
-
isUrlencoded: () => boolean;
|
|
21
|
+
declare const useBody: (ctx?: EventContext) => {
|
|
22
|
+
is: (type: KnownContentType | (string & {})) => boolean;
|
|
25
23
|
parseBody: <T>() => Promise<T>;
|
|
26
24
|
rawBody: () => Promise<Buffer<ArrayBufferLike>>;
|
|
27
25
|
};
|
|
28
26
|
|
|
29
27
|
export { useBody };
|
|
28
|
+
export type { KnownContentType };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cached, cachedBy, defineWook } from "@wooksjs/event-core";
|
|
2
|
+
import { EHttpStatusCode, HttpError, WooksURLSearchParams, useHeaders, useRequest } from "@wooksjs/event-http";
|
|
2
3
|
|
|
3
4
|
//#region packages/http-body/src/utils/safe-json.ts
|
|
4
5
|
const ILLEGAL_KEYS = [
|
|
@@ -6,137 +7,138 @@ const ILLEGAL_KEYS = [
|
|
|
6
7
|
"constructor",
|
|
7
8
|
"prototype"
|
|
8
9
|
];
|
|
9
|
-
const illigalKeySet = new Set(ILLEGAL_KEYS);
|
|
10
10
|
function safeJsonParse(src) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
});
|
|
11
|
+
const parsed = JSON.parse(src);
|
|
12
|
+
assertNoProtoKeys(parsed);
|
|
13
|
+
return parsed;
|
|
15
14
|
}
|
|
16
|
-
function
|
|
17
|
-
if (
|
|
15
|
+
function assertNoProtoKeys(obj) {
|
|
16
|
+
if (obj === null || typeof obj !== "object") return;
|
|
17
|
+
if (Array.isArray(obj)) {
|
|
18
|
+
for (const item of obj) assertNoProtoKeys(item);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const record = obj;
|
|
22
|
+
for (const key of Object.keys(record)) {
|
|
23
|
+
if (key === ILLEGAL_KEYS[0] || key === ILLEGAL_KEYS[1] || key === ILLEGAL_KEYS[2]) throw new HttpError(400, `Illegal key name "${key}"`);
|
|
24
|
+
assertNoProtoKeys(record[key]);
|
|
25
|
+
}
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
//#endregion
|
|
21
29
|
//#region packages/http-body/src/body.ts
|
|
30
|
+
const CONTENT_TYPE_MAP = {
|
|
31
|
+
json: "application/json",
|
|
32
|
+
html: "text/html",
|
|
33
|
+
xml: "text/xml",
|
|
34
|
+
text: "text/plain",
|
|
35
|
+
binary: "application/octet-stream",
|
|
36
|
+
"form-data": "multipart/form-data",
|
|
37
|
+
urlencoded: "application/x-www-form-urlencoded"
|
|
38
|
+
};
|
|
39
|
+
const contentIsSlot = cachedBy((type, ctx) => {
|
|
40
|
+
const contentType = useHeaders(ctx)["content-type"] || "";
|
|
41
|
+
const mime = CONTENT_TYPE_MAP[type] || type;
|
|
42
|
+
return contentType.includes(mime);
|
|
43
|
+
});
|
|
44
|
+
const parsedBodySlot = cached(async (ctx) => {
|
|
45
|
+
const { rawBody } = useRequest(ctx);
|
|
46
|
+
const contentType = useHeaders(ctx)["content-type"] || "";
|
|
47
|
+
const contentIs = (type) => contentType.includes(type);
|
|
48
|
+
const body = await rawBody();
|
|
49
|
+
const sBody = body.toString();
|
|
50
|
+
if (contentIs("application/json")) return jsonParser(sBody);
|
|
51
|
+
else if (contentIs("multipart/form-data")) return formDataParser(sBody, contentType);
|
|
52
|
+
else if (contentIs("application/x-www-form-urlencoded")) return urlEncodedParser(sBody);
|
|
53
|
+
else return sBody;
|
|
54
|
+
});
|
|
55
|
+
function jsonParser(v) {
|
|
56
|
+
try {
|
|
57
|
+
return safeJsonParse(v);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw new HttpError(400, error.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function formDataParser(v, contentType) {
|
|
63
|
+
const MAX_PARTS = 255;
|
|
64
|
+
const MAX_KEY_LENGTH = 100;
|
|
65
|
+
const MAX_VALUE_LENGTH = 100 * 1024;
|
|
66
|
+
const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`;
|
|
67
|
+
if (!boundary) throw new HttpError(EHttpStatusCode.BadRequest, "form-data boundary not recognized");
|
|
68
|
+
const parts = v.trim().split(boundary);
|
|
69
|
+
const result = Object.create(null);
|
|
70
|
+
let key = "";
|
|
71
|
+
let partContentType = "text/plain";
|
|
72
|
+
let partCount = 0;
|
|
73
|
+
for (const part of parts) {
|
|
74
|
+
parsePart();
|
|
75
|
+
key = "";
|
|
76
|
+
partContentType = "text/plain";
|
|
77
|
+
if (!part.trim() || part.trim() === "--") continue;
|
|
78
|
+
partCount++;
|
|
79
|
+
if (partCount > MAX_PARTS) throw new HttpError(413, "Too many form fields");
|
|
80
|
+
let valueMode = false;
|
|
81
|
+
const lines = part.trim().split(/\n/u).map((l) => l.trim());
|
|
82
|
+
for (const line of lines) {
|
|
83
|
+
if (valueMode) {
|
|
84
|
+
if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new HttpError(413, `Field "${key}" is too large`);
|
|
85
|
+
result[key] = (result[key] ? `${result[key]}\n` : "") + line;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!line) {
|
|
89
|
+
valueMode = !!key;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (line.toLowerCase().startsWith("content-disposition: form-data;")) {
|
|
93
|
+
key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? "";
|
|
94
|
+
if (!key) throw new HttpError(400, `Could not read multipart name: ${line}`);
|
|
95
|
+
if (key.length > MAX_KEY_LENGTH) throw new HttpError(413, "Field name too long");
|
|
96
|
+
if ([
|
|
97
|
+
"__proto__",
|
|
98
|
+
"constructor",
|
|
99
|
+
"prototype"
|
|
100
|
+
].includes(key)) throw new HttpError(400, `Illegal key name "${key}"`);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (line.toLowerCase().startsWith("content-type:")) {
|
|
104
|
+
partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? "";
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
parsePart();
|
|
110
|
+
return result;
|
|
111
|
+
function parsePart() {
|
|
112
|
+
if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function urlEncodedParser(v) {
|
|
116
|
+
return new WooksURLSearchParams(v.trim()).toJson();
|
|
117
|
+
}
|
|
22
118
|
/**
|
|
23
119
|
* Composable that provides request body parsing utilities for various content types.
|
|
24
120
|
*
|
|
25
121
|
* @example
|
|
26
122
|
* ```ts
|
|
27
123
|
* app.post('/api/data', async () => {
|
|
28
|
-
* const {
|
|
29
|
-
* if (
|
|
124
|
+
* const { is, parseBody } = useBody()
|
|
125
|
+
* if (is('json')) {
|
|
30
126
|
* const data = await parseBody<{ name: string }>()
|
|
31
127
|
* return { received: data.name }
|
|
32
128
|
* }
|
|
33
129
|
* })
|
|
34
130
|
* ```
|
|
35
131
|
*
|
|
36
|
-
* @returns Object with
|
|
132
|
+
* @returns Object with `is(type)` checker, `parseBody` function, and `rawBody` accessor.
|
|
37
133
|
*/
|
|
38
|
-
|
|
39
|
-
const {
|
|
40
|
-
const { init } = store("request");
|
|
41
|
-
const { rawBody } = useRequest();
|
|
42
|
-
const { "content-type": contentType } = useHeaders();
|
|
43
|
-
function contentIs(type) {
|
|
44
|
-
return (contentType || "").includes(type);
|
|
45
|
-
}
|
|
46
|
-
const isJson = () => init("isJson", () => contentIs("application/json"));
|
|
47
|
-
const isHtml = () => init("isHtml", () => contentIs("text/html"));
|
|
48
|
-
const isXml = () => init("isXml", () => contentIs("text/xml"));
|
|
49
|
-
const isText = () => init("isText", () => contentIs("text/plain"));
|
|
50
|
-
const isBinary = () => init("isBinary", () => contentIs("application/octet-stream"));
|
|
51
|
-
const isFormData = () => init("isFormData", () => contentIs("multipart/form-data"));
|
|
52
|
-
const isUrlencoded = () => init("isUrlencoded", () => contentIs("application/x-www-form-urlencoded"));
|
|
53
|
-
const parseBody = () => init("parsed", async () => {
|
|
54
|
-
const body = await rawBody();
|
|
55
|
-
const sBody = body.toString();
|
|
56
|
-
if (isJson()) return jsonParser(sBody);
|
|
57
|
-
else if (isFormData()) return formDataParser(sBody);
|
|
58
|
-
else if (isUrlencoded()) return urlEncodedParser(sBody);
|
|
59
|
-
else if (isBinary()) return textParser(sBody);
|
|
60
|
-
else return textParser(sBody);
|
|
61
|
-
});
|
|
62
|
-
function jsonParser(v) {
|
|
63
|
-
try {
|
|
64
|
-
return safeJsonParse(v);
|
|
65
|
-
} catch (error) {
|
|
66
|
-
throw new HttpError(400, error.message);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function textParser(v) {
|
|
70
|
-
return v;
|
|
71
|
-
}
|
|
72
|
-
function formDataParser(v) {
|
|
73
|
-
const MAX_PARTS = 255;
|
|
74
|
-
const MAX_KEY_LENGTH = 100;
|
|
75
|
-
const MAX_VALUE_LENGTH = 100 * 1024;
|
|
76
|
-
const boundary = `--${(/boundary=([^;]+)(?:;|$)/u.exec(contentType || "") || [, ""])[1]}`;
|
|
77
|
-
if (!boundary) throw new HttpError(EHttpStatusCode.BadRequest, "form-data boundary not recognized");
|
|
78
|
-
const parts = v.trim().split(boundary);
|
|
79
|
-
const result = Object.create(null);
|
|
80
|
-
let key = "";
|
|
81
|
-
let partContentType = "text/plain";
|
|
82
|
-
let partCount = 0;
|
|
83
|
-
for (const part of parts) {
|
|
84
|
-
parsePart();
|
|
85
|
-
key = "";
|
|
86
|
-
partContentType = "text/plain";
|
|
87
|
-
if (!part.trim() || part.trim() === "--") continue;
|
|
88
|
-
partCount++;
|
|
89
|
-
if (partCount > MAX_PARTS) throw new HttpError(413, "Too many form fields");
|
|
90
|
-
let valueMode = false;
|
|
91
|
-
const lines = part.trim().split(/\n/u).map((l) => l.trim());
|
|
92
|
-
for (const line of lines) {
|
|
93
|
-
if (valueMode) {
|
|
94
|
-
if (line.length + String(result[key] ?? "").length > MAX_VALUE_LENGTH) throw new HttpError(413, `Field "${key}" is too large`);
|
|
95
|
-
result[key] = (result[key] ? `${result[key]}\n` : "") + line;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (!line) {
|
|
99
|
-
valueMode = !!key;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (line.toLowerCase().startsWith("content-disposition: form-data;")) {
|
|
103
|
-
key = (/name=([^;]+)/.exec(line) || [])[1].replace(/^["']|["']$/g, "") ?? "";
|
|
104
|
-
if (!key) throw new HttpError(400, `Could not read multipart name: ${line}`);
|
|
105
|
-
if (key.length > MAX_KEY_LENGTH) throw new HttpError(413, "Field name too long");
|
|
106
|
-
if ([
|
|
107
|
-
"__proto__",
|
|
108
|
-
"constructor",
|
|
109
|
-
"prototype"
|
|
110
|
-
].includes(key)) throw new HttpError(400, `Illegal key name "${key}"`);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (line.toLowerCase().startsWith("content-type:")) {
|
|
114
|
-
partContentType = (/content-type:\s?([^;]+)/i.exec(line) || [])[1] ?? "";
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
parsePart();
|
|
120
|
-
return result;
|
|
121
|
-
function parsePart() {
|
|
122
|
-
if (key && partContentType.includes("application/json") && typeof result[key] === "string") result[key] = safeJsonParse(result[key]);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
function urlEncodedParser(v) {
|
|
126
|
-
return new WooksURLSearchParams(v.trim()).toJson();
|
|
127
|
-
}
|
|
134
|
+
const useBody = defineWook((ctx) => {
|
|
135
|
+
const { rawBody } = useRequest(ctx);
|
|
128
136
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
isXml,
|
|
132
|
-
isText,
|
|
133
|
-
isBinary,
|
|
134
|
-
isFormData,
|
|
135
|
-
isUrlencoded,
|
|
136
|
-
parseBody,
|
|
137
|
+
is: (type) => contentIsSlot(type, ctx),
|
|
138
|
+
parseBody: () => ctx.get(parsedBodySlot),
|
|
137
139
|
rawBody
|
|
138
140
|
};
|
|
139
|
-
}
|
|
141
|
+
});
|
|
140
142
|
|
|
141
143
|
//#endregion
|
|
142
144
|
export { useBody };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/http-body",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "@wooksjs/http-body",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api",
|
|
@@ -42,10 +42,12 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
|
-
"@wooksjs/event-
|
|
45
|
+
"@wooksjs/event-core": "^0.7.1",
|
|
46
|
+
"@wooksjs/event-http": "^0.7.1"
|
|
46
47
|
},
|
|
47
48
|
"peerDependencies": {
|
|
48
|
-
"@wooksjs/event-
|
|
49
|
+
"@wooksjs/event-core": "^0.7.1",
|
|
50
|
+
"@wooksjs/event-http": "^0.7.1"
|
|
49
51
|
},
|
|
50
52
|
"scripts": {
|
|
51
53
|
"build": "rolldown -c ../../rolldown.config.mjs"
|