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.cs — ExecuteCommand no longer flips LastInvocationHadErrors from HadErrors. The pipeline-output ErrorRecord detector that follows is unchanged.
src/Spe/sitecore modules/PowerShell/Services/Processors/ScriptProcessor.cs — hasErrors OR drops the LastErrors.Count > 0 clause.
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-RemoteScriptinto throwing anHttpRequestException, breaking common test-setup and "create-if-missing" idioms.Patterns that incorrectly produce 424 today:
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 flippedhasErrors=Trueand returned 424 before any work happened.Root cause
Commit
0dbef9e59("Enrich remoting audit; honest hasErrors and 424 for runtime failures") expanded thehasErrorssignal that drives the 424 status code from:to:
The intent was to catch parameter-binding errors and pipeline-output
ErrorRecordfailures that bypassed the host UI. The implementation was over-aggressive: bothLastInvocationHadErrorsandLastErrorsare populated frompowerShell.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
WriteErrorin a way that bypasses every user-level suppression mechanism:-ErrorAction SilentlyContinue,-ErrorAction Ignore,try/catcharound-ErrorAction Stop, and2>$nullredirect all leave the error inStreams.Error. The script reads "successful" from PowerShell's POV but the C# layer flags it as a runtime failure.Proposed solution
Refine the
hasErrorssignal so it fires only for unhandled failures, leavingLastErrorspopulated for the JSON / CliXml response paths that consume it as their "errors" channel:Coverage:
Output.HasErrorsflips whenScriptingHostUserInterface.WriteErrorLineis called.Write-Error, default-action errors that reachOut-Default, and parameter binding errors that the host renders all flow here.LastInvocationHadErrors(after the change) flips only via the existing pipeline-outputErrorRecorddetector — the case0dbef9e59explicitly called out (Out-Defaultre-emitting a binding failure as output).catch (Exception ex)block inProcessScriptand set 424 there, independent ofhasErrors.Verified empirically
'hello'Get-Item 'master:/none' -ErrorAction SilentlyContinue; 'ok'Get-Item 'master:/none' -ErrorAction Ignore; 'ok'Get-Item 'master:/none' 2>$null; 'ok'try { Get-Item 'master:/none' -ErrorAction Stop } catch { }; 'ok'Get-Role -Identity 'sitecore\NeverExisted' -ErrorAction SilentlyContinueTest-Path 'master:/none'try { throw 'BOOM' } catch { 'caught' }throw 'BOOM'(uncaught)1/0(uncaught terminating)Write-Error 'foo'Get-Item -BogusBindingParam x(default action)ErrorRecordAfter the change, the
Remoting.DelegatedAccess.Setup.ps1script that reproduced the failure completes cleanly:622 unit tests pass, 534 integration tests pass.
Files touched
src/Spe/Core/Host/ScriptSession.cs—ExecuteCommandno longer flipsLastInvocationHadErrorsfromHadErrors. The pipeline-output ErrorRecord detector that follows is unchanged.src/Spe/sitecore modules/PowerShell/Services/Processors/ScriptProcessor.cs—hasErrorsOR drops theLastErrors.Count > 0clause.