@remix-run/form-data-middleware 0.1.4 → 0.2.0

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 CHANGED
@@ -1,11 +1,11 @@
1
1
  # form-data-middleware
2
2
 
3
- Form body parsing middleware for Remix. It parses incoming [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and exposes `context.formData` and uploaded files on `context.files`.
3
+ Form body parsing middleware for Remix. It parses incoming [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) and exposes it via `context.get(FormData)`.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - **Request Form Parsing** - Parses request body form data once per request
8
- - **File Access** - Exposes uploaded files as `context.files`
8
+ - **File Access** - Uploaded files are available from `context.get(FormData)`
9
9
  - **Custom Upload Handling** - Supports pluggable upload handlers for file processing
10
10
  - **Error Control** - Optional suppression for malformed form data
11
11
 
@@ -17,9 +17,9 @@ npm i remix
17
17
 
18
18
  ## Usage
19
19
 
20
- Use the `formData()` middleware at the router level to parse `FormData` from the request body and make it available on the request context as `context.formData`.
20
+ Use the `formData()` middleware at the router level to parse `FormData` from the request body and make it available on request context via `context.get(FormData)`.
21
21
 
22
- `context.files` will also be available as a map of `File` objects keyed by the name of the form field.
22
+ Uploaded files are available in the parsed `FormData` object. For a single file field, use `formData.get(name)`. For repeated file fields, use `formData.getAll(name)`.
23
23
 
24
24
  ```ts
25
25
  import { createRouter } from 'remix/fetch-router'
@@ -30,13 +30,14 @@ let router = createRouter({
30
30
  })
31
31
 
32
32
  router.post('/users', async (context) => {
33
- let name = context.formData.get('name')
34
- let email = context.formData.get('email')
33
+ let formData = context.get(FormData)
34
+ let name = formData.get('name')
35
+ let email = formData.get('email')
35
36
 
36
37
  // Handle file uploads
37
- let avatar = context.files?.get('avatar')
38
+ let avatar = formData.get('avatar')
38
39
 
39
- return Response.json({ name, email, hasAvatar: !!avatar })
40
+ return Response.json({ name, email, hasAvatar: avatar instanceof File })
40
41
  })
41
42
  ```
42
43
 
@@ -62,9 +63,27 @@ let router = createRouter({
62
63
  })
63
64
  ```
64
65
 
66
+ ### Limit Multipart Growth
67
+
68
+ `formData()` forwards multipart limit options to `parseFormData()`, so you can cap uploads with
69
+ `maxHeaderSize`, `maxFiles`, `maxFileSize`, `maxParts`, and `maxTotalSize`.
70
+
71
+ ```ts
72
+ let router = createRouter({
73
+ middleware: [
74
+ formData({
75
+ maxFiles: 5,
76
+ maxFileSize: 10 * 1024 * 1024,
77
+ maxParts: 25,
78
+ maxTotalSize: 12 * 1024 * 1024,
79
+ }),
80
+ ],
81
+ })
82
+ ```
83
+
65
84
  ### Suppress Parse Errors
66
85
 
67
- Some requests may contain invalid form data that cannot be parsed. You can suppress parse errors by setting `suppressErrors` to `true`. In these cases, `context.formData` will be an empty `FormData` object.
86
+ Some requests may contain invalid form data that cannot be parsed. You can suppress those malformed-body parse errors by setting `suppressErrors` to `true`. In these cases, `context.get(FormData)` will be an empty `FormData` object. Multipart limit violations from `maxHeaderSize`, `maxFiles`, `maxFileSize`, `maxParts`, or `maxTotalSize` are never suppressed.
68
87
 
69
88
  ```ts
70
89
  let router = createRouter({
@@ -1,11 +1,13 @@
1
1
  import { type FileUploadHandler, type ParseFormDataOptions } from '@remix-run/form-data-parser';
2
2
  import type { Middleware } from '@remix-run/fetch-router';
3
+ type SetFormDataContextTransform = readonly [readonly [typeof FormData, FormData]];
3
4
  /**
4
- * Options for the `formData` middleware.
5
+ * Options for the {@link formData} middleware.
5
6
  */
6
7
  export interface FormDataOptions extends ParseFormDataOptions {
7
8
  /**
8
- * Set `true` to suppress parse errors.
9
+ * Set `true` to suppress malformed form-data parse errors. Multipart limit violations always
10
+ * surface as errors even when suppression is enabled.
9
11
  *
10
12
  * @default false
11
13
  */
@@ -18,10 +20,11 @@ export interface FormDataOptions extends ParseFormDataOptions {
18
20
  uploadHandler?: FileUploadHandler;
19
21
  }
20
22
  /**
21
- * Middleware that parses `FormData` from the request body and populates `context.formData`.
23
+ * Middleware that parses `FormData` from the request body and populates request context.
22
24
  *
23
25
  * @param options Options for parsing form data
24
26
  * @returns A middleware function that parses form data
25
27
  */
26
- export declare function formData(options?: FormDataOptions): Middleware;
28
+ export declare function formData(options?: FormDataOptions): Middleware<any, any, SetFormDataContextTransform>;
29
+ export {};
27
30
  //# sourceMappingURL=form-data.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"form-data.d.ts","sourceRoot":"","sources":["../../src/lib/form-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEzD;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,oBAAoB;IAC3D;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,iBAAiB,CAAA;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,UAAU,CA6B9D"}
1
+ {"version":3,"file":"form-data.d.ts","sourceRoot":"","sources":["../../src/lib/form-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EAC1B,MAAM,6BAA6B,CAAA;AACpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AAEzD,KAAK,2BAA2B,GAAG,SAAS,CAAC,SAAS,CAAC,OAAO,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAA;AAYlF;;GAEG;AACH,MAAM,WAAW,eAAgB,SAAQ,oBAAoB;IAC3D;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,iBAAiB,CAAA;CAClC;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CACtB,OAAO,CAAC,EAAE,eAAe,GACxB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,2BAA2B,CAAC,CAiCnD"}
@@ -1,6 +1,13 @@
1
- import { FormDataParseError, parseFormData, } from '@remix-run/form-data-parser';
1
+ import { MaxFilesExceededError, MaxFileSizeExceededError, MaxHeaderSizeExceededError, MaxPartsExceededError, MaxTotalSizeExceededError, parseFormData, } from '@remix-run/form-data-parser';
2
+ function isMultipartLimitError(error) {
3
+ return (error instanceof MaxFilesExceededError ||
4
+ error instanceof MaxHeaderSizeExceededError ||
5
+ error instanceof MaxFileSizeExceededError ||
6
+ error instanceof MaxPartsExceededError ||
7
+ error instanceof MaxTotalSizeExceededError);
8
+ }
2
9
  /**
3
- * Middleware that parses `FormData` from the request body and populates `context.formData`.
10
+ * Middleware that parses `FormData` from the request body and populates request context.
4
11
  *
5
12
  * @param options Options for parsing form data
6
13
  * @returns A middleware function that parses form data
@@ -9,6 +16,9 @@ export function formData(options) {
9
16
  let suppressErrors = options?.suppressErrors ?? false;
10
17
  let uploadHandler = options?.uploadHandler;
11
18
  return async (context) => {
19
+ if (context.has(FormData)) {
20
+ return;
21
+ }
12
22
  if (context.method === 'GET' || context.method === 'HEAD') {
13
23
  return;
14
24
  }
@@ -16,17 +26,17 @@ export function formData(options) {
16
26
  if (contentType == null ||
17
27
  (!contentType.startsWith('multipart/') &&
18
28
  !contentType.startsWith('application/x-www-form-urlencoded'))) {
19
- context.formData = new FormData();
29
+ context.set(FormData, new FormData());
20
30
  return;
21
31
  }
22
32
  try {
23
- context.formData = await parseFormData(context.request, options, uploadHandler);
33
+ context.set(FormData, await parseFormData(context.request, options, uploadHandler));
24
34
  }
25
35
  catch (error) {
26
- if (!suppressErrors || !(error instanceof FormDataParseError)) {
36
+ if (!suppressErrors || isMultipartLimitError(error)) {
27
37
  throw error;
28
38
  }
29
- context.formData = new FormData();
39
+ context.set(FormData, new FormData());
30
40
  }
31
41
  };
32
42
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remix-run/form-data-middleware",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Middleware for parsing FormData from request bodies",
5
5
  "author": "Michael Jackson <mjijackson@gmail.com>",
6
6
  "license": "MIT",
@@ -28,12 +28,12 @@
28
28
  "devDependencies": {
29
29
  "@types/node": "^24.6.0",
30
30
  "@typescript/native-preview": "7.0.0-dev.20251125.1",
31
- "@remix-run/fetch-router": "0.17.0",
32
- "@remix-run/form-data-parser": "0.15.0"
31
+ "@remix-run/fetch-router": "0.18.0",
32
+ "@remix-run/form-data-parser": "0.16.0"
33
33
  },
34
34
  "dependencies": {
35
- "@remix-run/form-data-parser": "^0.15.0",
36
- "@remix-run/fetch-router": "^0.17.0"
35
+ "@remix-run/fetch-router": "^0.18.0",
36
+ "@remix-run/form-data-parser": "^0.16.0"
37
37
  },
38
38
  "keywords": [
39
39
  "fetch",
@@ -46,7 +46,7 @@
46
46
  "scripts": {
47
47
  "build": "tsgo -p tsconfig.build.json",
48
48
  "clean": "git clean -fdX",
49
- "test": "node --disable-warning=ExperimentalWarning --test",
49
+ "test": "node --test",
50
50
  "typecheck": "tsgo --noEmit"
51
51
  }
52
52
  }
@@ -1,17 +1,34 @@
1
1
  import {
2
- FormDataParseError,
2
+ MaxFilesExceededError,
3
+ MaxFileSizeExceededError,
4
+ MaxHeaderSizeExceededError,
5
+ MaxPartsExceededError,
6
+ MaxTotalSizeExceededError,
3
7
  parseFormData,
4
8
  type FileUploadHandler,
5
9
  type ParseFormDataOptions,
6
10
  } from '@remix-run/form-data-parser'
7
11
  import type { Middleware } from '@remix-run/fetch-router'
8
12
 
13
+ type SetFormDataContextTransform = readonly [readonly [typeof FormData, FormData]]
14
+
15
+ function isMultipartLimitError(error: unknown): boolean {
16
+ return (
17
+ error instanceof MaxFilesExceededError ||
18
+ error instanceof MaxHeaderSizeExceededError ||
19
+ error instanceof MaxFileSizeExceededError ||
20
+ error instanceof MaxPartsExceededError ||
21
+ error instanceof MaxTotalSizeExceededError
22
+ )
23
+ }
24
+
9
25
  /**
10
- * Options for the `formData` middleware.
26
+ * Options for the {@link formData} middleware.
11
27
  */
12
28
  export interface FormDataOptions extends ParseFormDataOptions {
13
29
  /**
14
- * Set `true` to suppress parse errors.
30
+ * Set `true` to suppress malformed form-data parse errors. Multipart limit violations always
31
+ * surface as errors even when suppression is enabled.
15
32
  *
16
33
  * @default false
17
34
  */
@@ -25,16 +42,22 @@ export interface FormDataOptions extends ParseFormDataOptions {
25
42
  }
26
43
 
27
44
  /**
28
- * Middleware that parses `FormData` from the request body and populates `context.formData`.
45
+ * Middleware that parses `FormData` from the request body and populates request context.
29
46
  *
30
47
  * @param options Options for parsing form data
31
48
  * @returns A middleware function that parses form data
32
49
  */
33
- export function formData(options?: FormDataOptions): Middleware {
50
+ export function formData(
51
+ options?: FormDataOptions,
52
+ ): Middleware<any, any, SetFormDataContextTransform> {
34
53
  let suppressErrors = options?.suppressErrors ?? false
35
54
  let uploadHandler = options?.uploadHandler
36
55
 
37
56
  return async (context) => {
57
+ if (context.has(FormData)) {
58
+ return
59
+ }
60
+
38
61
  if (context.method === 'GET' || context.method === 'HEAD') {
39
62
  return
40
63
  }
@@ -45,18 +68,18 @@ export function formData(options?: FormDataOptions): Middleware {
45
68
  (!contentType.startsWith('multipart/') &&
46
69
  !contentType.startsWith('application/x-www-form-urlencoded'))
47
70
  ) {
48
- context.formData = new FormData()
71
+ context.set(FormData, new FormData())
49
72
  return
50
73
  }
51
74
 
52
75
  try {
53
- context.formData = await parseFormData(context.request, options, uploadHandler)
76
+ context.set(FormData, await parseFormData(context.request, options, uploadHandler))
54
77
  } catch (error) {
55
- if (!suppressErrors || !(error instanceof FormDataParseError)) {
78
+ if (!suppressErrors || isMultipartLimitError(error)) {
56
79
  throw error
57
80
  }
58
81
 
59
- context.formData = new FormData()
82
+ context.set(FormData, new FormData())
60
83
  }
61
84
  }
62
85
  }