Skip to content

Remoting: handled errors return 424 (Failed Dependency) #1492

@michaellwest

Description

@michaellwest

Problem

Remoting scripts that explicitly handle non-terminating errors return HTTP 424 "Failed Dependency" even though the script completed successfully from the caller's perspective. The 424 trips Invoke-RemoteScript into throwing an HttpRequestException, breaking common test-setup and "create-if-missing" idioms.

Patterns that incorrectly produce 424 today:

Get-Item 'master:/never-existed' -ErrorAction SilentlyContinue
Get-Item 'master:/never-existed' -ErrorAction Ignore
Get-Item 'master:/never-existed' 2>$null
try { Get-Item 'master:/never-existed' -ErrorAction Stop } catch { }
if (-not (Get-Role -Identity 'sitecore\NeverExisted' -ErrorAction SilentlyContinue)) { New-Role ... }

The downstream effect surfaced when integration test setup scripts (Remoting.DelegatedAccess.Setup.ps1, Remoting.WebApi.Tests.ps1, etc.) ran against a cold container; the first-run "if exists" check on a missing role/user/item flipped hasErrors=True and returned 424 before any work happened.

Root cause

Commit 0dbef9e59 ("Enrich remoting audit; honest hasErrors and 424 for runtime failures") expanded the hasErrors signal that drives the 424 status code from:

// Before
var hasErrors = session.Output.HasErrors;

to:

// After
var hasErrors = session.Output.HasErrors
    || session.LastInvocationHadErrors
    || (session.LastErrors != null && session.LastErrors.Count > 0);

The intent was to catch parameter-binding errors and pipeline-output ErrorRecord failures that bypassed the host UI. The implementation was over-aggressive: both LastInvocationHadErrors and LastErrors are populated from powerShell.Streams.Error, which is filled by any error in the pipeline, including ones explicitly suppressed by the caller.

Sitecore's PSProvider compounds this. It writes errors via WriteError in a way that bypasses every user-level suppression mechanism: -ErrorAction SilentlyContinue, -ErrorAction Ignore, try/catch around -ErrorAction Stop, and 2>$null redirect all leave the error in Streams.Error. The script reads "successful" from PowerShell's POV but the C# layer flags it as a runtime failure.

Proposed solution

Refine the hasErrors signal so it fires only for unhandled failures, leaving LastErrors populated for the JSON / CliXml response paths that consume it as their "errors" channel:

// In ScriptSession.ExecuteCommand: drop the LastInvocationHadErrors=true
// set inside the HadErrors branch. Keep LastErrors populating; keep the
// pipeline-output ErrorRecord detector that follows.

// In ScriptProcessor: drop the LastErrors.Count > 0 clause.
var hasErrors = session.Output.HasErrors      // host UI WriteErrorLine
    || session.LastInvocationHadErrors;        // pipeline-output ErrorRecord

Coverage:

  • Output.HasErrors flips when ScriptingHostUserInterface.WriteErrorLine is called. Write-Error, default-action errors that reach Out-Default, and parameter binding errors that the host renders all flow here.
  • LastInvocationHadErrors (after the change) flips only via the existing pipeline-output ErrorRecord detector — the case 0dbef9e59 explicitly called out (Out-Default re-emitting a binding failure as output).
  • Uncaught terminating exceptions are caught by the existing catch (Exception ex) block in ProcessScript and set 424 there, independent of hasErrors.

Verified empirically

Script Before After
'hello' 200 200
Get-Item 'master:/none' -ErrorAction SilentlyContinue; 'ok' 424 200
Get-Item 'master:/none' -ErrorAction Ignore; 'ok' 424 200
Get-Item 'master:/none' 2>$null; 'ok' 424 200
try { Get-Item 'master:/none' -ErrorAction Stop } catch { }; 'ok' 424 200
Get-Role -Identity 'sitecore\NeverExisted' -ErrorAction SilentlyContinue 424 200
Test-Path 'master:/none' 200 200
try { throw 'BOOM' } catch { 'caught' } 200 200
throw 'BOOM' (uncaught) 424 424
1/0 (uncaught terminating) 424 424
Write-Error 'foo' 424 424
Get-Item -BogusBindingParam x (default action) 424 424
Pipeline-output ErrorRecord 424 424

After the change, the Remoting.DelegatedAccess.Setup.ps1 script that reproduced the failure completes cleanly:

Setup result: ROLE_EXISTS|USER_EXISTS|SCRIPT:{343976DF-...}|DA:{9112958E-...}

622 unit tests pass, 534 integration tests pass.

Files touched

  • src/Spe/Core/Host/ScriptSession.csExecuteCommand no longer flips LastInvocationHadErrors from HadErrors. The pipeline-output ErrorRecord detector that follows is unchanged.
  • src/Spe/sitecore modules/PowerShell/Services/Processors/ScriptProcessor.cshasErrors OR drops the LastErrors.Count > 0 clause.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions