DEV Community

Cover image for Solved: PowerShell Script to Detect Code Impacted by the Invoke-WebRequest Breaking Change
Darian Vance
Darian Vance

Posted on • Originally published at wp.me

Solved: PowerShell Script to Detect Code Impacted by the Invoke-WebRequest Breaking Change

🚀 Executive Summary

TL;DR: PowerShell 7+ introduced a breaking change to Invoke-WebRequest by removing its Internet Explorer engine dependency, causing the .Content property to return a stream instead of a string and leading to “null-valued expression” errors. The solution involves either using the -UseBasicParsing switch for a quick fix, explicitly converting .Content.ToString() for raw HTML, or leveraging Invoke-RestMethod for structured data, alongside a proactive script scanner to identify all impacted code.

🎯 Key Takeaways

  • The Invoke-WebRequest breaking change in PowerShell 7+ stems from the removal of its Internet Explorer engine dependency, altering the .Content property from a string to a stream.
  • The -UseBasicParsing switch offers an immediate, temporary fix to restore the old string behavior for Invoke-WebRequest, though it is considered technical debt.
  • For a permanent solution, explicitly convert the content using $response.Content.ToString() for raw HTML, or utilize Invoke-RestMethod for structured data like JSON, which automatically parses the response into a PSCustomObject.
  • A PowerShell script can be deployed to scan codebases for Invoke-WebRequest calls that are not using -UseBasicParsing or Invoke-RestMethod, proactively identifying scripts vulnerable to this breaking change.

A senior engineer’s practical guide to finding and fixing PowerShell scripts affected by the Invoke-WebRequest breaking change in PS7+, with real-world code and solutions.

Caught Off Guard: A Senior Engineer’s Guide to Surviving the PowerShell Invoke-WebRequest Breaking Change

It was 2 AM. Of course it was. A critical deployment pipeline for our ‘Phoenix’ project was glowing red. The error was one of those infuriatingly vague ones: “Cannot index into a null-valued expression.” The script in question, a simple health checker, hadn’t been touched in over a year. It ran perfectly on our older build agents, but on the shiny new build-agent-07 we’d just provisioned with the latest OS and PowerShell 7, it fell apart. After an hour of digging, we found the culprit: a single line with Invoke-WebRequest. This is the kind of change that doesn’t show up in a PR; it just ambushes you when you upgrade the ground beneath your feet. It’s a reminder that even the most stable code can be broken by its environment.

So, What Actually Broke? The “Why” Behind the Pain

Let’s get this straight: this change was for the better, even if it caused us some late-night grief. In the old days of Windows PowerShell (version 5.1 and below), Invoke-WebRequest relied on the Internet Explorer engine (mshtml.dll) to parse HTML. It was slow, clunky, and tied PowerShell to a legacy browser component.

Starting with PowerShell Core (6+) and now in PowerShell 7, the team rightfully ripped out that dependency. The cmdlet now uses a more modern, cross-platform engine. The side effect? The object it returns is different.

Here’s what you probably used to do:

# The Old Way (Windows PowerShell 5.1)
$response = Invoke-WebRequest -Uri "http://prod-status-page.internal"
$htmlContent = $response.Content # This was just a big, beautiful string of HTML
Enter fullscreen mode Exit fullscreen mode

In PowerShell 7+, the .Content property is no longer a simple string. It’s a stream, and trying to treat it like the old string object is what causes those “null-valued expression” errors when you try to slice it or run a regex on it.

Fixing The Mess: From Band-Aids to Surgery

Okay, you’ve identified the problem. Your scripts are failing and management is asking for an ETA. Here are your options, from the “get it working NOW” fix to the “let’s do this right” solution.

Solution 1: The “Get-Me-Home-For-Dinner” Fix

This is the quickest way to restore the old behavior. You can tell Invoke-WebRequest to skip the advanced parsing and just give you the raw content, much like it used to. You do this with the -UseBasicParsing switch.

# The Quick Fix
$response = Invoke-WebRequest -Uri "http://prod-status-page.internal" -UseBasicParsing
$htmlContent = $response.Content # Hooray, it's a string again!
Enter fullscreen mode Exit fullscreen mode

Warning: While -UseBasicParsing is a lifesaver in an emergency, it’s technical debt. You’re opting out of the modern engine. Use it to get your systems back online, but plan to implement a proper fix later. It’s a band-aid, not a cure.

Solution 2: The “Do It Right” Permanent Fix

The modern, correct way to handle this is to work with the new object model. Instead of grabbing .Content, you should now access the parsed data from the appropriate properties, or explicitly convert the content stream to a string if that’s what you need.

If you’re dealing with structured data like JSON, PowerShell 7 is even smarter:

# The Modern Way (for APIs)
# The -ContentType "application/json" is key here
$apiResponse = Invoke-RestMethod -Uri "https://api.internal/v1/health" -ContentType "application/json"

# $apiResponse is now a PSCustomObject, no manual conversion needed!
if ($apiResponse.status -eq 'OK') {
    Write-Host "API on prod-api-gateway-01 is healthy."
}
Enter fullscreen mode Exit fullscreen mode

For HTML, if you really just need the raw text, you can be explicit:

# The Modern Way (for raw HTML)
$response = Invoke-WebRequest -Uri "http://prod-status-page.internal"
$htmlContent = $response.Content.ToString() # Explicitly convert the stream to a string

# Now you can use $htmlContent as you did before
if ($htmlContent -match 'All Systems Operational') {
    Write-Host "Status page confirmed OK."
}
Enter fullscreen mode Exit fullscreen mode

This is the path forward. It embraces the new engine and ensures your scripts will be compatible with future versions of PowerShell.

Solution 3: The “Nuclear Option” – Find All The Ticking Time Bombs

Okay, you’ve fixed the immediate fire, but how many other scripts are lurking in your codebase, waiting to explode the next time a server gets upgraded? This is where we go on the offensive.

I saw a great discussion on Reddit that sparked this idea. We can write a PowerShell script… to find broken PowerShell scripts. It’s a bit meta, but it’s exactly what a DevOps practice is all about: automating detection.

Here’s a simple scanner you can run against your script repositories. It looks for any .ps1 file that uses Invoke-WebRequest but conveniently forgets to use our friend -UseBasicParsing or its modern cousin Invoke-RestMethod.

# Script-Scanner.ps1 - Find potentially impacted scripts

param(
    [string]$Path = "C:\Scripts\Repo"
)

$impactedFiles = Get-ChildItem -Path $Path -Recurse -Filter "*.ps1" | ForEach-Object {
    $filePath = $_.FullName
    $fileContent = Get-Content -Path $filePath -Raw

    # Look for Invoke-WebRequest that ISN'T followed by -UseBasicParsing
    # This regex is a bit simplistic but works for most common cases.
    if ($fileContent -match 'Invoke-WebRequest(?!.*(-UseBasicParsing|Invoke-RestMethod))') {
        # We found a potential match. Let's find the line number.
        $matchInfo = Select-String -Path $filePath -Pattern 'Invoke-WebRequest' -AllMatches

        foreach ($match in $matchInfo) {
            # Further filter to ignore the ones we know are safe
            if ($match.Line -notmatch '-UseBasicParsing' -and $match.Context.PostContext -notmatch '-UseBasicParsing') {
                [PSCustomObject]@{
                    File      = $filePath
                    LineNumber = $match.LineNumber
                    CodeLine  = $match.Line.Trim()
                }
            }
        }
    }
}

if ($impactedFiles) {
    Write-Host "Found potentially impacted scripts! Review these immediately:" -ForegroundColor Yellow
    $impactedFiles | Format-Table
} else {
    Write-Host "Scan complete. No obvious uses of legacy Invoke-WebRequest found." -ForegroundColor Green
}
Enter fullscreen mode Exit fullscreen mode

Running this script against our entire library of deployment and maintenance scripts gave us a clear, actionable list of technical debt. It turned an unknown threat into a manageable backlog item. And that, my friends, is how you get a good night’s sleep.


Darian Vance

👉 Read the original article on TechResolve.blog


☕ Support my work

If this article helped you, you can buy me a coffee:

👉 https://buymeacoffee.com/darianvance

Top comments (0)