Basic multipart/form-data upload

Parse a simple multipart upload with one text field and one file.

Full source

File: pwsh/tutorial/examples/22.1-Basic-Multipart.ps1

<#!
    22.1 Basic multipart/form-data upload

    Client example (PowerShell):
        $client = [System.Net.Http.HttpClient]::new()
        $content = [System.Net.Http.MultipartFormDataContent]::new()

        $content.Add([System.Net.Http.StringContent]::new('Hello from client'), 'note')
        $bytes = [System.Text.Encoding]::UTF8.GetBytes('sample file')
        $fileContent = [System.Net.Http.ByteArrayContent]::new($bytes)
        $fileContent.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('text/plain')
        $content.Add($fileContent, 'file', 'hello.txt')
        $resp = $client.PostAsync("http://127.0.0.1:$Port/upload", $content).Result
        $resp.Content.ReadAsStringAsync().Result
        $content.Dispose()
        $client.Dispose()

    Cleanup:
        Remove-Item -Recurse -Force (Join-Path ([System.IO.Path]::GetTempPath()) 'kestrun-uploads-22.1-basic-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.1'

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 'file' -Required -AllowedContentTypes 'text/plain' -AllowOnlyOne |
    Add-KrFormOption -DefaultUploadPath $uploadRoot -ComputeSha256 |
    Add-KrFormRoute -Pattern '/upload' -ScriptBlock {
        $files = foreach ($entry in $FormPayload.Files.GetEnumerator()) {
            foreach ($file in $entry.Value) {
                [pscustomobject]@{
                    name = $file.Name
                    fileName = $file.OriginalFileName
                    contentType = $file.ContentType
                    length = $file.Length
                    sha256 = $file.Sha256
                }
            }
        }
        $fields = @{}
        foreach ($key in $FormPayload.Fields.Keys) {
            $fields[$key] = $FormPayload.Fields[$key]
        }
        Write-KrJsonResponse -InputObject @{ fields = $fields; files = $files } -StatusCode 200
    }

Enable-KrConfiguration

# Start the server asynchronously
Start-KrServer

Step-by-step

  1. Logger: Register a console logger for visibility.
  2. Server: Create the Kestrun host and bind a listener.
  3. Storage: Pick a temporary upload directory under the OS temp folder.
  4. Options: Enable SHA-256 hashing for stored files.
  5. Route: Add a POST /upload route with Add-KrFormRoute.
  6. Response: Return parsed fields and file metadata as JSON.

Try it

$client = [System.Net.Http.HttpClient]::new()
$content = [System.Net.Http.MultipartFormDataContent]::new()
$content.Add([System.Net.Http.StringContent]::new('Hello'),'note')
$bytes = [System.Text.Encoding]::UTF8.GetBytes('sample file')
$file = [System.Net.Http.ByteArrayContent]::new($bytes)
$file.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('text/plain')
$content.Add($file,'file','hello.txt')
$client.PostAsync('http://127.0.0.1:5000/upload', $content).Result.Content.ReadAsStringAsync().Result
curl -F "note=Hello" -F "file=@hello.txt;type=text/plain" http://127.0.0.1:5000/upload

Expected output

{
  "fields": {
    "note": ["Hello"]
  },
  "files": [
    {
      "name": "file",
      "fileName": "hello.txt",
      "contentType": "text/plain",
      "length": 11,
      "sha256": "..."
    }
  ]
}

Notes

  • Limits: KrFormOptions.Limits controls max request size, part size, and field size.
  • Security: Filenames are sanitized and stored in a temporary directory.
  • Logging: Parsing events are written through host.Logger without logging field values.

Troubleshooting

  • 415 / Unsupported Content-Type: Ensure you post multipart/form-data.
  • Empty files list: Ensure your multipart uses the expected field name (file).

References


Previous / Next

Previous: Localization Next: Multiple files (same field name)