The Deploy That Was Secretly Broken: A Backslash in Your Zip Can Corrupt Every Release

For months, deploying one of my Azure App Services (a Blazor Server app) via az webapp deploy threw an rsync warning — exit code 123, a Kudu Status: 400 buried in the output. The app came back up fine every time. The page loaded. So I filed it under “noisy but harmless” and moved on.

It wasn’t harmless. It was quietly corrupting a subset of every deploy I’d shipped for months.

The symptom that finally forced the issue

An intermittent CultureNotFoundException started showing up — not on every request, not on every environment, just often enough to be infuriating. Nothing in the code had changed around it. The exception pointed at culture-specific satellite resource DLLs, the kind ASP.NET Core generates automatically for localisation and rarely anyone thinks about.

Satellite DLLs live in subdirectories — bin/publish/fr/App.resources.dll, that sort of layout. That detail turned out to matter a lot.

What was actually happening

Compress-Archive — PowerShell’s built-in zip cmdlet — writes backslash-separated paths for zip entries that represent subdirectories. So does .NET Framework’s own [System.IO.Compression.ZipFile]::CreateFromDirectory() when called from Windows PowerShell 5.1. An entry like:

code
wwwroot\Components\Layout\ReconnectModal.razor.js

is technically invalid per the zip spec, which calls for forward slashes regardless of platform. Windows tools have gotten away with this for decades because Windows zip consumers tolerate it.

Linux’s rsync — which is what Azure’s Kudu deployment engine uses under the hood to unpack and sync your zip into the App Service filesystem — does not tolerate it. It can’t parse a backslash as a directory separator, so instead of writing wwwroot/Components/Layout/ReconnectModal.razor.js, it either drops the file outright or writes it as a single mangled filename with literal backslashes in it. Either way, the file that should exist at that path doesn’t.

Most of the app kept working because most of it doesn’t depend on deeply nested subdirectory files surviving the trip intact. The satellite resource DLLs did, and they’re exactly the kind of file nobody watches for after a deploy — it takes a specific culture, on a specific request, to notice they’re gone.

Confirming it

You can check any existing zip for the smoking gun without redeploying anything:

powershell
[System.IO.Compression.ZipFile]::OpenRead($zipPath).Entries |
    Where-Object { $_.FullName.IndexOf([char]92) -ge 0 }

If that returns anything, your zip has backslash paths in it, and whatever’s consuming it on the Linux side is either dropping or mangling those entries right now.

The fix

Stop using Compress-Archive for anything headed to a Linux-backed deploy target. Build the zip manually, entry by entry, forcing forward slashes:

powershell
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem

$sourceDir = "...\publish"
$zipPath   = "...\publish.zip"
$bs = [char]92
$fs = [char]47

$zip = [System.IO.Compression.ZipFile]::Open($zipPath, [System.IO.Compression.ZipArchiveMode]::Create)
Get-ChildItem -Path $sourceDir -Recurse -File | ForEach-Object {
    $entryName = $_.FullName.Substring($sourceDir.Length + 1).Replace($bs, $fs)
    [System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile(
        $zip, $_.FullName, $entryName, [System.IO.Compression.CompressionLevel]::Optimal
    ) | Out-Null
}
$zip.Dispose()

Then deploy as normal:

powershell
az webapp deploy --type zip --clean true --restart true --src-path $zipPath ...

With a correctly-built zip, the deploy reports genuine success — no rsync warnings at all. I confirmed this twice in a row after months of “false failures” that turned out not to be false.

The lesson

If a deploy tool has been throwing a warning “forever” and the app still boots, that’s not evidence the warning is safe to ignore — it’s evidence the warning only affects a subset of files you haven’t happened to notice missing yet. Treat a persistent deploy-time warning as a real defect until you’ve specifically ruled it out, especially anywhere a Windows-built artifact crosses onto a Linux-hosted target. Path separators are one of those assumptions that silently work almost all the time, right up until they don’t.