Resources / Field guide

When PATCH returns 500 on Azure

Every verb works except PATCH, which 500s in production while the exact same logic over POST or PUT is fine. The culprit isn't your code — it's a module IIS turns on by default. Here's how to confirm it, route around it fast, and fix it properly.

The symptom

On a recent deployment, every PATCH endpoint started returning 500 in the Azure environment — and only PATCH. GET, POST, and PUT all worked. It broke every "edit a field" operation in the app at once: rename, recategorize, update notes — anything wired to PATCH.

What makes this one nasty is that it looks like an application bug. The natural instinct is to dig into the handler. But here's the tell: compare two endpoints that run nearly identical code — one over POST, one over PATCH — both do "find the row, change some fields, save." If only the PATCH one 500s, the verb itself is the variable, not your logic. That points downstream of your code, at the host.

The cause: IIS's WebDAV module

The most common cause of this on an IIS-hosted .NET app is the WebDAV module. WebDAV is an old protocol for editing files over HTTP, and its IIS module registers itself for verbs like PUT, DELETE, and PATCH. When it's enabled, it can intercept those requests before they ever reach your application and reject them — surfacing as a 500 (or a 405) that never shows up in your app logs, because your app never ran.

On Azure App Service (Windows) this module is present by default, which is why the failure appears in the cloud but not necessarily on your machine.

The fast workaround: route PATCH over PUT

When you need the app working now and don't want to touch host config, lean on a verb WebDAV leaves alone. The trick is to make the same action reachable over both PATCH and PUT — one attribute change, zero logic change:

// Accept the update over BOTH verbs so it's reachable over the one that works
[HttpPatch("{id}")]
[HttpPut("{id}")]
public async Task<IActionResult> UpdateResource(int id, [FromBody] ResourceUpdate dto)
{
    var row = await _db.Resources.FindAsync(id);
    if (row is null) return NotFound();

    row.Apply(dto);          // same logic that PATCH always ran
    await _db.SaveChangesAsync();
    return NoContent();
}

Then point the client at PUT:

// before:  PATCH /api/resources/42
// after:   PUT   /api/resources/42      (identical body)

Deploy order matters. The new PUT route doesn't exist on the live server until the backend is redeployed. If you ship the client first, it calls a route that isn't there yet and gets a 404 instead of the old 500 — looks like a new bug, same root cause. Deploy the API first, confirm the PUT route answers, then ship the client.

The proper fix: remove the WebDAV module

The workaround routes around the problem; this removes it. Add a web.config (or edit the one your publish produces) to strip the WebDAV module and handler so PATCH passes straight through to your app:

<configuration>
  <system.webServer>
    <modules>
      <remove name="WebDAVModule" />
    </modules>
    <handlers>
      <remove name="WebDAV" />
    </handlers>
  </system.webServer>
</configuration>

With the module gone, native PATCH works normally and you can drop the dual-verb shim. Recycle the app pool (a redeploy does this) for the change to take effect.

How to confirm it's WebDAV (not you)

  • Isolate the verb. Two near-identical endpoints, different verbs — if only PATCH fails, it's the host, not the handler.
  • Check whether your app even ran. No entry in your application logs for the failed request is a strong signal the request was rejected before reaching ASP.NET Core.
  • Test after removing the module. If PATCH starts working with the web.config above and nothing else changed, WebDAV was the cause.

When to use which

Reach for the PUT workaround when you're mid-incident and need the feature back without a host-config change you can't fully test — it matches how the rest of a typical REST API already behaves. Apply the web.config fix when you want PATCH to work the way it's supposed to and keep your routes clean. Most teams ship the workaround first to stop the bleeding, then land the proper fix in the next deploy.

Stuck on a production-only bug?

The gnarliest bugs are the ones that only happen in the cloud. I debug and ship fixes for real ASP.NET / Azure systems, directly. If you want senior eyes on it, let's talk.

Work with me