Private/Invoke-RemoteCheck.ps1
|
# Copyright (c) 2026 Jeffrey Snover. All rights reserved. # Licensed under the MIT License. See LICENSE file in the project root. function Invoke-RemoteCheck { <# .SYNOPSIS Makes an HTTP request to a remote endpoint and returns a structured result. .DESCRIPTION Default-GET smoke-check helper used by Test-TaxEditorEndpoints and Test-AnonymousDebateFlow. Optional -Body, -Session, and -ExtraHeaders allow POST/PUT flows with cookie persistence across calls (anonymous session smoke test, etc). #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$BaseUrl, [Parameter(Mandatory)] [string]$Path, [Parameter()] [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] [string]$Method = 'GET', [Parameter()] [int]$TimeoutSec = 10, [Parameter()] [string]$ExpectedField, [Parameter()] [int[]]$AcceptableStatusCodes = @(200), # Optional request body. Hashtables/objects are JSON-encoded automatically # and Content-Type is set to application/json. [Parameter()] [object]$Body, # Optional shared WebRequestSession for cookie persistence across calls. [Parameter()] [Microsoft.PowerShell.Commands.WebRequestSession]$Session, # Optional extra headers merged on top of the default Accept: application/json. [Parameter()] [hashtable]$ExtraHeaders, # t/1474: when set, Success is $false if the response Content-Type is text/html. # Opt-in so callers that expect non-JSON responses (third-party-notices) are unaffected. [Parameter()] [switch]$ExpectJson ) $Url = "$BaseUrl$Path" $Sw = [System.Diagnostics.Stopwatch]::StartNew() $Headers = @{ 'Accept' = 'application/json' } if ($ExtraHeaders) { foreach ($K in $ExtraHeaders.Keys) { $Headers[$K] = $ExtraHeaders[$K] } } $WebParams = @{ Uri = $Url Method = $Method TimeoutSec = $TimeoutSec UseBasicParsing = $true ErrorAction = 'Stop' Headers = $Headers } if ($PSBoundParameters.ContainsKey('Body') -and $null -ne $Body) { if ($Body -is [string]) { $WebParams.Body = $Body } else { $WebParams.Body = ($Body | ConvertTo-Json -Depth 10 -Compress) } $WebParams.ContentType = 'application/json' } if ($Session) { $WebParams.WebSession = $Session } try { $Response = Invoke-WebRequest @WebParams $Sw.Stop() $StatusCode = $Response.StatusCode $ContentType = '' if ($Response.Headers -and $Response.Headers['Content-Type']) { # Headers[key] returns string[] in PS7 — take the first entry. $ContentType = @($Response.Headers['Content-Type'])[0] } # RawBody is a short slice for shape-matching (SPA-shell detection, etc). t/1355. # Bumped 400 → 4096 (t/1500#7): this app's <head> is 464+ chars before # Vite's script injection, so `<div id="root">` and `src="...js"` both # fall past a 400-char slice → SPA-shell check always fails on `/`. # 4096 gives ~10× headroom for future <head> growth without turning # RawBody into a full-page buffer. $RawBody = '' if ($Response.Content) { $RawBody = if ($Response.Content.Length -gt 4096) { $Response.Content.Substring(0, 4096) } else { $Response.Content } } $Body = $null if ($Response.Content) { try { $Body = $Response.Content | ConvertFrom-Json } catch { $Body = $null } } $Success = $StatusCode -in $AcceptableStatusCodes # t/1474 — catch the auth-shell trap: server returns 200 text/html # (login page) for API paths when the session cookie is missing. if ($Success -and $ExpectJson -and $ContentType -match 'text/html') { $Success = $false } # t/1474 — the previous guard `-and $Body` short-circuited when # ConvertFrom-Json failed on HTML, leaving Success=$true. Drop $Body # from the outer guard; move it inside as a negative check so a missing # or unparseable body flips Success. if ($Success -and $ExpectedField) { if (-not $Body -or -not $Body.PSObject.Properties[$ExpectedField]) { $Success = $false } } [PSCustomObject]@{ Success = $Success StatusCode = $StatusCode ResponseMs = $Sw.ElapsedMilliseconds Body = $Body ContentType = $ContentType RawBody = $RawBody Error = $null } } catch { $Sw.Stop() $StatusCode = 0 $ContentType = '' $RawBody = '' if ($_.Exception.PSObject.Properties['Response'] -and $_.Exception.Response) { $StatusCode = [int]$_.Exception.Response.StatusCode # Best-effort content-type extraction from the error-response headers # (used by the persona-endpoint 401/403 classifier — non-200 with HTML # body is a real signal for auth-shell distinguisher). try { if ($_.Exception.Response.Content -and $_.Exception.Response.Content.Headers.ContentType) { $ContentType = $_.Exception.Response.Content.Headers.ContentType.MediaType } } catch { } try { $ErrResp = $_.ErrorDetails if ($ErrResp -and $ErrResp.Message) { $ErrText = [string]$ErrResp.Message $RawBody = if ($ErrText.Length -gt 400) { $ErrText.Substring(0, 400) } else { $ErrText } } } catch { } } [PSCustomObject]@{ Success = $false StatusCode = $StatusCode ResponseMs = $Sw.ElapsedMilliseconds Body = $null ContentType = $ContentType RawBody = $RawBody Error = $_.Exception.Message } } } |