Multiple files (same field name)

Accept multiple files posted under a single field name.

Full source

File: pwsh/tutorial/examples/22.2-Multiple-Files.ps1

<#!
    22.2 multipart/form-data with multiple files under the same field name

    Client example (PowerShell):
        $Port = 5000
        $client = [System.Net.Http.HttpClient]::new()
        $content = [System.Net.Http.MultipartFormDataContent]::new()
        $content.Add([System.Net.Http.StringContent]::new('Batch upload'),'note')
        $file1 = [System.Net.Http.ByteArrayContent]::new([System.Text.Encoding]::UTF8.GetBytes('file-1'))
        $file1.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('text/plain')
        $content.Add($file1,'files','one.txt')
        $file2 = [System.Net.Http.ByteArrayContent]::new([System.Text.Encoding]::UTF8.GetBytes('file-2'))
        $file2.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('text/plain')
        $content.Add($file2,'files','two.txt')
        $resp = $client.PostAsync("http://127.0.0.1:$Port/upload", $content).Result
        $resp.Content.ReadAsStringAsync().Result

    Cleanup:
        Remove-Item -Recurse -Force (Join-Path ([System.IO.Path]::GetTempPath()) 'kestrun-uploads-22.2-multiple-files')
#>
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.2'

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"

New-KrFormPartRule -Name 'files' -Required -AllowedContentTypes 'text/plain' |
    Add-KrFormOption -DefaultUploadPath $uploadRoot -ComputeSha256 |
    Add-KrFormRoute -Pattern '/upload' -ScriptBlock {
        $files = @($FormPayload.Files['files'])
        $result = [pscustomobject]@{
            count = $files.Count
            files = $files | ForEach-Object {
                [pscustomobject]@{
                    fileName = $_.OriginalFileName
                    length = $_.Length
                    sha256 = $_.Sha256
                }
            }
        }
        Write-KrJsonResponse -InputObject $result -StatusCode 200
    }

Enable-KrConfiguration

# Start the server asynchronously
Start-KrServer

Step-by-step

  1. Logger: Register console logging for visibility.
  2. Server: Create and bind a listener.
  3. Storage: Configure a temporary upload directory.
  4. Options: Enable SHA-256 hashes for stored parts.
  5. Route: Add /upload with Add-KrFormRoute.
  6. Response: Return the list of uploaded files and counts.

Try it

$client = [System.Net.Http.HttpClient]::new()
$content = [System.Net.Http.MultipartFormDataContent]::new()
$content.Add([System.Net.Http.StringContent]::new('batch'),'note')
$file1 = [System.Net.Http.ByteArrayContent]::new([System.Text.Encoding]::UTF8.GetBytes('one'))
$file1.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('text/plain')
$content.Add($file1,'files','one.txt')
$file2 = [System.Net.Http.ByteArrayContent]::new([System.Text.Encoding]::UTF8.GetBytes('two'))
$file2.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('text/plain')
$content.Add($file2,'files','two.txt')
$client.PostAsync('http://127.0.0.1:5000/upload', $content).Result.Content.ReadAsStringAsync().Result
curl -F "note=batch" -F "files=@one.txt" -F "files=@two.txt" http://127.0.0.1:5000/upload

Expected output

{
  "count": 2,
  "files": [
    { "fileName": "one.txt", "length": 3, "sha256": "..." },
    { "fileName": "two.txt", "length": 3, "sha256": "..." }
  ]
}

Notes

  • Accessing files: Add-KrFormRoute injects a $FormPayload variable. Read files from $FormPayload.Files.
  • Multiple values: a single field name can map to multiple files (e.g. $FormPayload.Files['files']).
  • Limits: KrFormOptions.Limits.MaxParts protects against too many sections.
  • Security: Filenames are sanitized; uploads are stored in a temp directory.
  • Logging: Each part is logged with metadata only through host.Logger.

Troubleshooting

  • 415 / Unsupported Content-Type: Ensure you post multipart/form-data.
  • Missing files: Ensure you use the same field name (files) for each file part.

References


Previous / Next

Previous: Basic multipart/form-data upload Next: application/x-www-form-urlencoded forms