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

  1. Logger: Enable console logging.
  2. Server: Create the host and bind a listener.
  3. Options: Opt in to multipart/mixed and allow one level of nested multipart content.
  4. Route: Add /nested with Add-KrFormRoute.
  5. 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-KrFormRoute defaults to multipart/form-data only; this script opts in to multipart/mixed.
  • Limits: MaxNestingDepth defaults 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, include Content-Disposition: ...; name="..." in outer and nested parts.

Troubleshooting

  • 415 / Unsupported Content-Type: Ensure the outer request Content-Type is multipart/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