Content Negotiation
Write-KrResponse provides content negotiation and automatic format selection based on the client’s Accept header. It chooses the appropriate response format (JSON, XML, text, etc.) and sets the correct Content-Type header.
Full source
File: pwsh/tutorial/examples/9.9-Content-Negotiation.ps1
<#
Sample: Content Negotiation
Purpose: Demonstrate content negotiation and automatic format selection in a Kestrun server.
File: 9.9-Content-Negotiation.ps1
Notes: Shows how Write-KrResponse chooses format based on Accept header.
#>
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
# 1. Logging
New-KrLogger | Add-KrSinkConsole | Register-KrLogger -Name 'console' -SetAsDefault
# 2. Server
New-KrServer -Name 'Responses 9.9'
# 3. Listener
Add-KrEndpoint -IPAddress $IPAddress -Port $Port
# Finalize configuration
Enable-KrConfiguration
# Content negotiation route
Add-KrMapRoute -Pattern '/negotiate' -Verbs GET -ScriptBlock {
$data = @{
message = 'Hello World'
timestamp = (Get-Date).ToUniversalTime()
id = 123
}
Write-KrResponse -InputObject $data -StatusCode 200
}
# Explicit format route
Add-KrMapRoute -Pattern '/explicit' -Verbs GET -ScriptBlock {
$text = 'Explicit plain text response'
Write-KrResponse -InputObject $text -StatusCode 200 -ContentType 'text/plain'
}
# Error response route
Add-KrMapRoute -Pattern '/error' -Verbs GET -ScriptBlock {
$errorData = @{
error = 'Not Found'
code = 404
details = 'The requested resource was not found'
}
Write-KrResponse -InputObject $errorData -StatusCode 404
}
# Start the server
Start-KrServer
Step-by-step
- Logging: Register console logger as default.
- Server: Create server named ‘Responses 9.9’.
- Listener: Listen on 127.0.0.1:5000.
/negotiate: Uses Write-KrResponse with object, format determined by Accept header./explicit: Uses Write-KrResponse with explicit ContentType override./error: Uses Write-KrResponse with error object and 404 status code.- Enable configuration and start server.
Try it
# Test content negotiation - JSON (default)
curl -i http://127.0.0.1:5000/negotiate
# Request JSON explicitly
curl -i -H "Accept: application/json" http://127.0.0.1:5000/negotiate
# Request XML format
curl -i -H "Accept: application/xml" http://127.0.0.1:5000/negotiate
# Request plain text
curl -i -H "Accept: text/plain" http://127.0.0.1:5000/negotiate
# Explicit content type
curl -i http://127.0.0.1:5000/explicit
# Error response
curl -i http://127.0.0.1:5000/error
PowerShell equivalents:
# Test content negotiation - JSON (default)
Invoke-WebRequest -Uri http://127.0.0.1:5000/negotiate | Select-Object -ExpandProperty Headers | Format-List
# Request JSON explicitly
$headers = @{ 'Accept' = 'application/json' }
Invoke-WebRequest -Uri http://127.0.0.1:5000/negotiate -Headers $headers | Select-Object Content, Headers
# Request XML format
$headers = @{ 'Accept' = 'application/xml' }
Invoke-WebRequest -Uri http://127.0.0.1:5000/negotiate -Headers $headers | Select-Object Content, Headers
# Request plain text
$headers = @{ 'Accept' = 'text/plain' }
Invoke-WebRequest -Uri http://127.0.0.1:5000/negotiate -Headers $headers | Select-Object Content, Headers
# Explicit content type
Invoke-WebRequest -Uri http://127.0.0.1:5000/explicit | Select-Object Content, Headers
# Error response (use SkipHttpErrorCheck to avoid exception on 404)
Invoke-WebRequest -Uri http://127.0.0.1:5000/error -SkipHttpErrorCheck | Select-Object StatusCode, Content
Content Format Selection
Automatic Format Selection
Write-KrResponse examines the client’s Accept header and chooses the best matching format:
| Accept Header | Response Format | Content-Type |
|---|---|---|
application/json | JSON | application/json; charset=utf-8 |
application/xml | XML | application/xml; charset=utf-8 |
text/plain | Plain text | text/plain; charset=utf-8 |
text/html | HTML (if supported) | text/html; charset=utf-8 |
*/* or missing | JSON (default) | application/json; charset=utf-8 |
Format Examples
$data = @{ name = 'John'; age = 30; active = $true }
# With Accept: application/json
# Response: {"name":"John","age":30,"active":true}
# Content-Type: application/json; charset=utf-8
# With Accept: application/xml
# Response: <Object><name>John</name><age>30</age><active>true</active></Object>
# Content-Type: application/xml; charset=utf-8
# With Accept: text/plain
# Response: name=John, age=30, active=True
# Content-Type: text/plain; charset=utf-8
Advanced Usage
Explicit Content-Type Override
Add-KrMapRoute -Pattern '/custom' -Verbs GET -ScriptBlock {
$csv = "Name,Age`nJohn,30`nJane,25"
Write-KrResponse -InputObject $csv -ContentType 'text/csv' -StatusCode 200
}
Error Responses with Negotiation
Add-KrMapRoute -Pattern '/api/users/:id' -Verbs GET -ScriptBlock {
$id = $Context.Request.RouteValues['id']
$user = Get-User -Id $id
if (-not $user) {
$errorResponse = @{
error = 'User not found'
code = 'USER_NOT_FOUND'
id = $id
timestamp = (Get-Date).ToUniversalTime()
}
Write-KrResponse -InputObject $errorResponse -StatusCode 404
return
}
Write-KrResponse -InputObject $user -StatusCode 200
}
Complex Object Serialization
Add-KrMapRoute -Pattern '/api/report' -Verbs GET -ScriptBlock {
$report = @{
summary = @{
totalUsers = 1250
activeUsers = 980
newUsers = 45
}
data = @(
@{ date = '2024-01-01'; count = 100 }
@{ date = '2024-01-02'; count = 150 }
@{ date = '2024-01-03'; count = 120 }
)
metadata = @{
generatedAt = (Get-Date).ToUniversalTime()
version = '1.0'
}
}
Write-KrResponse -InputObject $report -StatusCode 200
}
When to Use Write-KrResponse
| Scenario | Benefit |
|---|---|
| REST APIs | Automatic JSON/XML negotiation |
| Multi-format endpoints | Single endpoint serves multiple formats |
| Error responses | Consistent error format across clients |
| Complex objects | Automatic serialization handling |
| Client flexibility | Let clients choose preferred format |
When to Use Specialized Helpers Instead
| Use This | Instead of Write-KrResponse | When |
|---|---|---|
Write-KrJsonResponse | Write-KrResponse with JSON objects | Always want JSON, regardless of Accept header |
Write-KrTextResponse | Write-KrResponse with strings | Always want plain text |
Write-KrXmlResponse | Write-KrResponse with XML | Always want XML format |
Write-KrHtmlResponse | Write-KrResponse with HTML | Always want HTML |
Status Codes
# Success responses
Write-KrResponse -InputObject $data -StatusCode 200 # OK
Write-KrResponse -InputObject $newData -StatusCode 201 # Created
Write-KrResponse -InputObject $null -StatusCode 204 # No Content
# Client error responses
Write-KrResponse -InputObject $errorData -StatusCode 400 # Bad Request
Write-KrResponse -InputObject $authError -StatusCode 401 # Unauthorized
Write-KrResponse -InputObject $notFound -StatusCode 404 # Not Found
Write-KrResponse -InputObject $conflict -StatusCode 409 # Conflict
# Server error responses
Write-KrResponse -InputObject $serverError -StatusCode 500 # Internal Server Error
Write-KrResponse -InputObject $maintenance -StatusCode 503 # Service Unavailable
Best Practices
1. Consistent Error Format
function Write-ApiError {
param(
[string]$Message,
[string]$Code,
[int]$StatusCode = 400,
[hashtable]$Details = @{}
)
$errorResponse = @{
error = @{
message = $Message
code = $Code
timestamp = (Get-Date).ToUniversalTime().ToString('O')
details = $Details
}
}
Write-KrResponse -InputObject $errorResponse -StatusCode $StatusCode
}
# Usage
Add-KrMapRoute -Pattern '/api/validate' -Verbs POST -ScriptBlock {
$input = $Context.Request.Body | ConvertFrom-Json
if (-not $input.email) {
Write-ApiError -Message 'Email is required' -Code 'MISSING_EMAIL' -StatusCode 400
return
}
if ($input.email -notmatch '^[^@]+@[^@]+\.[^@]+$') {
Write-ApiError -Message 'Invalid email format' -Code 'INVALID_EMAIL' -StatusCode 400 -Details @{ email = $input.email }
return
}
Write-KrResponse -InputObject @{ message = 'Valid email' } -StatusCode 200
}
2. Pagination Support
Add-KrMapRoute -Pattern '/api/users' -Verbs GET -ScriptBlock {
$page = [int]($Context.Request.Query['page'] ?? 1)
$limit = [int]($Context.Request.Query['limit'] ?? 10)
$offset = ($page - 1) * $limit
$users = Get-Users -Offset $offset -Limit $limit
$totalCount = Get-UserCount
$response = @{
data = $users
pagination = @{
page = $page
limit = $limit
total = $totalCount
pages = [Math]::Ceiling($totalCount / $limit)
hasNext = ($page * $limit) -lt $totalCount
hasPrev = $page -gt 1
}
}
Write-KrResponse -InputObject $response -StatusCode 200
}
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Wrong format returned | Accept header not sent | Send appropriate Accept header |
| Unexpected content type | Explicit ContentType overrides | Remove or adjust -ContentType |
| XML missing fields | Complex objects serialization | Simplify object shape for XML |
References
Previous / Next
Previous: Caching & Revalidation Next: Low-Level Response Stream