Response Compression

Reduce payload size (bandwidth + latency) for text-based responses by enabling HTTP content encoding (gzip, brotli, deflate) when the client sends an Accept-Encoding header.

The compression middleware sits early in the pipeline and, for eligible MIME types, wraps the response body stream so data is compressed on-the-fly.

Aspect Details
Enabled For HTTPS Yes (opt-in with -EnableForHttps)
Negotiation Chooses first supported algorithm from client Accept-Encoding (br, gzip, deflate)
Typical Savings 60–95% for verbose JSON / HTML / XML payloads
CPU Cost Low for gzip, higher for brotli (better ratio)
Skip Heuristics Very small bodies may be skipped (no Content-Encoding)

Binary formats (e.g. already-compressed images) are not included by default; focus on text-centric content types.

Full source

File: pwsh/tutorial/examples/10.2-Compression.ps1

<#
    Compression Demo Script
    Demonstrates response compression for multiple content types.
    Routes return sufficiently large payloads so you can observe the
    `Content-Encoding` header (gzip/brotli/deflate) when the client
    sends an appropriate `Accept-Encoding`.

    Try (PowerShell 7+):
        irm https://127.0.0.1:5000/text    -SkipCertificateCheck -MaximumRedirection 0 -Headers @{ 'Accept-Encoding'='gzip' } -Verbose
        (Invoke-WebRequest -Uri https://127.0.0.1:5000/json -SkipCertificateCheck -Headers @{ 'Accept-Encoding'='br, gzip' }).Headers.'Content-Encoding'

    NOTE: Very small responses may be excluded from compression depending
          on server heuristics; all payloads below are padded to be >1KB.
#>
param(
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback
)

# 1. Logging (console only for clarity)
New-KrLogger |
    Add-KrSinkConsole |
    Register-KrLogger -Name 'console' -SetAsDefault

# 2. Server
New-KrServer -Name 'Compression Demo'

# 3. Listener (HTTP + self-signed HTTPS)
Add-KrEndpoint -Port $Port -IPAddress $IPAddress -SelfSignedCert | Out-Null

<#
.SYNOPSIS
    Generate a large block of text by repeating a seed string.
.PARAMETER seed
    The seed string to repeat.
.PARAMETER repeat
    The number of times to repeat the seed string (default: 80).
.RETURNS
    A large string composed of the seed repeated.
#>
function _NewLargeBlock([string]$seed, [int]$repeat = 80) {
    return (($seed + ' ') * $repeat).Trim()
}


# 5. Compression middleware (cover common textual MIME types)
Add-KrCompressionMiddleware -EnableForHttps -MimeTypes @('text/plain', 'text/html', 'application/json', 'application/xml', 'application/x-www-form-urlencoded')

# 6. Lock in baseline configuration
Enable-KrConfiguration

# /text   (plain text)
Add-KrMapRoute -Verbs Get -Pattern '/text' -ScriptBlock {
    $body = _NewLargeBlock 'PlainText payload demonstrating compression' 120
    Write-Verbose "[/text] length=$($body.Length)" -Verbose
    Write-KrTextResponse -InputObject $body -StatusCode 200
}

# /json   (JSON array)
Add-KrMapRoute -Verbs Get -Pattern '/json' -ScriptBlock {
    $items = 1..150 | ForEach-Object { [pscustomobject]@{ id = $_; message = 'Compression JSON sample record ' + $_ } }
    Write-Verbose "[/json] count=$($items.Count)" -Verbose
    Write-KrJsonResponse -InputObject $items -StatusCode 200
}

# /html   (HTML page)
Add-KrMapRoute -Verbs Get -Pattern '/html' -ScriptBlock {
    $paras = 1..40 | ForEach-Object { '<p>Lorem ipsum dolor sit amet (para ' + $_ + ')</p>' }
    $html = @"
<!DOCTYPE html>
<html>
<head><meta charset='utf-8'><title>Compression Demo</title></head>
<body>
<h1>HTML Compression Demo</h1>
$($paras -join "`n")
</body>
</html>
"@
    Write-KrHtmlResponse -Template $html -StatusCode 200
}

# /xml    (XML serialization of objects)
Add-KrMapRoute -Verbs Get -Pattern '/xml' -ScriptBlock {
    $data = 1..120 | ForEach-Object { [pscustomobject]@{ Index = $_; Text = 'Sample XML record ' + $_ } }
    Write-Verbose "[/xml] count=$($data.Count)" -Verbose
    Write-KrXmlResponse -InputObject $data -StatusCode 200
}

# /form   (url-encoded style body)
Add-KrMapRoute -Verbs Get -Pattern '/form' -ScriptBlock {
    $pairs = 1..200 | ForEach-Object { "k$($_)=value$($_)" }
    $payload = ($pairs -join '&')
    Write-Verbose "[/form] pairs=$($pairs.Count) length=$($payload.Length)" -Verbose
    $Context.Response.ContentType = 'application/x-www-form-urlencoded'
    $Context.Response.Body = $payload
    $Context.Response.StatusCode = 200
}

# /raw-nocompress  (large payload explicitly excluded from compression)
Add-KrMapRoute -Options (New-KrMapRouteOption -Property @{
        Pattern = '/raw-nocompress'
        HttpVerbs = 'Get'
        ScriptCode = @{
            Code = {
                $body = _NewLargeBlock 'This route intentionally disables response compression.' 150
                Write-Verbose "[/raw-nocompress] length=$($body.Length)" -Verbose
                Write-KrTextResponse -InputObject $body -StatusCode 200 -ContentType 'text/plain'
            }
            Language = 'PowerShell'
        }
        DisableResponseCompression = $true
    })

# /info   (reflect Accept-Encoding to show negotiation)
Add-KrMapRoute -Verbs Get -Pattern '/info' -ScriptBlock {
    $ae = Get-KrRequestHeader -Name 'Accept-Encoding'
    $body = [pscustomobject]@{
        AcceptEncoding = $ae
        Tip = 'Use -Headers @{"Accept-Encoding"="gzip"} (or br,deflate) to see compressed responses.'
        Routes = '/text,/json,/html,/xml,/form,/raw-nocompress'
        NonCompressedExample = '/raw-nocompress'
    }
    Write-KrJsonResponse -InputObject $body -StatusCode 200
}

# Start server
Start-KrServer -CloseLogsOnExit

# Close default logger when process exits
Close-KrLogger

Step-by-step

  1. Add the compression middleware with desired MIME types and HTTPS behavior.
  2. Map routes that emit sizable text/JSON/HTML/XML bodies for meaningful compression.
  3. Commit configuration and start the server.
  4. Call routes with Accept-Encoding header (e.g., gzip, br) to negotiate compression.
  5. Inspect Content-Encoding on responses to verify compression.

Try it

Route Type Notes
/text text/plain Large repeated block >1KB
/json application/json Array of objects (n=150)
/html text/html Many <p> tags to grow payload
/xml application/xml Serialized collection
/form application/x-www-form-urlencoded Key=value pairs joined with &
/info application/json Echoes Accept-Encoding to aid debugging

How negotiation works

  1. Client sends Accept-Encoding: br, gzip, deflate (order denotes preference).
  2. Server picks first matching implementation it supports.
  3. Middleware sets Content-Encoding header and compresses the body stream.
  4. Size-sensitive heuristics (or excluded MIME type) → response left uncompressed.

Open a terminal and run the sample (PowerShell 7+):

pwsh .\docs\_includes\examples\pwsh\10.2-Compression.ps1

Then exercise routes (add -Headers @{ 'Accept-Encoding'='gzip' } or include brotli):

# Plain text
(Invoke-WebRequest https://127.0.0.1:5000/text -SkipCertificateCheck -Headers @{ 'Accept-Encoding'='gzip' }).Headers.'Content-Encoding'

# JSON
(Invoke-WebRequest https://127.0.0.1:5000/json -SkipCertificateCheck -Headers @{ 'Accept-Encoding'='br, gzip' }).Headers.'Content-Encoding'

# HTML
(Invoke-WebRequest https://127.0.0.1:5000/html -SkipCertificateCheck -Headers @{ 'Accept-Encoding'='gzip' }).Headers.'Content-Encoding'

# Info (see what you sent)
Invoke-RestMethod https://127.0.0.1:5000/info -SkipCertificateCheck -Headers @{ 'Accept-Encoding'='gzip' }

Testing strategy

Key assertions for automated tests:

  • Eligible routes return status 200.
  • Without Accept-Encoding, either no Content-Encoding or allowed default.
  • With Accept-Encoding: gzip, response includes Content-Encoding: gzip for large bodies.
  • /info reflects the header value.
  • Body lengths differ (compressed smaller) — len(compressed) < len(uncompressed) for same route.

Disabling / tuning

Omit routes or MIME types not desired:

Add-KrCompressionMiddleware -MimeTypes @('text/plain','application/json')

Skip HTTPS compression by removing -EnableForHttps (some platforms historically disabled HTTPS compression to mitigate certain attacks; modern mitigations usually make it safe for typical APIs).

Troubleshooting

Symptom Cause Fix
No Content-Encoding header Body below minimum size / MIME type excluded Increase payload size or add MIME type
Large CPU increase Brotli at high quality Prefer gzip in latency-sensitive APIs
Double compression Reverse proxy already compressing Disable at one layer

References


Previous / Next

Previous: Antiforgery Protection Next: Rate Limiting (planned)