1/**
2 * Parses and persists HTTP `multipart/form-data` request bodies.
3 *
4 * `Multipart` turns incoming byte streams into typed form parts. Text parts
5 * become decoded fields, while upload parts stay as streamed files until they
6 * are collected or written to scoped temporary files. The persisted
7 * representation can then be decoded with schemas for handlers that receive
8 * fields and uploaded files together. This module also includes multipart error
9 * types, schema helpers for persisted files, and parser limit settings.
10 *
11 * @stability unstable
12 * @since 4.0.0
13 */
14import * as Arr from "../Array.ts"
15import * as ByteSize from "../ByteSize.ts"
16import * as Cause from "../Cause.ts"
17import * as Channel from "../Channel.ts"
18import * as Context from "../Context.ts"
19import * as Data from "../Data.ts"
20import * as Effect from "../Effect.ts"
21import * as ErrorReporter from "../ErrorReporter.ts"
22import * as Exit from "../Exit.ts"
23import * as FileSystem from "../FileSystem.ts"
24import { constant, dual } from "../Function.ts"
25import * as Inspectable from "../Inspectable.ts"
26import * as Option from "../Option.ts"
27import * as Path from "../Path.ts"
28import * as Predicate from "../Predicate.ts"
29import * as Pull from "../Pull.ts"
30import * as Schema from "../Schema.ts"
31import type { ParseOptions } from "../SchemaAST.ts"
32import * as SchemaTransformation from "../SchemaTransformation.ts"
33import type * as Scope from "../Scope.ts"
34import * as Stream from "../Stream.ts"
35import * as IncomingMessage from "./HttpIncomingMessage.ts"
36import * as HttpServerRespondable from "./HttpServerRespondable.ts"
37import * as HttpServerResponse from "./HttpServerResponse.ts"
38import * as MP from "./MultipartParser.ts"
39
40/**
41 * Type identifier used to brand multipart part values.
42 *
43 * @stability unstable
44 * @category type IDs
45 * @since 4.0.0
46 */
47export const TypeId = "~effect/http/Multipart"
48
49/**
50 * A parsed multipart part.
51 *
52 * **Details**
53 *
54 * A part is either a text `Field` or a streamed `File`.
55 *
56 * @stability unstable
57 * @category models
58 * @since 4.0.0
59 */
60export type Part = Field | File
61
62/**
63 * Namespace containing shared multipart part model types.
64 *
65 * @stability unstable
66 * @since 4.0.0
67 */
68export declare namespace Part {
69 /**
70 * Common protocol implemented by multipart part values.
71 *
72 * **Details**
73 *
74 * It provides the multipart type identifier, tag, and inspectable behavior shared
75 * by fields, files, and persisted files.
76 *
77 * @stability unstable
78 * @category models
79 * @since 4.0.0
80 */
81 export interface Proto extends Inspectable.Inspectable {
82 readonly [TypeId]: typeof TypeId
83 readonly _tag: string
84 }
85}
86
87/**
88 * Multipart form field containing a decoded text value.
89 *
90 * **Details**
91 *
92 * The `key` is the field name, `contentType` is the part media type, and `value`
93 * is the decoded field content.
94 *
95 * @stability unstable
96 * @category models
97 * @since 4.0.0
98 */
99export interface Field extends Part.Proto {
100 readonly _tag: "Field"
101 readonly key: string
102 readonly contentType: string
103 readonly value: string
104}
105
106/**
107 * Returns `true` when a value has the multipart part type identifier.
108 *
109 * **Details**
110 *
111 * This includes `Field`, `File`, and `PersistedFile` values. Use
112 * `isStreamPart` to identify only parsed, streamed parts.
113 *
114 * @stability unstable
115 * @category guards
116 * @since 4.0.0
117 */
118export const isPart = (u: unknown): u is Part => Predicate.hasProperty(u, TypeId)
119
120/**
121 * Returns `true` when a value is a multipart text `Field` or streamed `File`.
122 *
123 * @stability unstable
124 * @category guards
125 * @since 4.0.0
126 */
127export const isStreamPart = (u: unknown): u is Part => isPart(u) && (u._tag === "Field" || u._tag === "File")
128
129/**
130 * Returns `true` when a value is a multipart text `Field`.
131 *
132 * @stability unstable
133 * @category guards
134 * @since 4.0.0
135 */
136export const isField = (u: unknown): u is Field => isPart(u) && u._tag === "Field"
137
138/**
139 * Multipart file part.
140 *
141 * **Gotchas**
142 *
143 * The file content is exposed as a byte stream. `contentEffect` collects the full
144 * file into memory and should be used only when the file size is acceptable.
145 *
146 * @stability unstable
147 * @category models
148 * @since 4.0.0
149 */
150export interface File extends Part.Proto {
151 readonly _tag: "File"
152 readonly key: string
153 readonly name: string
154 readonly contentType: string
155 readonly content: Stream.Stream<Uint8Array, MultipartError>
156 readonly contentEffect: Effect.Effect<Uint8Array, MultipartError>
157}
158
159/**
160 * Returns `true` when a value is a multipart `File`.
161 *
162 * @stability unstable
163 * @category guards
164 * @since 4.0.0
165 */
166export const isFile = (u: unknown): u is File => isPart(u) && u._tag === "File"
167
168/**
169 * Multipart file part that has been written to the filesystem.
170 *
171 * **Details**
172 *
173 * The `path` points to the persisted file while the scope used to persist the
174 * multipart data remains open.
175 *
176 * @stability unstable
177 * @category models
178 * @since 4.0.0
179 */
180export interface PersistedFile extends Part.Proto {
181 readonly _tag: "PersistedFile"
182 readonly key: string
183 readonly name: string
184 readonly contentType: string
185 readonly path: string
186}
187
188/**
189 * Returns `true` when a value is a persisted multipart file.
190 *
191 * @stability unstable
192 * @category guards
193 * @since 4.0.0
194 */
195export const isPersistedFile = (u: unknown): u is PersistedFile =>
196 Predicate.hasProperty(u, TypeId) && Predicate.isTagged(u, "PersistedFile")
197
198/**
199 * Record representation of persisted multipart data.
200 *
201 * **Details**
202 *
203 * Field names map to text values, arrays of text values, or arrays of
204 * `PersistedFile` values.
205 *
206 * @stability unstable
207 * @category models
208 * @since 4.0.0
209 */
210export interface Persisted {
211 readonly [key: string]: ReadonlyArray<PersistedFile> | ReadonlyArray<string> | string
212}
213
214const MultipartErrorTypeId = "~effect/http/Multipart/MultipartError"
215
216const toMultipartError = (cause: unknown): MultipartError =>
217 isMultipartError(cause)
218 ? cause
219 : MultipartError.fromReason("InternalError", cause)
220
221/**
222 * Error reason carried by a `MultipartError`.
223 *
224 * **Details**
225 *
226 * It identifies parser and limit failures such as oversized files or fields, too
227 * many parts, total body size limits, parse errors, and internal errors.
228 *
229 * @stability unstable
230 * @category errors
231 * @since 4.0.0
232 */
233export class MultipartErrorReason extends Data.Error<{
234 readonly _tag: "FileTooLarge" | "FieldTooLarge" | "BodyTooLarge" | "TooManyParts" | "InternalError" | "Parse"
235 readonly cause?: unknown
236}> {}
237
238const responseStatusByReason = {
239 FileTooLarge: 413,
240 FieldTooLarge: 413,
241 BodyTooLarge: 413,
242 TooManyParts: 413,
243 InternalError: 500,
244 Parse: 400
245} as const satisfies Record<MultipartErrorReason["_tag"], number>
246
247/**
248 * Error raised while parsing, streaming, or persisting multipart form data.
249 *
250 * **Details**
251 *
252 * The `reason` field contains the concrete `MultipartErrorReason`. When used as
253 * a server response, parse errors render as `400`, limit errors as `413`, and
254 * internal errors as `500`. Multipart errors are ignored by the error reporter.
255 *
256 * @stability unstable
257 * @category errors
258 * @since 4.0.0
259 */
260export class MultipartError extends Data.TaggedError("MultipartError")<{
261 readonly reason: MultipartErrorReason
262}> implements HttpServerRespondable.Respondable {
263 /**
264 * Creates a multipart error from a reason tag and optional cause.
265 *
266 * @stability unstable
267 * @since 4.0.0
268 */
269 static fromReason(reason: MultipartErrorReason["_tag"], cause?: unknown): MultipartError {
270 return new MultipartError({ reason: new MultipartErrorReason({ _tag: reason, cause }) })
271 }
272
273 /**
274 * Marks this value as a multipart error for runtime guards.
275 *
276 * @stability unstable
277 * @since 4.0.0
278 */
279 readonly [MultipartErrorTypeId] = MultipartErrorTypeId
280
281 override readonly [ErrorReporter.ignore] = true;
282
283 /**
284 * Converts the multipart error into an HTTP response based on its reason.
285 *
286 * **Details**
287 *
288 * Parse errors produce `400`, size and part-count limits produce `413`, and
289 * internal errors produce `500`.
290 *
291 * @stability unstable
292 * @since 4.0.0
293 */
294 [HttpServerRespondable.symbol]() {
295 return Effect.succeed(HttpServerResponse.empty({ status: responseStatusByReason[this.reason._tag] }))
296 }
297
298 /**
299 * Uses the concrete multipart error reason as the public message.
300 *
301 * @stability unstable
302 * @since 4.0.0
303 */
304 override get message(): string {
305 return this.reason._tag
306 }
307}
308
309/**
310 * Checks whether a value is a `MultipartError`.
311 *
312 * @stability unstable
313 * @category guards
314 * @since 4.0.0
315 */
316export const isMultipartError = (u: unknown): u is MultipartError => Predicate.hasProperty(u, MultipartErrorTypeId)
317
318/**
319 * Schema type for persisted multipart files.
320 *
321 * @stability unstable
322 * @category schemas
323 * @since 4.0.0
324 */
325export interface PersistedFileSchema extends Schema.declare<PersistedFile> {}
326
327const PersistedFileEncoded = Schema.Struct({
328 key: Schema.String,
329 name: Schema.String,
330 contentType: Schema.String.annotate({ contentEncoding: "binary" }),
331 path: Schema.String
332})
333
334/**
335 * Schema for persisted multipart files.
336 *
337 * **Details**
338 *
339 * The encoded form contains the field key, original file name, content type, and
340 * filesystem path.
341 *
342 * @stability unstable
343 * @category schemas
344 * @since 4.0.0
345 */
346export const PersistedFileSchema: PersistedFileSchema = Schema.declare(
347 isPersistedFile,
348 {
349 representation: {
350 id: "effect/http/PersistedFile",
351 payload: null
352 },
353 toCode: () => ({
354 runtime: "Multipart.PersistedFileSchema",
355 Type: "Multipart.PersistedFile",
356 importDeclarations: [`import * as Multipart from "effect/http/Multipart"`]
357 }),
358 expected: "PersistedFile",
359 toCodecJson: () =>
360 Schema.link<PersistedFile>()(
361 PersistedFileEncoded,
362 SchemaTransformation.transform({
363 decode: ({ contentType, key, name, path }) => new PersistedFileImpl(key, name, contentType, path),
364 encode: (file) => ({
365 key: file.key,
366 name: file.name,
367 contentType: file.contentType,
368 path: file.path
369 })
370 })
371 )
372 }
373)
374
375/**
376 * Schema for an array of persisted multipart files.
377 *
378 * @stability unstable
379 * @category schemas
380 * @since 4.0.0
381 */
382export const FilesSchema: Schema.$Array<PersistedFileSchema> = Schema.Array(PersistedFileSchema)
383
384/**
385 * Schema for exactly one persisted multipart file.
386 *
387 * **Details**
388 *
389 * The encoded form is a one-element file array, while the decoded value is the
390 * single `PersistedFile`.
391 *
392 * @stability unstable
393 * @category schemas
394 * @since 4.0.0
395 */
396export const SingleFileSchema: Schema.decodeTo<PersistedFileSchema, Schema.$Array<PersistedFileSchema>> = FilesSchema
397 .check(
398 Schema.isBetweenLength(1, 1)
399 ).pipe(
400 Schema.decodeTo(
401 PersistedFileSchema,
402 SchemaTransformation.transform({
403 decode: ([file]) => file,
404 encode: (file) => [file]
405 })
406 )
407 )
408
409/**
410 * Creates a decoder for persisted multipart data using the supplied schema.
411 *
412 * **Details**
413 *
414 * The returned function decodes an unknown input into the schema output and fails
415 * with `SchemaError` when validation fails.
416 *
417 * @stability unstable
418 * @category schemas
419 * @since 4.0.0
420 */
421export const schemaPersisted = <A, I extends Partial<Persisted>, RD>(
422 schema: Schema.ConstraintCodec<A, I, RD, unknown>
423): (input: unknown, options?: ParseOptions) => Effect.Effect<A, Schema.SchemaError, RD> =>
424 Schema.decodeUnknownEffect(schema)
425
426/**
427 * Creates a decoder for a JSON-encoded field in persisted multipart data.
428 *
429 * **Details**
430 *
431 * The selected field is parsed from a JSON string and decoded with the supplied
432 * schema.
433 *
434 * @stability unstable
435 * @category schemas
436 * @since 4.0.0
437 */
438export const schemaJson = <A, RD>(
439 schema: Schema.ConstraintDecoder<A, RD>,
440 options?: (ParseOptions & IncomingMessage.JsonOptions) | undefined
441): {
442 (
443 field: string
444 ): (persisted: Persisted) => Effect.Effect<A, Schema.SchemaError, RD>
445 (
446 persisted: Persisted,
447 field: string
448 ): Effect.Effect<A, Schema.SchemaError, RD>
449} => {
450 const fromJson = Schema.fromJsonString(schema, options)
451 return dual(2, (persisted: Persisted, field: string): Effect.Effect<A, Schema.SchemaError, RD> =>
452 Effect.map(
453 Schema.decodeUnknownEffect(Schema.Struct({ [field]: fromJson }))(persisted, options),
454 (_) => _[field]
455 ))
456}
457
458/**
459 * Builds the low-level multipart parser configuration from request headers and
460 * the current fiber context.
461 *
462 * **Details**
463 *
464 * Parser limits are read from the multipart references, including maximum parts,
465 * field size, file size, total body size, and field MIME type overrides.
466 *
467 * @stability unstable
468 * @category configuration
469 * @since 4.0.0
470 */
471export const makeConfig = (
472 headers: Record<string, string>
473): Effect.Effect<MP.BaseConfig> =>
474 Effect.withFiber((fiber) => {
475 const mimeTypes = Context.get(fiber.context, FieldMimeTypes)
476 return Effect.succeed<MP.BaseConfig>({
477 headers,
478 maxParts: fiber.getRef(MaxParts),
479 maxFieldSize: fiber.getRef(MaxFieldSize),
480 maxPartSize: fiber.getRef(MaxFileSize),
481 maxTotalSize: fiber.getRef(IncomingMessage.MaxBodySize),
482 isFile: mimeTypes.length === 0 ? undefined : (info: MP.PartInfo): boolean =>
483 !mimeTypes.some(
484 (_) => info.contentType.includes(_)
485 ) && MP.defaultIsFile(info)
486 })
487 })
488
489/**
490 * Creates a channel that parses multipart byte chunks into multipart parts.
491 *
492 * **Details**
493 *
494 * The channel consumes non-empty batches of `Uint8Array` chunks and emits
495 * non-empty batches of parsed `Part` values, failing with `MultipartError` for
496 * parser and limit failures.
497 *
498 * @stability unstable
499 * @category parsing
500 * @since 4.0.0
501 */
502export const makeChannel = <IE>(headers: Record<string, string>): Channel.Channel<
503 Arr.NonEmptyReadonlyArray<Part>,
504 MultipartError | IE,
505 void,
506 Arr.NonEmptyReadonlyArray<Uint8Array>,
507 IE,
508 unknown
509> =>
510 Channel.fromTransform((upstream) =>
511 Effect.map(makeConfig(headers), (config) => {
512 let partsBuffer: Array<Part> = []
513 let exit = Option.none<Exit.Exit<never, IE | MultipartError | Cause.Done>>()
514 let ended = false
515
516 const parser = MP.make({
517 ...config,
518 onField(info, value) {
519 partsBuffer.push(new FieldImpl(info.name, info.contentType, MP.decodeField(info, value)))
520 },
521 onFile(info) {
522 let chunks: Array<Uint8Array> = []
523 let finished = false
524 const pullChunks = Channel.fromPull(
525 Effect.succeed(
526 Effect.suspend(function loop(): Pull.Pull<Arr.NonEmptyReadonlyArray<Uint8Array>, IE | MultipartError> {
527 if (!Arr.isReadonlyArrayNonEmpty(chunks)) {
528 if (finished) {
529 return Cause.done()
530 }
531 if (Option.isSome(exit)) {
532 return exit.value
533 }
534 return Effect.flatMap(pump, loop)
535 }
536 const chunk = chunks
537 chunks = []
538 return Effect.succeed(chunk)
539 })
540 )
541 )
542 partsBuffer.push(new FileImpl(info, Channel.mapError(pullChunks, toMultipartError)))
543 return function(chunk) {
544 if (chunk === null) {
545 finished = true
546 } else {
547 chunks.push(chunk)
548 }
549 }
550 },
551 onError(error_) {
552 exit = Option.some(Exit.fail(convertError(error_)))
553 },
554 onDone() {
555 if (Option.isNone(exit)) {
556 exit = Option.some(Exit.fail(Cause.Done()))
557 }
558 }
559 })
560
561 const pump = upstream.pipe(
562 Effect.flatMap((chunk) => {
563 for (let i = 0; i < chunk.length; i++) {
564 parser.write(chunk[i])
565 }
566 return Effect.void
567 }),
568 Effect.catchCause((cause) => {
569 if (Pull.isDoneCause(cause)) {
570 if (!ended) {
571 ended = true
572 parser.end()
573 }
574 } else {
575 exit = Option.some(Exit.failCause(cause)) as any
576 }
577 return Effect.void
578 })
579 )
580
581 return Effect.suspend(function loop(): Pull.Pull<Arr.NonEmptyReadonlyArray<Part>, IE | MultipartError> {
582 if (!Arr.isReadonlyArrayNonEmpty(partsBuffer)) {
583 if (Option.isSome(exit)) {
584 return exit.value
585 }
586 return Effect.flatMap(pump, loop)
587 }
588 const parts = partsBuffer
589 partsBuffer = []
590 return Effect.succeed(parts)
591 })
592 })
593 )
594
595function convertError(cause: MP.MultipartError): MultipartError {
596 switch (cause._tag) {
597 case "ReachedLimit": {
598 switch (cause.limit) {
599 case "MaxParts": {
600 return MultipartError.fromReason("TooManyParts", cause)
601 }
602 case "MaxFieldSize": {
603 return MultipartError.fromReason("FieldTooLarge", cause)
604 }
605 case "MaxPartSize": {
606 return MultipartError.fromReason("FileTooLarge", cause)
607 }
608 case "MaxTotalSize": {
609 return MultipartError.fromReason("BodyTooLarge", cause)
610 }
611 }
612 }
613 default: {
614 return MultipartError.fromReason("Parse", cause)
615 }
616 }
617}
618
619abstract class PartBase extends Inspectable.Class {
620 readonly [TypeId]: typeof TypeId
621 constructor() {
622 super()
623 this[TypeId] = TypeId
624 }
625}
626
627class FieldImpl extends PartBase implements Field {
628 readonly _tag = "Field"
629 readonly key: string
630 readonly contentType: string
631 readonly value: string
632
633 constructor(
634 key: string,
635 contentType: string,
636 value: string
637 ) {
638 super()
639 this.key = key
640 this.contentType = contentType
641 this.value = value
642 }
643
644 toJSON(): unknown {
645 return {
646 _id: "@effect/platform/Multipart/Part",
647 _tag: "Field",
648 key: this.key,
649 contentType: this.contentType,
650 value: this.value
651 }
652 }
653}
654
655class FileImpl extends PartBase implements File {
656 readonly _tag = "File"
657 readonly key: string
658 readonly name: string
659 readonly contentType: string
660 readonly content: Stream.Stream<Uint8Array, MultipartError>
661 readonly contentEffect: Effect.Effect<Uint8Array, MultipartError>
662
663 constructor(
664 info: MP.PartInfo,
665 channel: Channel.Channel<Arr.NonEmptyReadonlyArray<Uint8Array>, MultipartError>
666 ) {
667 super()
668 this.key = info.name
669 this.name = info.filename ?? info.name
670 this.contentType = info.contentType
671 this.content = Stream.fromChannel(channel)
672 this.contentEffect = Channel.mkUint8Array(channel)
673 }
674
675 toJSON(): unknown {
676 return {
677 _id: "@effect/platform/Multipart/Part",
678 _tag: "File",
679 key: this.key,
680 name: this.name,
681 contentType: this.contentType
682 }
683 }
684}
685
686const defaultWriteFile = (path: string, file: File) =>
687 Effect.flatMap(
688 FileSystem.FileSystem,
689 (fs) => Effect.mapError(Stream.run(file.content, fs.sink(path)), toMultipartError)
690 )
691
692/**
693 * Runs a channel of byte chunks and collects all output into a single
694 * `Uint8Array`.
695 *
696 * **Gotchas**
697 *
698 * This materializes the full content in memory.
699 * The source channel must not reuse or mutate emitted buffers, which are retained
700 * until collection completes.
701 *
702 * @stability unstable
703 * @category converting
704 * @since 4.0.0
705 */
706export const collectUint8Array = <OE, OD, R>(
707 self: Channel.Channel<Arr.NonEmptyReadonlyArray<Uint8Array>, OE, OD, unknown, unknown, unknown, R>
708): Effect.Effect<Uint8Array<ArrayBuffer>, OE, R> => Channel.mkUint8Array(self)
709
710/**
711 * Persists a stream of multipart parts into a record.
712 *
713 * **Details**
714 *
715 * Text fields are collected as strings, and file parts are written to files in a
716 * scoped temporary directory.
717 *
718 * **Gotchas**
719 *
720 * Persisted file paths remain valid for the lifetime of the scope.
721 *
722 * @stability unstable
723 * @category converting
724 * @since 4.0.0
725 */
726export const toPersisted = (
727 stream: Stream.Stream<Part, MultipartError>,
728 writeFile = defaultWriteFile
729): Effect.Effect<Persisted, MultipartError, FileSystem.FileSystem | Path.Path | Scope.Scope> =>
730 Effect.gen(function*() {
731 const fs = yield* FileSystem.FileSystem
732 const path_ = yield* Path.Path
733 const dir = yield* fs.makeTempDirectoryScoped()
734 const persisted: Record<string, Array<PersistedFile> | Array<string> | string> = Object.create(null)
735 const usedPaths = new Set<string>()
736 let fileIndex = 0
737 yield* Stream.runForEach(stream, (part) => {
738 if (part._tag === "Field") {
739 if (!(part.key in persisted)) {
740 persisted[part.key] = part.value
741 } else if (typeof persisted[part.key] === "string") {
742 persisted[part.key] = [persisted[part.key] as string, part.value]
743 } else {
744 ;(persisted[part.key] as Array<string>).push(part.value)
745 }
746 return Effect.void
747 } else if (part.name === "") {
748 return Effect.void
749 }
750 const file = part
751 const fileName = path_.basename(file.name).slice(-128)
752 let path = path_.join(dir, fileName)
753 while (usedPaths.has(path)) {
754 path = path_.join(dir, `${fileIndex++}-${fileName}`)
755 }
756 usedPaths.add(path)
757 const filePart = new PersistedFileImpl(
758 file.key,
759 file.name,
760 file.contentType,
761 path
762 )
763 if (Array.isArray(persisted[part.key])) {
764 ;(persisted[part.key] as Array<PersistedFile>).push(filePart)
765 } else {
766 persisted[part.key] = [filePart]
767 }
768 return writeFile(path, file)
769 })
770 return persisted
771 }).pipe(
772 Effect.catchTag("PlatformError", (cause) => Effect.fail(MultipartError.fromReason("InternalError", cause)))
773 )
774
775class PersistedFileImpl extends PartBase implements PersistedFile {
776 readonly _tag = "PersistedFile"
777 readonly key: string
778 readonly name: string
779 readonly contentType: string
780 readonly path: string
781
782 constructor(
783 key: string,
784 name: string,
785 contentType: string,
786 path: string
787 ) {
788 super()
789 this.key = key
790 this.name = name
791 this.contentType = contentType
792 this.path = path
793 }
794
795 toJSON(): unknown {
796 return {
797 _id: "@effect/platform/Multipart/Part",
798 _tag: "PersistedFile",
799 key: this.key,
800 name: this.name,
801 contentType: this.contentType,
802 path: this.path
803 }
804 }
805}
806
807/**
808 * Creates a context containing multipart parser limit settings.
809 *
810 * **Details**
811 *
812 * The context can provide maximum part count, field size, file size, total body
813 * size, and MIME types that should be parsed as fields.
814 *
815 * @stability unstable
816 * @category references
817 * @since 4.0.0
818 */
819export const limitsServices = (options: {
820 readonly maxParts?: number | undefined
821 readonly maxFieldSize?: ByteSize.Input | undefined
822 readonly maxFileSize?: ByteSize.Input | undefined
823 readonly maxTotalSize?: ByteSize.Input | undefined
824 readonly fieldMimeTypes?: ReadonlyArray<string> | undefined
825}): Context.Context<never> => {
826 const map = new Map<string, unknown>()
827 if (options.maxParts !== undefined) {
828 map.set(MaxParts.key, options.maxParts)
829 }
830 if (options.maxFieldSize !== undefined) {
831 map.set(MaxFieldSize.key, ByteSize.fromInputUnsafe(options.maxFieldSize))
832 }
833 if (options.maxFileSize !== undefined) {
834 map.set(MaxFileSize.key, ByteSize.fromInputUnsafe(options.maxFileSize))
835 }
836 if (options.maxTotalSize !== undefined) {
837 map.set(IncomingMessage.MaxBodySize.key, ByteSize.fromInputUnsafe(options.maxTotalSize))
838 }
839 if (options.fieldMimeTypes !== undefined) {
840 map.set(FieldMimeTypes.key, options.fieldMimeTypes)
841 }
842 return Context.makeUnsafe(map)
843}
844
845/**
846 * Namespace containing multipart parser limit option types.
847 *
848 * @stability unstable
849 * @since 4.0.0
850 */
851export declare namespace withLimits {
852 /**
853 * Options for overriding multipart parser limits.
854 *
855 * **Details**
856 *
857 * These settings control maximum part count, field size, file size, total body
858 * size, and MIME types that should be treated as fields instead of files.
859 *
860 * @stability unstable
861 * @category options
862 * @since 4.0.0
863 */
864 export type Options = {
865 readonly maxParts?: number | undefined
866 readonly maxFieldSize?: ByteSize.Input | undefined
867 readonly maxFileSize?: ByteSize.Input | undefined
868 readonly maxTotalSize?: ByteSize.Input | undefined
869 readonly fieldMimeTypes?: ReadonlyArray<string> | undefined
870 }
871}
872
873/**
874 * Context reference for the maximum number of multipart parts allowed.
875 *
876 * **Details**
877 *
878 * The default is `undefined`, meaning no explicit part-count limit.
879 *
880 * @stability unstable
881 * @category services
882 * @since 4.0.0
883 */
884export const MaxParts = Context.Reference<number | undefined>("effect/http/Multipart/MaxParts", {
885 defaultValue: () => undefined
886})
887
888/**
889 * Context reference for the maximum size of a multipart field value.
890 *
891 * **Details**
892 *
893 * The default limit is 10 MiB.
894 *
895 * @stability unstable
896 * @category services
897 * @since 4.0.0
898 */
899export const MaxFieldSize = Context.Reference<ByteSize.ByteSize>("effect/http/Multipart/MaxFieldSize", {
900 defaultValue: constant(ByteSize.mebibytes(10))
901})
902
903/**
904 * Context reference for the maximum size of a multipart file part.
905 *
906 * **Details**
907 *
908 * The default is `undefined`, meaning no explicit per-file limit.
909 *
910 * @stability unstable
911 * @category services
912 * @since 4.0.0
913 */
914export const MaxFileSize = Context.Reference<ByteSize.ByteSize | undefined>(
915 "effect/http/Multipart/MaxFileSize",
916 { defaultValue: () => undefined }
917)
918
919/**
920 * Context reference for MIME type fragments that should be parsed as multipart
921 * fields instead of files.
922 *
923 * **Details**
924 *
925 * The default treats `application/json` parts as fields.
926 *
927 * @stability unstable
928 * @category services
929 * @since 4.0.0
930 */
931export const FieldMimeTypes = Context.Reference<ReadonlyArray<string>>("effect/http/Multipart/FieldMimeTypes", {
932 defaultValue: constant(["application/json"])
933})