Common Status Codes
Try common HTTP status codes (401/403/404/405/415/400/422/200/201/204) on a small set of routes.
Full source
File: pwsh/tutorial/examples/17.8-StatusCodePages-Common-Status-Codes.ps1
<#
Sample: Common HTTP status codes
Purpose: Provide a small API surface to manually try common HTTP status codes
(401/403/404/405/415/400/200/201/204) across GET/POST/DELETE.
File: 17.8-StatusCodePages-Common-Status-Codes.ps1
Notes:
- Basic auth is used only to demonstrate real 401/403 behavior.
- The /secure/resource/{id} DELETE route requires policy CanDelete.
- /json/echo requires Content-Type: application/json (else 415).
To test:
$script:instance=@{Url="http://127.0.0.1:5000"}
$script:adminAuth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('admin:password'))
$script:userAuth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('user:password'))
$script:badAuth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes('admin:wrong'))
$resp = Invoke-WebRequest -Uri "$($script:instance.Url)/public" -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 200
$resp = Invoke-WebRequest -Uri "$($script:instance.Url)/secure/hello" -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 401
$resp = Invoke-WebRequest -Uri "$($script:instance.Url)/secure/hello" -Headers @{ Authorization = $script:badAuth } -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 401
$resp = Invoke-WebRequest -Uri "$($script:instance.Url)/secure/hello" -Headers @{ Authorization = $script:adminAuth } -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 200
$resp = Invoke-WebRequest -Method Delete -Uri "$($script:instance.Url)/secure/resource/1" -Headers @{ Authorization = $script:adminAuth } -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 403
$resp = Invoke-WebRequest -Method Delete -Uri "$($script:instance.Url)/secure/resource/1" -Headers @{ Authorization = $script:userAuth } -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 403
$resp =curl -X POST "$($script:instance.Url)/json/echo" -H "Content-Type:" --data 'hi'
if ($resp -match 'Status:\s*415' -and
$resp -match 'Content-Type header is required\. Supported types: application/json'){ Write-Host "✅ Test passed"}else{ Write-Host "❌ Test failed. Response was:`n$resp"}
$resp = Invoke-WebRequest -Method Post -Uri "$($script:instance.Url)/json/echo" -Body 'hi' -ContentType 'text/plain' -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 415
$resp = Invoke-WebRequest -Method Post -Uri "$($script:instance.Url)/json/echo" -Body '{"a":' -ContentType 'application/json' -SkipCertificateCheck -SkipHttpErrorCheck
$resp.StatusCode -eq 400
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Tutorial sample uses basic auth demo credentials.')]
param(
[int]$Port = 5000,
[IPAddress]$IPAddress = [IPAddress]::Loopback
)
New-KrLogger |
Set-KrLoggerLevel -Value Debug |
Add-KrSinkConsole |
Register-KrLogger -Name 'console' -SetAsDefault | Out-Null
New-KrServer -Name 'Status Codes'
Add-KrEndpoint -Port $Port -IPAddress $IPAddress | Out-Null
# =========================================================
# TOP-LEVEL OPENAPI
# =========================================================
Add-KrOpenApiInfo -Title 'Status Codes' -Version '1.0.0' -Description 'Endpoints to exercise common HTTP status codes.'
Add-KrOpenApiContact -Email 'support@example.com'
# =========================================================
# COMPONENT SCHEMAS
# =========================================================
[OpenApiSchemaComponent(
Description = 'JSON echo request payload.', RequiredProperties = ('name', 'quantity')
)]
class JsonEchoRequest {
[OpenApiProperty(Description = 'Friendly name', Example = 'widget')]
[ValidateNotNullOrEmpty()]
[string]$name
[OpenApiProperty(Description = 'Quantity (1..100)', Example = 2)]
[ValidateRange(1, 100)]
[int]$quantity
[OpenApiProperty(Description = 'Optional priority', Example = 'normal')]
[ValidateSet('low', 'normal', 'high')]
[string]$priority
}
[OpenApiSchemaComponent(Description = 'JSON echo request payload.', AdditionalPropertiesAllowed = $true, AdditionalProperties = [bool])]
class JsonEchoRequestPlus: JsonEchoRequest {}
# =========================================================
# 401/403 DEMO (Basic auth used only to trigger real 401/403)
# =========================================================
$claimConfig = New-KrClaimPolicy |
Add-KrClaimPolicy -PolicyName 'CanRead' -ClaimType 'can_read' -AllowedValues 'true' |
Add-KrClaimPolicy -PolicyName 'CanWrite' -ClaimType 'can_write' -AllowedValues 'true' |
Add-KrClaimPolicy -PolicyName 'CanDelete' -ClaimType 'can_delete' -AllowedValues 'true' |
Build-KrClaimPolicy
Add-KrBasicAuthentication -AuthenticationScheme 'StatusBasic' -Realm 'StatusCodes' -AllowInsecureHttp -ScriptBlock {
param($Username, $Password)
Write-KrLog -Level Debug -Message "Authenticating user '{username}'." -Values $Username
if ($Password -ne 'password') { return $false }
return ($Username -eq 'admin' -or $Username -eq 'user')
} -IssueClaimsScriptBlock {
param($Identity)
if ($Identity -eq 'admin') {
return (
Add-KrUserClaim -ClaimType 'can_read' -Value 'true' |
Add-KrUserClaim -ClaimType 'can_write' -Value 'true'
)
}
if ($Identity -eq 'user') {
return (Add-KrUserClaim -ClaimType 'can_read' -Value 'true')
}
} -ClaimPolicyConfig $claimConfig
# =========================================================
# ROUTES
# =========================================================
<#
.SYNOPSIS
Public endpoint.
#>
function getPublic {
[OpenApiPath(HttpVerb = 'get', Pattern = '/public', Summary = 'Public health check', Tags = ('StatusCodes'))]
[OpenApiResponse(StatusCode = '200', Description = 'OK', ContentType = 'text/plain', Schema = [string])]
param()
Write-KrTextResponse -Text 'OK' -StatusCode 200
}
<#
.SYNOPSIS
Protected endpoint (401 when missing/invalid credentials).
#>
function getSecureHello {
[OpenApiPath(HttpVerb = 'get', Pattern = '/secure/hello', Summary = 'Protected GET (Basic auth)', Tags = ('StatusCodes'))]
[OpenApiAuthorization(Scheme = 'StatusBasic')]
[OpenApiResponse(StatusCode = '200', Description = 'OK', ContentType = 'application/json', Schema = [object])]
[OpenApiResponse(StatusCode = '401', Description = 'Unauthorized')]
param()
Write-KrJsonResponse -InputObject @{ message = 'hello'; user = $Context.User.Identity.Name } -StatusCode 200
}
<#
.SYNOPSIS
Delete a resource (403 when authenticated but missing CanDelete).
.DESCRIPTION
This endpoint deletes a resource identified by its ID.
.PARAMETER id
The ID of the resource to delete.
#>
function deleteSecureResource {
[OpenApiPath(HttpVerb = 'delete', Pattern = '/secure/resource/{id}', Summary = 'Protected DELETE (requires CanDelete)', Tags = ('StatusCodes'))]
[OpenApiAuthorization(Scheme = 'StatusBasic', Policies = 'CanDelete')]
[OpenApiResponse(StatusCode = '204', Description = 'Deleted')]
[OpenApiResponse(StatusCode = '401', Description = 'Unauthorized')]
[OpenApiResponse(StatusCode = '403', Description = 'Forbidden')]
param(
[OpenApiParameter(In = 'path', Required = $true, Description = 'Resource id')]
[int]$id
)
Write-KrLog -Level Information -Message 'Delete requested for resource {id}' -Values $id
Write-KrStatusResponse -StatusCode 204
}
<#
.SYNOPSIS
JSON echo (415 when Content-Type is not application/json; 400 when body is invalid JSON).
.DESCRIPTION
This endpoint echoes back the received JSON payload.
.PARAMETER body
The JSON body to echo back.
#>
function postJsonEcho {
[OpenApiPath(HttpVerb = 'post', Pattern = '/json/echo', Summary = 'POST requires application/json', Tags = ('StatusCodes'))]
[OpenApiResponse(StatusCode = '201', Description = 'Created', ContentType = 'application/json', Schema = [object])]
[OpenApiResponse(StatusCode = '400', Description = 'Bad Request')]
[OpenApiResponse(StatusCode = '422', Description = 'Unprocessable Entity')]
[OpenApiResponse(StatusCode = '415', Description = 'Unsupported Media Type')]
param(
[OpenApiRequestBody( ContentType = 'application/json', Required = $true)]
[JsonEchoRequest]$body
)
Expand-KrObject -InputObject $body
Write-KrResponse -InputObject @{ received = $body } -StatusCode 201
}
<#
.SYNOPSIS
JSON echo (415 when Content-Type is not application/json; 400 when body is invalid JSON).
.DESCRIPTION
This endpoint echoes back the received JSON payload.
.PARAMETER body
The JSON body to echo back.
#>
function postJsonEchoPlus {
[OpenApiPath(HttpVerb = 'post', Pattern = '/json/echoPlus', Summary = 'POST requires application/json', Tags = ('StatusCodes'))]
[OpenApiResponse(StatusCode = '201', Description = 'Created', ContentType = ('application/json', 'application/xml'), Schema = [object])]
[OpenApiResponse(StatusCode = '400', Description = 'Bad Request')]
[OpenApiResponse(StatusCode = '422', Description = 'Unprocessable Entity')]
[OpenApiResponse(StatusCode = '415', Description = 'Unsupported Media Type')]
param(
[OpenApiRequestBody( ContentType = ('application/json', 'application/yaml'), Required = $true)]
[JsonEchoRequestPlus]$body
)
Expand-KrObject -InputObject $body
Write-KrResponse -InputObject @{ received = $body } -StatusCode 201
}
<#
.SYNOPSIS
GET-only endpoint (POST will produce 405 Method Not Allowed).
.DESCRIPTION
This endpoint only supports the GET method.
#>
function getOnlyGet {
[OpenApiPath(HttpVerb = 'get', Pattern = '/only-get', Summary = 'GET-only endpoint', Tags = ('StatusCodes'))]
[OpenApiResponse(StatusCode = '200', Description = 'OK', ContentType = 'text/plain', Schema = [string])]
param()
Write-KrResponse -InputObject 'GET OK' -StatusCode 200
}
<#
.SYNOPSIS
Demonstrate no-content response contract enforcement.
.DESCRIPTION
Declares a 200 response without content and intentionally writes a payload.
Runtime should reject this and return a 500 contract error.
#>
function testNoContentContract {
[OpenApiPath(HttpVerb = 'get', Pattern = '/test')]
[OpenApiResponse(StatusCode = '200', Description = 'No content response contract')]
param()
Write-KrResponse @{ message = 'This payload violates the no-content contract.' } -StatusCode 200
}
Enable-KrConfiguration
# =========================================================
# OPENAPI DOC ROUTE / UI
# =========================================================
Add-KrOpenApiRoute
Add-KrApiDocumentationRoute -DocumentType Swagger
Add-KrApiDocumentationRoute -DocumentType Redoc
Start-KrServer
Step-by-step
- Logging: Register a console logger.
- OpenAPI: Set a title/version and expose OpenAPI routes.
- 401/403 demo: Configure Basic auth only to trigger real 401/403 responses.
- Routes: Add endpoints that return 200/201/204 and trigger 400/415/422/405/404.
- Start: Apply configuration and start the server.
Try it
# 200
curl -i http://127.0.0.1:5000/public
# 401 (no credentials)
curl -i http://127.0.0.1:5000/secure/hello
# 401 (bad credentials)
curl -i -H "Authorization: Basic YWRtaW46d3Jvbmc=" http://127.0.0.1:5000/secure/hello
# 201
curl -i -X POST http://127.0.0.1:5000/json/echo -H "Content-Type: application/json" -d '{"name":"widget","quantity":2,"priority":"normal"}'
# 415 (wrong content type)
curl -i -X POST http://127.0.0.1:5000/json/echo -H "Content-Type: text/plain" -d 'hi'
# 405
curl -i -X POST http://127.0.0.1:5000/only-get
# 404
curl -i http://127.0.0.1:5000/does-not-exist
OpenAPI Contract Enforcement
This sample demonstrates runtime behaviors that can also be represented in generated OpenAPI:
- 415 Unsupported Media Type when request
Content-Typedoes not match declared request body content types. - 400 Bad Request for malformed payloads.
- 422 Unprocessable Entity for validation failures after successful parsing.
- 406 Not Acceptable when response negotiation cannot satisfy the
Acceptheader.
For predictable OpenAPI output, configure the auto-generated error schema before building docs:
Set-KrOpenApiErrorSchema -Name 'KestrunErrorResponse' -ContentType @('application/problem+json')
This keeps the OpenAPI 4xx response schema/media type aligned with the runtime contract you are exercising in this tutorial.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
401 when you expected 200 on /secure/hello | Missing/invalid Authorization header | Send valid Basic auth credentials or try /public for a non-auth route |
403 on DELETE /secure/resource/{id} even with valid credentials | Authenticated user does not meet the CanDelete policy | This sample intentionally blocks deletes; use it only to demonstrate 403 Forbidden |
415 from /json/echo | Content-Type is missing or not application/json | Set -ContentType 'application/json' (PowerShell) or -H "Content-Type: application/json" (curl) |
400 from /json/echo | Invalid JSON body | Fix JSON syntax and retry |
422 from /json/echo | JSON is valid but fails validation | Provide name and a quantity in 1..100 (and omit unknown fields unless using echoPlus) |
References
- Add-KrBasicAuthentication
- Write-KrStatusResponse
- Write-KrResponse
- Add-KrOpenApiRoute
- Set-KrOpenApiErrorSchema
Previous / Next
Previous: Re-execute Next: Exception Handling