Custom Error Handler

Use OpenAPI error schemas with a custom PowerShell runtime error response script for OpenAPI routes.

Full source

File: pwsh/tutorial/examples/10.26-OpenAPI-Custom-Error-Handler.ps1

<#
    Sample: OpenAPI Custom Error Handler
    Purpose: Demonstrate OpenAPI error schema + custom PowerShell runtime error response script.
    File:    10.26-OpenAPI-Custom-Error-Handler.ps1
#>
param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)

New-KrLogger | Add-KrSinkConsole |
    Set-KrLoggerLevel -Value Debug |
    Register-KrLogger -Name 'console' -SetAsDefault

New-KrServer -Name 'OpenAPI Custom Error Handler'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress

# OpenAPI metadata
Add-KrOpenApiInfo -Title 'Orders API - Custom Error Handler' -Version '1.0.0' -Description 'OpenAPI routes with a custom runtime error response script.'
Set-KrOpenApiErrorSchema -Name 'ApiError' -ContentType @('application/problem+json', 'application/json')

# Runtime custom error response script
Set-KrPowerShellErrorResponse -ScriptBlock {
    $payload = [ordered]@{
        status = $StatusCode
        title = 'Request failed'
        detail = $ErrorMessage
        path = [string]$Context.Request.Path
        timestamp = (Get-Date).ToUniversalTime().ToString('o')
    }

    Write-KrJsonResponse -InputObject $payload -StatusCode $StatusCode -ContentType 'application/problem+json'
}

[OpenApiSchemaComponent(RequiredProperties = ('id', 'sku', 'quantity'))]
class OrderDto {
    [OpenApiProperty(Description = 'Order identifier', Example = 1001)]
    [int]$id

    [OpenApiProperty(Description = 'Item sku', Example = 'SKU-RED-001')]
    [string]$sku

    [OpenApiProperty(Description = 'Quantity requested', Example = 2)]
    [int]$quantity
}

[OpenApiSchemaComponent(RequiredProperties = ('status', 'title', 'detail', 'path', 'timestamp'))]
class ApiError {
    [OpenApiProperty(Description = 'HTTP status code', Example = 500)]
    [int]$status

    [OpenApiProperty(Description = 'Short error summary', Example = 'Request failed')]
    [string]$title

    [OpenApiProperty(Description = 'Detailed error message', Example = 'Unhandled exception')]
    [string]$detail

    [OpenApiProperty(Description = 'Request path', Example = '/orders/13')]
    [string]$path

    [OpenApiProperty(Description = 'UTC timestamp', Example = '2026-02-17T10:11:12.0000000Z')]
    [OpenApiDateTime]$timestamp
}

[OpenApiSchemaComponent(RequiredProperties = ('sku', 'quantity'))]
class CreateOrderRequest {
    [OpenApiProperty(Description = 'Item sku', Example = 'SKU-RED-001')]
    [string]$sku

    [OpenApiProperty(Description = 'Quantity requested', Example = 2)]
    [int]$quantity
}

Enable-KrConfiguration

<#
.SYNOPSIS
    Get an order by ID.
.DESCRIPTION
    Returns a sample order payload and raises a runtime exception for order 13 to demonstrate
    the custom PowerShell error response handler on an OpenAPI route.
.PARAMETER orderId
    The order identifier from the route path.
#>
function getOrder {
    [OpenApiPath(HttpVerb = 'get', Pattern = '/orders/{orderId}', Tags = ('orders'))]
    [OpenApiResponse(StatusCode = '200', Description = 'Order found', Schema = [OrderDto], ContentType = 'application/json')]
    [OpenApiResponse(StatusCode = '500', Description = 'Server error', Schema = [ApiError], ContentType = ('application/problem+json', 'application/json'))]
    param(
        [OpenApiParameter(In = [OaParameterLocation]::Path, Required = $true)]
        [int]$orderId
    )

    if ($orderId -eq 13) {
        throw [System.InvalidOperationException]::new('Order service unavailable for order 13.')
    }

    Write-KrResponse -InputObject @{
        id = $orderId
        sku = 'SKU-RED-001'
        quantity = 2
    } -StatusCode 200
}

<#
.SYNOPSIS
    Create a new order.
.DESCRIPTION
    Accepts an OpenAPI-described JSON request body and returns a created order. Invalid quantity
    raises a runtime exception to exercise the custom error response script.
.PARAMETER body
    The order creation payload bound from the request body.
#>
function createOrder {
    [OpenApiPath(HttpVerb = 'post', Pattern = '/orders', Tags = ('orders'))]
    [OpenApiResponse(StatusCode = '201', Description = 'Order created', Schema = [OrderDto], ContentType = 'application/json')]
    [OpenApiResponse(StatusCode = '415', Description = 'Unsupported media type', Schema = [ApiError], ContentType = ('application/problem+json', 'application/json'))]
    [OpenApiResponse(StatusCode = '500', Description = 'Server error', Schema = [ApiError], ContentType = ('application/problem+json', 'application/json'))]
    param(
        [OpenApiRequestBody(Description = 'Order payload', Required = $true, ContentType = 'application/json')]
        [CreateOrderRequest]$body
    )

    if ($body.quantity -le 0) {
        throw [System.ArgumentOutOfRangeException]::new('quantity', 'Quantity must be greater than zero.')
    }

    Write-KrResponse -InputObject @{
        id = 2001
        sku = $body.sku
        quantity = $body.quantity
    } -StatusCode 201
}

Add-KrOpenApiRoute
Add-KrApiDocumentationRoute -DocumentType Swagger
Build-KrOpenApiDocument | Out-Null
Test-KrOpenApiDocument | Out-Null

Start-KrServer -CloseLogsOnExit

Step-by-step

  1. Server and logger: Create the server, endpoint, and console logger.
  2. OpenAPI metadata: Add OpenAPI info and set the error schema contract with Set-KrOpenApiErrorSchema.
  3. Custom runtime handler: Register Set-KrPowerShellErrorResponse with a scriptblock that builds a consistent ProblemDetails-style payload.
  4. Schema components: Define OrderDto, CreateOrderRequest, and ApiError as reusable OpenAPI components.
  5. GET route: Add /orders/{orderId} with OpenAPI responses and throw for a specific ID to trigger the custom error path.
  6. POST route: Add /orders with JSON request body and OpenAPI 415/500 error responses.
  7. OpenAPI routes: Expose /openapi/* and Swagger docs, then build and validate the document.
  8. Run: Start the server and verify both success and error behavior.

Try it

# Success path
curl -i http://127.0.0.1:5000/orders/1

# Runtime exception path (custom 500 payload)
curl -i http://127.0.0.1:5000/orders/13

# Media type contract error (custom 415 payload)
curl -i -X POST http://127.0.0.1:5000/orders \
  -H "Content-Type: text/plain" \
  -d "bad"

# OpenAPI document
curl -i http://127.0.0.1:5000/openapi/v3.1/openapi.json

PowerShell equivalent for POST:

Invoke-WebRequest -Uri http://127.0.0.1:5000/orders `
    -Method Post `
    -ContentType 'text/plain' `
    -Body 'bad' `
    -SkipHttpErrorCheck | Select-Object StatusCode, Content

Key points

  • Set-KrOpenApiErrorSchema controls the OpenAPI contract for generated/standardized error responses.
  • Set-KrPowerShellErrorResponse controls runtime payload shape when PowerShell route execution hits an error path.
  • This pattern is OpenAPI-focused: your route docs and runtime errors stay aligned around a shared ApiError schema.

Troubleshooting

  • 415 does not return JSON: send Accept: application/json in your request while validating payload shape.
  • Custom payload not applied: ensure Set-KrPowerShellErrorResponse runs before Start-KrServer and the scriptblock does not throw.
  • OpenAPI missing error schema: verify Set-KrOpenApiErrorSchema is configured before Build-KrOpenApiDocument.

References


Previous / Next

Previous: Additional and Pattern Properties Next: Razor