Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Resulting JSON is truncated" error with Pode and Pode.Web for API and WEB UI #639

Open
ChrisLynchHPE opened this issue Nov 12, 2024 · 2 comments
Labels
bug 🐛 Something isn't working

Comments

@ChrisLynchHPE
Copy link

ChrisLynchHPE commented Nov 12, 2024

Describe the Bug

Similar to #605. When combining Pode.Web with Pode to provide API and Web Front-End, I get:

WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 10.

Then the state.json is empty.

If I remove all Add-PodeWebPage calls, I do not get this error.

Steps To Reproduce

Import-Module Pode
Import-Module Pode.Web

Start-PodeServer {

    Class AuthHeaders {

        [String] static $token = 'TOKEN'

        static [Hashtable] GetAuthHeaders() {

            return [Hashtable] @{
                "Authorization" = ("Bearer {0}" -f [AuthHeaders]::token)
                "Content-type"  = "application/json"

            }

        }

    }

    Class AppVeyorJob {

        [ValidateSet("Group1", "Group2", 'Group3', 'Group4', 'TotGroup', 'LTSGroup')] [String] $Name
        [String[]] $Appliances = [String[]]::new(4)
        [String] $JobID
        [String] $JobUrl
        [ValidateSet("Stopped", "Running", 'Completed', 'Maintenance')] [String] $State = 'Stopped'
        [DateTime] $Start
        [DateTime] $End

        AppVeyorJob ($Name) {

            $this.Name = $Name

        }

        AppVeyorJob ([String]$Name, [String]$JobID, [Uri]$JobUrl, [String]$State) {

            # If the evaluation is false, throw error
            if (-not $JobUrl -eq 'INTERNAL' -and -not [String]::IsNullOrEmpty($JobUrl.AbsolutePath) -and -not $JobUrl.AbsolutePath.StartsWith("/api/projects/org/")) {

                Throw "JobURI is not a valid URI.  Must start with /api/projects/org/."

            }

            # $this.Appliances = $state:CurrentConfig.Groups.$Name
            $this.Name = $Name
            $this.JobID = $JobID
            $this.JobUrl = $JobUrl
            $this.State = $State

        }

        [AppVeyorJob] SetAppliances ([String[]]$Appliances) {

            For ($i = 0; $i -lt $Appliances.Count; $i++) {

                $this.Appliances[$i] = $Appliances[$i]

            }

            return $this

        }

        [Boolean] IsRunning() {

            if ([String]::IsNullOrEmpty($this.JobURL)) {

                Throw "JobURL is null.  Cannot check if job is still running."

            }

            elseif ($this.JobUrl -eq 'INTERNAL') {

                $this.State = "Running"

                return $true

            }

            else {

                try {

                    $_currentState = Invoke-RestMethod -Uri $this.JobUrl -Method Get -Headers ([AuthHeaders]::GetAuthHeaders())

                }

                catch {

                    Write-Host "Error: $($_.Exception.Message)"
                    return $false

                }

                if ($_currentState.build.status -eq 'Running') {

                    $this.State = "Running"
                    $this.Start = $_currentState.build.started

                    return $true

                }

                else {

                    $this.State = "Stopped"
                    $this.Start = $_currentState.build.started
                    $this.End = $_currentState.build.updated

                    return $false

                }

            }

        }

        static [AppVeyorJob[]] Init() {

            $GroupNames = "Group1", "Group2", 'Group3', 'Group4', 'TotGroup', 'LTSGroup'

            $Collection = @()

            ForEach ($_group in $GroupNames) {

                $Collection += [AppVeyorJob]::new($_group)

            }

            return $Collection

        }

    }

    # Create static class to return network interface(s) index to bind Pode service to based on connected state and those with an IPv4 address
    Class NetInterface {

        static [Int[]] GetAvailableAdapterIndex() {

            $FoundAdapterIndexes = Get-NetAdapter | Where-Object {
                ($_.MediaConnectionState -eq 'Connected' -or $_.ifOperStatus -eq 'Connected') -and
                $_.ifDesc -notmatch 'Loopback' -and
                $_.ifDesc -notmatch 'VPN' -and
                $_.ifDesc -notmatch 'TAP'
            } | Select-Object ifIndex -Unique -ExpandProperty ifIndex

            if ($FoundAdapterIndexes.Count -eq 0) {

                Throw "No network interfaces found with a connected state."

            }

            else {

                return $FoundAdapterIndexes

            }

        }

    }


    # request logging
    # New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging
    New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Levels Error, Warning, Informational, Verbose
    Set-PodeSecurityAccessControl -Methods 'GET', 'POST', 'PATCH' -Origin '*' -Headers 'Content-Type'

    $PodeBasePath = Join-Path 'C:' 'Temp' 'pode'
    $BaseUploadPath = Join-Path 'C:' 'Temp' 'pode' 'uploads'

    "Setting PodeStateConfigFileLocation" | Out-Default
    # Location where the Pode server state variables are stored
    $PodeStateConfigFileLocation = (Join-Path $PodeBasePath 'state.json')
    $cache:PodeStateConfig = $PodeStateConfigFileLocation

    "Setting UploadFileDirectoryLocation" | Out-Default
    # File upload location, which is for uploading pester unit test result documents
    $cache:UploadFileDirectory = $BaseUploadPath

    Get-PodeConfig

    "Attempting to restore pode state" | Out-Default
    # attempt to re-initialise the state (will do nothing if the file doesn't exist)
    Restore-PodeState -Path $cache:PodeStateConfig

    # create the shared variable
    if ($null -eq $state:CurrentJobs) {

        # $Appliances = Set-PodeState -name 'Appliances' -value @{} -Scope Scope0
        Set-PodeState -Name 'CurrentJobs' -Value ([AppVeyorJob]::Init())

        $null = Save-PodeState -Path $cache:PodeStateConfig

    }

    # Need to loop through each group and check if the job is still running
    else {

        # Need to re-create the class objects after restoring the state from the JSON file
        For ($j = 0; $j -lt $state:CurrentJobs.Count; $j++) {

            $state:CurrentJobs[$j] = [AppVeyorJob]::new($state:CurrentJobs[$j].Name, $state:CurrentJobs[$j].JobID, $state:CurrentJobs[$j].JobUrl, $state:CurrentJobs[$j].State)

            if ($state:CurrentJobs[$j].State -eq "Running" -and -not [String]::IsNullOrEmpty($state:CurrentJobs[$j].JobUrl)) {

                # check if the job is still running
                $Result = $state:CurrentJobs[$j] -ne "Maintenance" ? $state:CurrentJobs[$j].IsRunning() : @{ status = 'Maintenance' }

                "Job still running: {0}" -f $Result | Out-Host

                if ($Result.status -eq "running") {

                    "Job {0} is still running." -f $state:CurrentJobs[$j].JobID | Out-Host

                    $state:CurrentJobs[$j].State = "Running"

                }

                elseif ($Result.status -ne 'Maintenance') {

                    "Job {0} is no longer running.  Clearing from tracking." -f $state:CurrentJobs[$j].JobID | Out-Host

                    $state:CurrentJobs[$j].State = "Stopped"
                    $state:CurrentJobs[$j].Appliances.Clear()
                    $state:CurrentJobs[$j].JobID = $null
                    $state:CurrentJobs[$j].JobUrl = $null

                }

            }

            elseif ($state:CurrentJobs[$j].State -eq "Running" -and [String]::IsNullOrEmpty($state:CurrentJobs[$j].JobUrl)) {

                "Job {0} is no longer running as JobURL is null.  Clearing from tracking." -f $state:CurrentJobs[$j].JobId | Out-Host

                $state:CurrentJobs[$j].State = "Stopped"
                $state:CurrentJobs[$j].JobID = $null

            }

        }

        $null = Save-PodeState -Path $cache:PodeStateConfig

    }

    # Check to make sure the cert has not expired
    $Cert = Get-ChildItem Cert:\LocalMachine\My | ? { $_.Subject -match $env:COMPUTERNAME -and $_.Issuer -match "CA" -and -not $_.Archived }

    if ($null -eq $Cert) {

        Thorw "No available web certificates found."

    }

    # Attempt to renew the cert if expired
    if ([datetime]::Now -ge $Cert.NotAfter) {

        certreq -enroll -machine -q -PolicyServer * -cert $Cert.Thumbprint renew reusekeys

    }

    ForEach ($ip in ([Netinterface]::GetAvailableAdapterIndex() | % { Get-NetIPAddress -InterfaceIndex $_ }).IPAddress) {

        "Binding to $ip" | Out-Default

        Add-PodeEndpoint -Address $ip -Port 8443 -Protocol Https -CertificateStoreLocation LocalMachine -CertificateStoreName My -CertificateThumbprint $Cert.Thumbprint

    }

    # Return the appliance configuration JSON object
    Add-PodeRoute -Method Get -Path '/api/currentConfig' -ScriptBlock {
        
        Lock-PodeObject -ScriptBlock {
        
            Write-PodeJsonResponse -StatusCode 200 -Value $state:CurrentConfig
        
        }
    
    }

    # API to check out an available group and assign a job to it.  If JobURL is "INTERNAL", the job is not checked with Appveyor API
    # POST /api/jobs/checkOut
    # Request body:
    #     {
    #         "JobID": "1.10.10",
    #         "JobURL": "https://ci.appveyor.com/api/projects/org/proj/build/1.10.10"
    #     }
    #
    # Response body:
    #     {
    #         "Name": "Group1",
    #         "Appliances": [
    #             "Appliance1",
    #             "Appliance2",
    #             "Appliance3",
    #             "Appliance4"
    #         ],
    #         "JobID": "1.10.10",
    #         "JobURL": "https://ci.appveyor.com/api/projects/org/proj/build/1.10.10"
    #         "State": "Running"
    #     }
    #
    # Error response body with HTTP400:
    #     {
    #         "ErrorID": "Invalid_Request_Body",
    #         "ErrorDescription": "The missing JobID and JobURL in request body."
    #     }
    #
    # Error response body with HTTP400:
    #     {
    #         "ErrorID": "Invalid_Request_Body",
    #         "ErrorDescription": "The missing JobID in request body."
    #     }
    #
    # Error response body with HTTP400:
    #     {
    #         "ErrorID": "Invalid_Request_Body",
    #         "ErrorDescription": "The missing JobURL in request body."
    #     }
    #
    #
    # Error response body with HTTP406:
    #     {
    #         "ErrorID": "No_Available_Slots",
    #         "ErrorDescription": "No available slots."
    #     }
    #
    Add-PodeRoute -Method Post -Path '/api/jobs/checkOut' -ScriptBlock {

        $WebEvent.Data | Out-PodeHost

        $RequestBody = $WebEvent.Data

        if ($RequestBody.Count -eq 0 -or $null -eq $RequestBody) {

            Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_Request_Body'; ErrorDescription = "The missing JobID and JobURL in request body." }

        }

        elseif (-not $RequestBody['JobID']) {

            Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_Request_Body'; ErrorDescription = "The missing JobID in request body." }

        }

        elseif (-not $RequestBody['JobURL']) {

            Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_Request_Body'; ErrorDescription = "The missing JobURL in request body." }

        }

        else {

            $JobIdRegex = [regex]::new("^\d{1,3}\.\d+(\.\d+){0,2}$")

            "JobID: {0}" -f $RequestBody['JobID'] | Out-Host

            # Will also need to have a validation on the JobID format
            if (-not $JobIdRegex.Match($RequestBody['JobID']).Success) {

                Write-PodeJsonResponse -StatusCode 400 -Value @{ ErrorID = 'Invalid_JobID_Format'; ErrorDescription = "JobID format is invalid.  Expected format: 1.10, 1.10.100, 1.10.1234.5678." }

            }

            else {

                $FoundDuplicate = $state:CurrentJobs.GetEnumerator() | ? { $_['JobID'] -eq $RequestBody['JobID'] }

                if ($FoundDuplicate) {

                    Write-PodeJsonResponse -StatusCode 409 -Value @{ ErrorID = 'Duplicate_JobID'; ErrorDescription = "JobID already exists." }

                }

                else {

                    "No dupe found." | Out-Host

                    # again, ensure we're thread safe
                    Lock-PodeObject -ScriptBlock {

                        "Locked." | Out-Host

                        # $state:CurrentJobs.GetEnumerator() | ? { $_.State -eq 'Stopped' } | Select -First 1 | Out-Host
                        [Version]$JobVersion = $RequestBody['JobID']
                        [Version]$LtsVersion = $state:CurrentConfig.WhatIsLts
                        [Version]$TotVersion = $state:CurrentConfig.WhatIsTot

                        # Need to enhance this to handle LTS and TOT groups based on JobID (which should be a version string)
                        if ($JobVersion.Major -eq $LtsVersion.Major -and $JobVersion.Minor -eq $LtsVersion.Minor) {

                            $Result = $state:CurrentJobs.GetEnumerator() | ? { $_.Name -eq 'LTSGroup' -and $_.State -eq 'Stopped' }

                        }

                        elseif ($JobVersion.Major -eq $TotVersion.Major -and $JobVersion.Minor -eq $TotVersion.Minor) {

                            $Result = $state:CurrentJobs.GetEnumerator() | ? { $_.Name -eq 'TotGroup' -and $_.State -eq 'Stopped' }

                        }

                        else {

                            $Result = $state:CurrentJobs.GetEnumerator() | ? { $_.State -eq 'Stopped' } | Select -First 1

                        }

                        if ($null -eq $Result -or $WebEvent.Query['TestError'] -eq "True") {

                            Write-PodeJsonResponse -StatusCode 406 -Value @{ ErrorID = 'No_Available_Slots'; ErrorDescription = "No available slots." }

                        }

                        else {

                            "Setting job to Group '{0}'." -f $Result.Name | Out-Host

                            $Result.JobID = $RequestBody['JobID']
                            $Result.JobUrl = $RequestBody['JobURL']
                            $Result.State = "Running"

                            $GroupID = $Result.Name

                            "Setting appliances within tracking object" | Out-Host

                            $Result.SetAppliances($state:CurrentConfig.Groups.${GroupID})

                            # Do not monitor an INTERNAL job
                            if ($Result.JobUrl -ne 'INTERNAL') {

                                "Job not 'INTERNAL', getting AppVeyor job state." | Out-Host

                                # Need to connect back to AppVeyors API to get the job start time
                                $JobStatus = Invoke-RestMethod -Uri $Result.JobUrl -Method Get -Headers ([AuthHeaders]::GetAuthHeaders())
                                $Result.Start = $JobStatus.build.started

                            }

                            else {

                                "Job 'INTERNAL'." | Out-Host

                                $Result.Start = [datetime]::now

                            }

                            "Return result." | Out-Host

                            # Will need to add logic here to validate the job is still running by using Invoke-RestMethod to $Result.JobUrl
                            Write-PodeJsonResponse -Value $Result

                            # Need to have Pode to continuously check the job status as a scheduled task
                            # Invoke-PodeTask -Name 'MonitorJob' -ArgumentList @{ GroupID = $Result.Name }

                            "Saving StateConfig." | Out-Host

                            $null = Save-PodeState -Path $cache:PodeStateConfig

                        }

                    }

                    "Done." | Out-Host

                }

            }

        }

    }


    Add-PodeRoute -Method Get -Path '/api/ping' -ScriptBlock {
        Write-PodeJsonResponse -Value @{ 'value' = 'pong' }
    }

    # WEB UI FRONT-END

    Use-PodeWebTemplates -Title 'Example' -Theme Dark
    # # Use-PodeWebTemplates -Title 'Example' -Theme Dark -EndpointName Admin

    $navAppVeyor = New-PodeWebNavLink -Name 'Appveyor' -Icon 'factory' -Url 'https://ci.appveyor.com' -NewTab
    $navDiv = New-PodeWebNavDivider

    Set-PodeWebNavDefault -Items $navAppVeyor, $navDiv

    Set-PodeWebHomePage -Layouts @(
        New-PodeWebHero -Title 'Welcome!' -Message 'This is the home page' -Content @(
            New-PodeWebText -Value 'Here is some text!' -InParagraph -Alignment Center
        )
    )

    Add-PodeWebPage -Name Services -Icon Settings -ScriptBlock {
        $value = $WebEvent.Query['value']
    
        # table of services
        if ([string]::IsNullOrWhiteSpace($value)) {
            New-PodeWebCard -Content @(
                New-PodeWebTable -Name 'Services' -DataColumn Name -Click -ScriptBlock {
                    foreach ($svc in (Get-Service)) {
                        [ordered]@{
                            Name = $svc.Name
                            Status = "$($svc.Status)"
                        }
                    }
                }
            )
        }
    
        # code-block with service info
        else {
            $svc = Get-Service -Name $value | Out-String
    
            New-PodeWebCard -Name "$($value) Details" -Content @(
                New-PodeWebCodeBlock -Value $svc -NoHighlight
            )
        }
    }

}

The Services web page is just for testing purposes and was copied from the Pode.Web documentation, here.

I then issue the following POST to the /api/jobs/checkOut:

$Body = @{
     JobID = "1.10.5555.8900";
     JobURL = "INTERNAL"
 } | ConvertTo-Json

Invoke-RestMethod -Uri https://172.30.231.75:8443/api/jobs/checkOut -Method Post -Body $Body -SkipCertificateCheck -Headers @{"Content-Type" = "application/json"}

This is then displayed on the Pode server console:

No dupe found.
Locked.
Setting job to Group 'Group1'.
Setting appliances within tracking object
Job 'INTERNAL'.
Return result.
Saving StateConfig.
WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 10.
[Warning]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Request timeout reached: 30 seconds
[Warning]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Request timeout reached: 30 seconds
[Verbose]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Disposing Context
[Verbose]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Sending response timed-out
[Verbose]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Response timed-out sent
[Verbose]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Request disposed
[Verbose]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Response disposed
[Verbose] OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken, Int32 estimatedSize)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Pode.PodeRequest.Receive(CancellationToken cancellationToken) in C:\Projects\pode-builds\2.11.1\src\Listener\PodeRequest.cs:line 243
[Verbose]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Disposing Context
[Verbose]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Sending response timed-out
[Verbose]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Response timed-out sent
[Verbose]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Request disposed
[Verbose]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Response disposed
[Verbose] OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken, Int32 estimatedSize)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Pode.PodeRequest.Receive(CancellationToken cancellationToken) in C:\Projects\pode-builds\2.11.1\src\Listener\PodeRequest.cs:line 243
[Verbose]: [ContextId: 3cf913ee-b41d-f21a-c4bd-08907aad20ba] Received request
[Warning]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Request timeout reached: 30 seconds
[Verbose]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Disposing Context
[Verbose]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Sending response timed-out
[Verbose]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Response timed-out sent
[Verbose]: [ContextId: b6942c87-8438-6bed-05af-7d36932aa8ae] Disposing Context
[Verbose] OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken, Int32 estimatedSize)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Pode.PodeRequest.Receive(CancellationToken cancellationToken) in C:\Projects\pode-builds\2.11.1\src\Listener\PodeRequest.cs:line 243
[Verbose]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Request disposed
[Verbose]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Response disposed
[Verbose]: [ContextId: fd0eb77f-3d0b-019a-cf45-63d9e03d5611] Received request
[Warning]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Request timeout reached: 30 seconds
[Verbose]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Disposing Context
[Verbose]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Sending response timed-out
[Verbose] OperationCanceledException: The operation was canceled.
[Verbose]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Response timed-out sent
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken, Int32 estimatedSize)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Pode.PodeRequest.Receive(CancellationToken cancellationToken) in C:\Projects\pode-builds\2.11.1\src\Listener\PodeRequest.cs:line 243
[Verbose]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Request disposed
[Verbose]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Response disposed
[Verbose]: [ContextId: 77d9402e-20a6-6faa-d5c2-bb1834da37ec] Received request
[Warning]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Request timeout reached: 30 seconds
[Verbose]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Disposing Context
[Verbose] OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken, Int32 estimatedSize)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Pode.PodeRequest.Receive(CancellationToken cancellationToken) in C:\Projects\pode-builds\2.11.1\src\Listener\PodeRequest.cs:line 243
[Verbose]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Sending response timed-out
[Verbose]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Response timed-out sent
[Verbose]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Request disposed
[Verbose]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Response disposed
[Verbose]: [ContextId: b926254a-baa6-aa28-b9e6-036f86c84fff] Disposing Context
[Warning]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Request timeout reached: 30 seconds
[Verbose]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Disposing Context
[Verbose] OperationCanceledException: The operation was canceled.
   at System.Threading.CancellationToken.ThrowOperationCanceledException()
   at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource<System.Int32>.GetResult(Int16 token)
   at System.Net.Security.SslStream.EnsureFullTlsFrameAsync[TIOAdapter](CancellationToken cancellationToken, Int32 estimatedSize)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at System.Net.Security.SslStream.ReadAsyncInternal[TIOAdapter](Memory`1 buffer, CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
   at Pode.PodeRequest.Receive(CancellationToken cancellationToken) in C:\Projects\pode-builds\2.11.1\src\Listener\PodeRequest.cs:line 243
[Verbose]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Sending response timed-out
[Verbose]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Response timed-out sent
[Verbose]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Request disposed
[Verbose]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Response disposed
[Verbose]: [ContextId: 03dde4ef-87e0-46ff-af6a-cb4e94f23a11] Disposing Context
[Warning]: [ContextId: 8d79a583-6779-108e-09b1-634d34370609] Request timeout reached: 30 seconds
[Verbose]: [ContextId: 8d79a583-6779-108e-09b1-634d34370609] Disposing Context
[Verbose]: [ContextId: 8d79a583-6779-108e-09b1-634d34370609] Sending response timed-out
[Verbose]: [ContextId: 8d79a583-6779-108e-09b1-634d34370609] Response timed-out sent
[Verbose]: [ContextId: 8d79a583-6779-108e-09b1-634d34370609] Request disposed
[Verbose]: [ContextId: 8d79a583-6779-108e-09b1-634d34370609] Response disposed

Prefacing the Save-PodeState call in the Add-PodeRoute with $null = or even piping to Out-Null, the error continues. Removing the Add-PodeWebPage code I don't get the error at all.

Here is state.json:

{
  "CurrentJobs": {
    "Scope": [],
    "Value": [
      {
        "Name": "Group1",
        "Appliances": [
          null,
          null,
          null,
          null
        ],
        "JobID": "",
        "JobUrl": "",
        "State": "Stopped",
        "Start": "0001-01-01T00:00:00",
        "End": "0001-01-01T00:00:00"
      },
      {
        "Name": "Group2",
        "Appliances": [
          null,
          null,
          null,
          null
        ],
        "JobID": "",
        "JobUrl": "",
        "State": "Stopped",
        "Start": "0001-01-01T00:00:00",
        "End": "0001-01-01T00:00:00"
      },
      {
        "Name": "Group3",
        "Appliances": [
          null,
          null,
          null,
          null
        ],
        "JobID": "",
        "JobUrl": "",
        "State": "Stopped",
        "Start": "0001-01-01T00:00:00",
        "End": "0001-01-01T00:00:00"
      },
      {
        "Name": "Group4",
        "Appliances": [
          null,
          null,
          null,
          null
        ],
        "JobID": "",
        "JobUrl": "",
        "State": "Stopped",
        "Start": "0001-01-01T00:00:00",
        "End": "0001-01-01T00:00:00"
      },
      {
        "Name": "TotGroup",
        "Appliances": [
          null,
          null,
          null,
          null
        ],
        "JobID": "",
        "JobUrl": "",
        "State": "Stopped",
        "Start": "0001-01-01T00:00:00",
        "End": "0001-01-01T00:00:00"
      },
      {
        "Name": "LTSGroup",
        "Appliances": [
          null,
          null,
          null,
          null
        ],
        "JobID": "",
        "JobUrl": "",
        "State": "Stopped",
        "Start": "0001-01-01T00:00:00",
        "End": "0001-01-01T00:00:00"
      }
    ]
  },
  "CurrentConfig": {
    "Value": {
      "Groups": {
        "Group1": [
          "name1",
          "name2",
          "name3",
          "namefex"
        ],
        "Group2": [
          "name5",
          "name6",
          "name7",
          "namefex2"
        ],
        "Group3": [
          "name9",
          "name10",
          "name11",
          "namefex3"
        ],
        "Group4": [
          "name13",
          "name14",
          "name15",
          "namefex4"
        ],
        "LTS": [
          "namelts1",
          "namelts2",
          "namelts3",
          "nameltsfex"
        ],
        "TOT": [
          "name-tot1",
          "name-tot2",
          "name-tot3",
          "totfex"
        ]
      },
      "version": {
        "ExpectedAPIVersion": 1234,
        "Appliance1": "name1",
        "Appliance2": "name2",
        "Appliance3": "name3",
        "Appliance4": "name4",
        "key_1": "[REDACTED]",
        "key_2": "[REDACTED]",
        "key_3": "[REDACTED]"
      },
      "Config": {
        "RemoteBackupPublicKey": "ssh-rsa [REDACTED]",
        "Key1": "[REDACTED]",
        "Key2": "[REDACTED]",
        "Key3": "[REDACTED]",
        "Key4": "[REDACTED]"
      }
    },
    "Scope": []
  },
  "pode.web.app-path": {
    "Value": "",
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.title": {
    "Value": "Example",
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.logo": {
    "Value": "",
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.favicon": {
    "Value": "/pode.web/images/favicon.ico",
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.no-page-filter": {
    "Value": false,
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.hide-sidebar": {
    "Value": false,
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.social": {
    "Value": {},
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.pages": {
    "Value": {
      "/": {
        "Path": "/",
        "Title": "Home",
        "Layouts": [
          {
            "Title": "Welcome!",
            "CssClasses": "",
            "CssStyles": "",
            "ID": "hero_tgehx",
            "ComponentType": "Layout",
            "Message": "This is the home page",
            "Content": [
              {
                "ID": "txt_fmrxl",
                "Parent": null,
                "NoEvents": true,
                "InParagraph": true,
                "Value": "Here is some text!",
                "CssClasses": "",
                "CssStyles": "",
                "Style": "Normal",
                "ObjectType": "Text",
                "Pronunciation": "",
                "ComponentType": "Element",
                "Alignment": "center"
              }
            ],
            "ObjectType": "Hero"
          }
        ],
        "IsSystem": true,
        "Navigation": null,
        "ObjectType": "Page",
        "DisplayName": "Home",
        "ComponentType": "Page",
        "NoTitle": false,
        "Name": "Home"
      }
    },
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.default-nav": {
    "Value": [
      {
        "ID": "navlink_appveyor",
        "Disabled": false,
        "InDropdown": false,
        "IsDynamic": false,
        "NavType": "Link",
        "Icon": "factory",
        "DisplayName": "Appveyor",
        "ComponentType": "Navigation",
        "Url": "https://ci.appveyor.com",
        "NewTab": true,
        "Name": "Appveyor"
      },
      {
        "InDropdown": false,
        "ComponentType": "Navigation",
        "NavType": "Divider"
      }
    ],
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.endpoint-name": {
    "Value": null,
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.custom-css": {
    "Value": [],
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.custom-js": {
    "Value": [],
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.theme": {
    "Value": "dark",
    "Scope": [
      "pode.web"
    ]
  },
  "pode.web.custom-themes": {
    "Value": {
      "Themes": {},
      "Default": null
    },
    "Scope": [
      "pode.web"
    ]
  }
}

Expected Behavior

I'm trying to implement an API service, and a web UI front end to show the data from the $state:CurrentJobs variable, and need to save its state across restarts.

Platform

  • OS: Windows Server 2016 (10.0.14393.7428)
  • Versions:
    • Pode: 2.11.1
    • Pode.Web: 0.8.3
    • PowerShell: 7.4.5
@ChrisLynchHPE ChrisLynchHPE added the bug 🐛 Something isn't working label Nov 12, 2024
@ChrisLynchHPE
Copy link
Author

ChrisLynchHPE commented Nov 12, 2024

Also, I just found that Save-PodeState has a -Depth parameter, and I see that maps to ConvertTo-Json's -Depth parameter. I added -Depth 99 to my code and still get pretty much the same error. This time, instead of saying:

WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 10

is now

WARNING: Resulting JSON is truncated as serialization has exceeded the set depth of 99

One additional difference is that there are no further exceptions in the console, just the WARNING message.

@ChrisLynchHPE
Copy link
Author

ChrisLynchHPE commented Nov 13, 2024

I think I may have found where this issue is caused. I've narrowed it down to one of the elements with $PodeContext.Server.State is causing ConvertTo-Json to attempt an infinite loop to serialize to JSON: pode.web.pages. If I comment out Add-PodeWebPage -Name Services -Icon Settings -ScriptBlock {}, restart Pode, I do not get any JSON serialization issues when I issue a POST /api/jobs/checkOut call. The Add-PodeWebPage script is from the Pode.Web documentation, so clearly something is wrong. I'm thinking that the Scriptblock within the Add-PodeWebPage is not being handled properly, and inadvertently being stored in the $PodeContext variable as a ScriptBlock, or not an OrderedHashtable object as other entries within $PodeContext.Server.State are.

[edit]
And it is. Specifically, the ScriptBlock property within $PodeContext.Server.State.'pode.web.pages'.Value.'PAGE-URI' is the culprit. If I manipulate it by inserting $PodeContext.Server.State.'pode.web.pages'.Value.'/pages/Services'.ScriptBlock = @{} right before the call to Save-PodeState, then no JSON truncation and the state is saved (minus the ScriptBlock property value for /pages/Services).

What is the advice here? Is this a bug? I don't want to go manually manipulating $PodeContext.Server.State unless I have to.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug 🐛 Something isn't working
Projects
Status: Backlog
Development

No branches or pull requests

1 participant