Nested multipart/mixed
Handle a single nested multipart/mixed payload inside an ordered multipart body.
Full source
File: pwsh/tutorial/examples/22.5-Nested-Multipart.ps1
<#!
22.5 nested multipart/mixed (one level)
Client example (PowerShell):
$outer = 'outer-boundary'
$inner = 'inner-boundary'
$innerBody = @(
"--$inner",
"Content-Type: text/plain",
"",
"inner-1",
"--$inner",
"Content-Type: application/json",
"",
'{"nested":true}',
"--$inner--",
""
) -join "`r`n"
$outerBody = @(
"--$outer",
"Content-Type: application/json",
"",
'{"stage":"outer"}',
"--$outer",
"Content-Type: multipart/mixed; boundary=$inner",
"",
$innerBody,
"--$outer--",
""
) -join "`r`n"
Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$Port/nested" -ContentType "multipart/mixed; boundary=$outer" -Body $outerBody
Cleanup:
Remove-Item -Recurse -Force (Join-Path ([System.IO.Path]::GetTempPath()) 'kestrun-uploads-22.5-nested-multipart')
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
New-KrLogger |
Set-KrLoggerLevel -Value Debug |
Add-KrSinkConsole |
Register-KrLogger -Name 'console' -SetAsDefault
New-KrServer -Name 'Forms 22.5'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress | Out-Null
# Upload directory
$scriptName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$uploadRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "kestrun-uploads-$scriptName"
# Add Rules
# Note: nested multipart is parsed as ordered parts; rules apply when a part includes a Content-Disposition name.
#New-KrFormPartRule -Name 'outer' -MaxBytes 1024 |
#New-KrFormPartRule -Name 'nested' -MaxBytes (1024 * 1024) |
New-KrFormPartRule -Name 'outer' -Required -MaxBytes 1024 `
-AllowOnlyOne `
-AllowedContentTypes 'application/json' |
New-KrFormPartRule -Name 'nested' -Required -MaxBytes (1024 * 1024) `
-AllowOnlyOne `
-AllowedContentTypes 'multipart/mixed' |
# These apply only inside the nested multipart container (Scope = 'nested')
New-KrFormPartRule -Name 'text' -Scope 'nested' -Required -MaxBytes 1024 `
-AllowOnlyOne `
-AllowedContentTypes 'text/plain' |
New-KrFormPartRule -Name 'json' -Scope 'nested' -Required -MaxBytes 4096 `
-AllowOnlyOne `
-AllowedContentTypes 'application/json' |
Add-KrFormOption -DefaultUploadPath $uploadRoot -AllowedRequestContentTypes 'multipart/mixed' -MaxNestingDepth 1 |
Add-KrFormRoute -Pattern '/nested' -ScriptBlock {
$outerParts = $FormPayload.Parts
$nestedSummary = @()
foreach ($part in $outerParts) {
if ($null -ne $part.NestedPayload) {
$nestedSummary += [pscustomobject]@{
outerContentType = $part.ContentType
nestedCount = $part.NestedPayload.Parts.Count
}
}
}
Write-KrJsonResponse -InputObject @{ outerCount = $outerParts.Count; nested = $nestedSummary } -StatusCode 200
}
Enable-KrConfiguration
# Start the server asynchronously
Start-KrServer
Step-by-step
- Logger: Enable console logging.
- Server: Create the host and bind a listener.
- Options: Opt in to
multipart/mixedand allow one level of nested multipart content. - Route: Add
/nestedwithAdd-KrFormRoute. - Response: Summarize outer parts and nested counts.
Try it
$outer = 'outer-boundary'
$inner = 'inner-boundary'
$innerBody = @(
"--$inner",
"Content-Disposition: form-data; name=\"text\"",
"Content-Type: text/plain",
"",
"inner-1",
"--$inner",
"Content-Disposition: form-data; name=\"json\"",
"Content-Type: application/json",
"",
'{"nested":true}',
"--$inner--",
""
) -join "`r`n"
$outerBody = @(
"--$outer",
"Content-Disposition: form-data; name=\"outer\"",
"Content-Type: application/json",
"",
'{"stage":"outer"}',
"--$outer",
"Content-Disposition: form-data; name=\"nested\"",
"Content-Type: multipart/mixed; boundary=$inner",
"",
$innerBody,
"--$outer--",
""
) -join "`r`n"
Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:5000/nested' -ContentType "multipart/mixed; boundary=$outer" -Body $outerBody
cat > payload.txt <<'EOF'
--outer-boundary
Content-Disposition: form-data; name="outer"
Content-Type: application/json
{"stage":"outer"}
--outer-boundary
Content-Disposition: form-data; name="nested"
Content-Type: multipart/mixed; boundary=inner-boundary
--inner-boundary
Content-Disposition: form-data; name="text"
Content-Type: text/plain
inner-1
--inner-boundary
Content-Disposition: form-data; name="json"
Content-Type: application/json
{"nested":true}
--inner-boundary--
--outer-boundary--
EOF
curl -X POST -H "Content-Type: multipart/mixed; boundary=outer-boundary" --data-binary @payload.txt http://127.0.0.1:5000/nested
Expected output
{
"outerCount": 2,
"nested": [
{ "outerContentType": "multipart/mixed", "nestedCount": 2 }
]
}
Notes
- Content types:
Add-KrFormRoutedefaults tomultipart/form-dataonly; this script opts in tomultipart/mixed. - Limits:
MaxNestingDepthdefaults to 1; increase with care. - Security: Nested multipart parts are stored to disk before parsing.
- Logging: Nested parsing decisions are logged via
host.Logger. - Rules: If you configure
KrPartRule, includeContent-Disposition: ...; name="..."in outer and nested parts.
Troubleshooting
- 415 / Unsupported Content-Type: Ensure the outer request
Content-Typeismultipart/mixed; boundary=...and the route options allow it. - Nested parts not detected: Ensure the nested part has
Content-Type: multipart/mixed; boundary=...and includes its own boundary markers.
References
Previous / Next
Previous: multipart/mixed ordered parts Next: Request-level compression