Public/Invoke-OpenApiRequest.ps1

<#
.SYNOPSIS
    Sends a request for an OpenAPI operation and returns the response as PowerShell objects.

.DESCRIPTION
    Invoke-OpenApiRequest is the request engine that every module generated by New-OpenApiModule uses. Wrapper
    functions call it with the operation's metadata and the parameter values the user passed; you can also call it
    directly, for example for an operation the generator skipped.

    It uses the connection stored with Set-OpenApiContext for the service (base URI, credentials, headers, proxy,
    timeout and retries) and:

      - builds the URL from the path template and the path and query parameters, serialising arrays and objects
        with the parameter's style and explode settings (form, spaceDelimited, pipeDelimited, deepObject, simple,
        label, matrix); booleans are sent as true/false and dates as ISO 8601; a path parameter flagged CatchAll
        or AllowReserved keeps the '/' of its value (each segment is escaped on its own);
      - sends header and cookie parameters (cookies as one Cookie header);
      - encodes the body by content type: JSON, application/x-www-form-urlencoded, multipart/form-data (FileInfo,
        byte[] and stream values become file parts), text, or binary (byte[], stream or FileInfo);
      - authenticates with the first security requirement of the operation that the context has credentials for
        (API key in a header, query or cookie, HTTP basic, bearer token, or OAuth2 client credentials with token
        caching and one refresh on 401);
      - retries throttled and failed requests with tcs.core Invoke-WithRetry, honouring Retry-After;
      - converts JSON responses to objects (adding the response PSTypeName), outputs the items of a page for a
        pageable operation and the UnwrapProperty of the response for other operations that name one, saves
        binary responses to -OutFile, and with -All follows next-page links or repeats the request with the
        page token of each response.

    HTTP errors are written as ErrorRecords with the FullyQualifiedErrorId 'OpenApi.<Service>.<StatusCode>'
    (connection failures 'OpenApi.<Service>.Connection'); when -Cmdlet is passed they are written through the
    calling command, so its -ErrorAction and -ErrorVariable apply.

.PARAMETER Service
    The service name used with Set-OpenApiContext. Defaults to the Service of the operation metadata.

.PARAMETER Operation
    The operation metadata (a hashtable or object with OperationId, Method, Path, Parameters, RequestContentTypes,
    ResponseContentTypes, BinaryResponse, Security, SecuritySchemes, Paging, ResponseTypeName and optionally
    UnwrapProperty), as written by the generator to OpenApi/operations.json. Paging is
    { Kind = 'nextLink'; ItemsProperty; NextLinkProperty }, { Kind = 'linkHeader'; ItemsProperty } or
    { Kind = 'token'; ItemsProperty; TokenParameter; TokenProperty }. Members that are missing (metadata written
    by an older generator) keep their earlier behaviour.

.PARAMETER PathParameters
    Values of path parameters, keyed by their name in the specification.

.PARAMETER QueryParameters
    Values of query parameters, keyed by their name in the specification. Only the keys present are sent.

.PARAMETER HeaderParameters
    Values of header parameters, keyed by their name in the specification.

.PARAMETER CookieParameters
    Values of cookie parameters, keyed by their name in the specification.

.PARAMETER Body
    The request body: a hashtable or object (JSON, form and multipart), a string (sent as it is), a byte array,
    stream or FileInfo (binary). An explicit $null is sent as JSON null.

.PARAMETER ContentType
    The media type of the body. Defaults to the first of the operation's RequestContentTypes, else application/json.

.PARAMETER OutFile
    Saves the response body to this file and returns the FileInfo.

.PARAMETER All
    Follows next-page links (a nextLink property or a Link header with rel="next") on the same host, or for token
    paging repeats the request with the token query parameter set from each response (the other query parameters,
    the method and the body are kept) until the token is empty, missing or repeats, and writes the items of every
    page.

.PARAMETER Raw
    Returns an object with StatusCode, Headers and Content (text, or bytes for binary responses) instead of
    converted objects.

.PARAMETER Cmdlet
    The $PSCmdlet of the calling function. Errors, warnings and verbose output are written through it.

.INPUTS
    None
    This function does not accept pipeline input.

.OUTPUTS
    System.Object
    The converted response (objects with the operation's response PSTypeName), a Tcs.OpenApi.RawResponse with
    -Raw, System.IO.FileInfo with -OutFile, a byte array for binary responses, or nothing for empty responses.

.EXAMPLE
    $operation = @{
        OperationId = 'getPetById'; Method = 'GET'; Path = '/pets/{petId}'
        Parameters = @(@{ Name = 'petId'; In = 'path' })
        Security = @(@{ api_key = @() })
        SecuritySchemes = @{ api_key = @{ Type = 'apiKey'; In = 'header'; ParameterName = 'X-API-Key' } }
        ResponseTypeName = 'PetStore.Pet'
    }
    Invoke-OpenApiRequest -Service 'PetStore' -Operation $operation -PathParameters @{ petId = 42 }

    Gets pet 42 from the service configured with Set-OpenApiContext -Service PetStore.

.EXAMPLE
    Invoke-OpenApiRequest -Service 'PetStore' -Operation $listPets -QueryParameters @{ tags = @('dog', 'cat') } -All

    Lists pets with two tags, following next-page links.

.NOTES
    Author: Nigel Tatschner
    Company: TheCodeSaiyan

    Verbose output shows each request line and status; Debug output also shows headers and bodies with
    Authorization, API keys, cookies and properties named like password, secret or token replaced by ********.
    A deprecated operation writes one warning per session.

.LINK
    Set-OpenApiContext
#>

function Invoke-OpenApiRequest {
    [CmdletBinding()]
    [OutputType([System.Object])]
    param(
        [Parameter(HelpMessage = 'The service name used with Set-OpenApiContext.')]
        [string]$Service,

        [Parameter(Mandatory, HelpMessage = 'The operation metadata.')]
        [ValidateNotNull()]
        [object]$Operation,

        [Parameter(HelpMessage = 'Path parameter values keyed by spec name.')]
        [AllowNull()]
        [System.Collections.IDictionary]$PathParameters,

        [Parameter(HelpMessage = 'Query parameter values keyed by spec name.')]
        [AllowNull()]
        [System.Collections.IDictionary]$QueryParameters,

        [Parameter(HelpMessage = 'Header parameter values keyed by spec name.')]
        [AllowNull()]
        [System.Collections.IDictionary]$HeaderParameters,

        [Parameter(HelpMessage = 'Cookie parameter values keyed by spec name.')]
        [AllowNull()]
        [System.Collections.IDictionary]$CookieParameters,

        [Parameter(HelpMessage = 'The request body.')]
        [AllowNull()]
        [object]$Body,

        [Parameter(HelpMessage = 'The media type of the body.')]
        [string]$ContentType,

        [Parameter(HelpMessage = 'File to save the response body to.')]
        [string]$OutFile,

        [Parameter(HelpMessage = 'Follow next-page links.')]
        [switch]$All,

        [Parameter(HelpMessage = 'Return status, headers and content instead of objects.')]
        [switch]$Raw,

        [Parameter(HelpMessage = 'The $PSCmdlet of the calling function.')]
        [AllowNull()]
        [System.Management.Automation.PSCmdlet]$Cmdlet
    )

    $bodyGiven = $PSBoundParameters.ContainsKey('Body')
    Import-OpenApiHttpAssembly

    # Preferences of the calling command do not cross module boundaries; take them from -Cmdlet.
    # Set-Variable honours -WhatIf, so it is called with -WhatIf:$false: a read has no -WhatIf of its own and
    # must still run, with the caller's preferences, under a session-wide $WhatIfPreference
    if ($null -ne $Cmdlet) {
        $callerPreference = Get-OpenApiCallerPreference -Cmdlet $Cmdlet
        foreach ($preference in @($callerPreference.Keys)) {
            $commonName = $preference.Replace('Preference', '')
            if (-not $PSBoundParameters.ContainsKey($commonName) -and -not ($preference -eq 'WarningPreference' -and $PSBoundParameters.ContainsKey('WarningAction'))) {
                $value = $callerPreference[$preference]
                if ([string]$value -eq 'Ignore') {
                    # See below: Ignore is not a valid preference variable value on Windows PowerShell 5.1
                    $value = [System.Management.Automation.ActionPreference]::SilentlyContinue
                }
                Set-Variable -Name $preference -Value $value -Confirm:$false -WhatIf:$false
            }
        }
    }
    if ($DebugPreference -eq 'Inquire') {
        # Windows PowerShell 5.1 sets Inquire for -Debug; do not prompt for every message
        $DebugPreference = 'Continue'
    }
    # Windows PowerShell 5.1 throws when Write-Warning (or Write-Verbose ...) reads a preference variable set to
    # Ignore (as -WarningAction Ignore does); the engine writes its own messages with SilentlyContinue instead
    foreach ($preference in @('VerbosePreference', 'DebugPreference', 'WarningPreference', 'InformationPreference')) {
        if ([string](Get-Variable -Name $preference -ValueOnly) -eq 'Ignore') {
            Set-Variable -Name $preference -Value ([System.Management.Automation.ActionPreference]::SilentlyContinue) -Confirm:$false -WhatIf:$false
        }
    }

    $writeError = {
        param([System.Management.Automation.ErrorRecord]$Record)
        if ($null -ne $Cmdlet) {
            $Cmdlet.WriteError($Record)
        }
        else {
            Write-Error -ErrorRecord $Record
        }
    }

    $operationId = [string](Get-OpenApiMember -InputObject $Operation -Name 'OperationId')
    $method = ([string](Get-OpenApiMember -InputObject $Operation -Name 'Method')).ToUpperInvariant()
    if ([string]::IsNullOrEmpty($method)) {
        $method = 'GET'
    }
    $path = [string](Get-OpenApiMember -InputObject $Operation -Name 'Path')
    if ([string]::IsNullOrEmpty($Service)) {
        $Service = [string](Get-OpenApiMember -InputObject $Operation -Name 'Service')
    }
    if ([string]::IsNullOrEmpty($Service)) {
        $exception = New-Object System.ArgumentException -ArgumentList 'No service name was given: pass -Service or an operation with a Service property.'
        & $writeError (New-Object System.Management.Automation.ErrorRecord -ArgumentList $exception, 'OpenApi.MissingService', ([System.Management.Automation.ErrorCategory]::InvalidArgument), $operationId)
        return
    }

    $loadProblem = ''
    try {
        $context = Resolve-OpenApiContext -Service $Service
    }
    catch {
        $context = $null
        $loadProblem = " The saved context could not be loaded: $($_.Exception.Message)"
    }
    if ($null -eq $context) {
        $exception = New-Object System.InvalidOperationException -ArgumentList "There is no connection for the '$Service' service. Run Set-OpenApiContext -Service '$Service' -BaseUri <url> first.$loadProblem"
        & $writeError (New-OpenApiErrorRecord -Service $Service -OperationId $operationId -Method $method -Uri $path -Exception $exception -Kind 'NoContext' -Category ([System.Management.Automation.ErrorCategory]::ConnectionError))
        return
    }

    if ([bool](Get-OpenApiMember -InputObject $Operation -Name 'Deprecated') -and -not [string]::IsNullOrEmpty($operationId)) {
        Write-OpenApiDeprecationWarning -Service $Service -OperationId $operationId -Cmdlet $Cmdlet
    }

    # Get-OpenApiMember returns a collection as one object; piping the variable enumerates it
    $parameterSpecs = Get-OpenApiMember -InputObject $Operation -Name 'Parameters'
    $parameterSpecs = @($parameterSpecs | Where-Object -FilterScript { $null -ne $_ })
    try {
        $uri = New-OpenApiRequestUri -BaseUri $context.BaseUri -Path $path -Parameter $parameterSpecs -PathParameters $PathParameters -QueryParameters $QueryParameters
    }
    catch {
        & $writeError (New-OpenApiErrorRecord -Service $Service -OperationId $operationId -Method $method -Uri $path -Exception $_.Exception -Kind 'InvalidArgument' -Category ([System.Management.Automation.ErrorCategory]::InvalidArgument))
        return
    }

    # Headers: context defaults, then Accept, then header parameters
    $headers = [ordered]@{}
    $headers['User-Agent'] = 'tcs.openapi/{0} PowerShell/{1}' -f $MyInvocation.MyCommand.Module.Version, $PSVersionTable.PSVersion
    $responseTypes = Get-OpenApiMember -InputObject $Operation -Name 'ResponseContentTypes'
    $responseTypes = @($responseTypes | Where-Object -FilterScript { -not [string]::IsNullOrEmpty($_) })
    if ($responseTypes.Count -gt 0) {
        $headers['Accept'] = $responseTypes -join ', '
    }
    else {
        $headers['Accept'] = 'application/json, */*;q=0.8'
    }
    foreach ($key in $context.Header.Keys) {
        $headers[$key] = $context.Header[$key]
    }
    if ($null -ne $HeaderParameters) {
        foreach ($key in $HeaderParameters.Keys) {
            $style = Get-OpenApiParameterStyle -Parameter $parameterSpecs -Name ([string]$key) -In 'header'
            $headers[[string]$key] = ConvertTo-OpenApiHeaderParameter -Value $HeaderParameters[$key] -Explode:$style.Explode
        }
    }
    $cookies = New-Object System.Collections.Generic.List[string]
    if ($null -ne $CookieParameters) {
        foreach ($key in $CookieParameters.Keys) {
            $style = Get-OpenApiParameterStyle -Parameter $parameterSpecs -Name ([string]$key) -In 'cookie'
            foreach ($pair in (ConvertTo-OpenApiCookieParameter -Name ([string]$key) -Value $CookieParameters[$key] -Explode:$style.Explode)) {
                $cookies.Add($pair)
            }
        }
    }

    # Body
    $contentFactory = $null
    if ($bodyGiven) {
        $bodyContentType = $ContentType
        if ([string]::IsNullOrEmpty($bodyContentType)) {
            $requestTypes = Get-OpenApiMember -InputObject $Operation -Name 'RequestContentTypes'
            $bodyContentType = @($requestTypes | Where-Object -FilterScript { -not [string]::IsNullOrEmpty($_) }) | Select-Object -First 1
        }
        if ([string]::IsNullOrEmpty($bodyContentType)) {
            $bodyContentType = 'application/json'
        }
        if ($Body -is [System.IO.Stream]) {
            # Read the stream once: every attempt then sends the same bytes, and the caller's stream is never
            # handed to HttpClient (on .NET Framework HttpClient disposes the request content, and so the stream)
            $startPosition = $null
            if ($Body.CanSeek) {
                $startPosition = $Body.Position
                $Body.Position = 0
            }
            $buffer = New-Object System.IO.MemoryStream
            $Body.CopyTo($buffer)
            if ($null -ne $startPosition) {
                $Body.Position = $startPosition
            }
            $Body = $buffer.ToArray()
            $buffer.Dispose()
        }
        # A new HttpContent is built for every attempt (a sent request cannot be sent again)
        $contentFactory = { New-OpenApiHttpContent -Body $Body -ContentType $bodyContentType }
    }

    $paging = Get-OpenApiMember -InputObject $Operation -Name 'Paging'
    $pagingKind = [string](Get-OpenApiMember -InputObject $paging -Name 'Kind')
    $itemsProperty = [string](Get-OpenApiMember -InputObject $paging -Name 'ItemsProperty')
    $nextLinkProperty = [string](Get-OpenApiMember -InputObject $paging -Name 'NextLinkProperty')
    $tokenParameter = [string](Get-OpenApiMember -InputObject $paging -Name 'TokenParameter')
    $tokenProperty = [string](Get-OpenApiMember -InputObject $paging -Name 'TokenProperty')
    $tokenPaging = ($pagingKind -eq 'token' -and -not [string]::IsNullOrEmpty($tokenParameter))
    if ($tokenPaging) {
        $nextLinkProperty = ''
        if ([string]::IsNullOrEmpty($tokenProperty)) {
            $tokenProperty = $tokenParameter
        }
    }
    elseif ($null -ne $paging -and $pagingKind -ne 'linkHeader') {
        if ([string]::IsNullOrEmpty($itemsProperty)) {
            $itemsProperty = 'value'
        }
        if ([string]::IsNullOrEmpty($nextLinkProperty)) {
            $nextLinkProperty = 'nextLink'
        }
    }
    # A pageable operation outputs the items of its pages; UnwrapProperty applies to the other operations
    $unwrapProperty = ''
    if ($null -eq $paging) {
        $unwrapProperty = [string](Get-OpenApiMember -InputObject $Operation -Name 'UnwrapProperty')
    }
    $typeName = [string](Get-OpenApiMember -InputObject $Operation -Name 'ResponseTypeName')
    $binaryResponse = [bool](Get-OpenApiMember -InputObject $Operation -Name 'BinaryResponse')
    $sensitive = @($context.Header.Keys | Where-Object -FilterScript { Test-OpenApiSensitiveName -Name $_ })
    if (-not [string]::IsNullOrEmpty($OutFile)) {
        $OutFile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutFile)
    }

    try {
        $client = Get-OpenApiHttpClient -Context $context
    }
    catch {
        & $writeError (New-OpenApiErrorRecord -Service $Service -OperationId $operationId -Method $method -Uri $uri -Exception $_.Exception -Kind 'Connection' -Category ([System.Management.Automation.ErrorCategory]::ConnectionError))
        return
    }

    $originUri = $uri
    $visited = @{}
    $visitedTokens = @{}
    if ($tokenPaging -and $null -ne $QueryParameters) {
        foreach ($key in $QueryParameters.Keys) {
            if ([string]$key -eq $tokenParameter -and -not [string]::IsNullOrEmpty([string]$QueryParameters[$key])) {
                $visitedTokens[[string]$QueryParameters[$key]] = $true
            }
        }
    }
    $pageMethod = $method
    $pageFactory = $contentFactory
    while ($null -ne $uri) {
        $visited[$uri] = $true
        $parsed = $null
        $parsedOk = $false
        $content = $null
        try {
            $exchange = @{
                Context        = $context
                Operation      = $Operation
                Client         = $client
                Method         = $pageMethod
                Uri            = $uri
                Header         = $headers
                Cookie         = $cookies.ToArray()
                ContentFactory = $pageFactory
                SensitiveName  = $sensitive
            }
            $response = Invoke-OpenApiHttpExchange @exchange
        }
        catch {
            $failure = $_.Exception
            $kind = 'Connection'
            $category = [System.Management.Automation.ErrorCategory]::ConnectionError
            if ($failure -is [System.Security.Authentication.AuthenticationException]) {
                $kind = 'Authentication'
                $category = [System.Management.Automation.ErrorCategory]::AuthenticationError
            }
            elseif ($failure -is [System.ArgumentException] -or $failure -is [System.IO.FileNotFoundException] -or $failure -is [System.FormatException]) {
                $kind = 'InvalidArgument'
                $category = [System.Management.Automation.ErrorCategory]::InvalidArgument
            }
            elseif ($failure -is [System.TimeoutException]) {
                $category = [System.Management.Automation.ErrorCategory]::OperationTimeout
            }
            & $writeError (New-OpenApiErrorRecord -Service $Service -OperationId $operationId -Method $pageMethod -Uri $uri -Exception $failure -Kind $kind -Category $category)
            return
        }

        if ($response.StatusCode -lt 200 -or $response.StatusCode -gt 299) {
            & $writeError (New-OpenApiErrorRecord -Service $Service -OperationId $operationId -Method $pageMethod -Uri $uri -Response $response -SensitiveName $sensitive)
            return
        }

        $mediaKind = Get-OpenApiMediaTypeKind -ContentType $response.ContentType
        if (-not [string]::IsNullOrEmpty($OutFile)) {
            try {
                Save-OpenApiResponseFile -Response $response -Path $OutFile
            }
            catch {
                & $writeError (New-OpenApiErrorRecord -Service $Service -OperationId $operationId -Method $pageMethod -Uri $uri -Exception $_.Exception -Kind 'OutFile' -Category ([System.Management.Automation.ErrorCategory]::WriteError))
            }
            return
        }

        $bytes = Read-OpenApiResponseBody -Response $response
        $isText = ($mediaKind -eq 'Json' -or $mediaKind -eq 'Text' -or $mediaKind -eq 'Form')
        if ($Raw) {
            $content = $bytes
            if ($isText -or [string]::IsNullOrEmpty($response.ContentType)) {
                $content = ConvertTo-OpenApiResponseText -Bytes $bytes -ContentType $response.ContentType
            }
            [pscustomobject]@{
                PSTypeName = 'Tcs.OpenApi.RawResponse'
                StatusCode = $response.StatusCode
                Headers    = $response.Headers
                Content    = $content
            }
        }
        elseif ($response.StatusCode -eq 204 -or $bytes.Length -eq 0) {
            Write-Verbose -Message 'The response has no content.'
        }
        elseif ($binaryResponse -and $mediaKind -ne 'Json') {
            , $bytes
        }
        elseif ($mediaKind -eq 'Json' -or ([string]::IsNullOrEmpty($response.ContentType) -and -not $binaryResponse)) {
            $text = ConvertTo-OpenApiResponseText -Bytes $bytes -ContentType $response.ContentType
            if ($DebugPreference -ne 'SilentlyContinue') {
                Write-Debug -Message ('Response body: ' + (Get-OpenApiRedactedText -Text $text))
            }
            $parsed = $null
            $parsedOk = $true
            try {
                $parsed = ConvertFrom-OpenApiResponseJson -Text $text
            }
            catch {
                $parsedOk = $false
                Write-Verbose -Message "The response is not valid JSON and is returned as text: $($_.Exception.Message)"
                $text
            }
            if ($parsedOk) {
                Write-OpenApiResponseOutput -InputObject $parsed -ItemsProperty $itemsProperty -TypeName $typeName -UnwrapProperty $unwrapProperty
            }
        }
        elseif ($mediaKind -eq 'Text' -or $mediaKind -eq 'Form') {
            ConvertTo-OpenApiResponseText -Bytes $bytes -ContentType $response.ContentType
        }
        else {
            , $bytes
        }

        # Next page
        $next = $null
        $link = $null
        if ($tokenPaging) {
            $token = $null
            $page = $null
            if ($parsedOk -and $null -ne $parsed -and -not $Raw) {
                $page = $parsed
            }
            elseif ($Raw -and $isText) {
                try {
                    $page = ConvertFrom-OpenApiResponseJson -Text $content
                }
                catch {
                    $page = $null
                }
            }
            if ($null -ne $page -and $page -isnot [array]) {
                $token = Get-OpenApiMember -InputObject $page -Name $tokenProperty
            }
            if ($null -ne $token -and -not [string]::IsNullOrEmpty([string]$token)) {
                $token = [string]$token
                if (-not $All) {
                    Write-Verbose -Message 'More results are available; use -All to get every page.'
                }
                elseif ($visitedTokens.ContainsKey($token)) {
                    Write-Warning -Message "Paging stopped: the page token '$token' was already used."
                }
                else {
                    $visitedTokens[$token] = $true
                    # The caller's query parameters with the token set; same method and body as the first request
                    $pageQuery = [ordered]@{}
                    if ($null -ne $QueryParameters) {
                        foreach ($key in $QueryParameters.Keys) {
                            if ([string]$key -ne $tokenParameter) {
                                $pageQuery[[string]$key] = $QueryParameters[$key]
                            }
                        }
                    }
                    $pageQuery[$tokenParameter] = $token
                    $next = New-OpenApiRequestUri -BaseUri $context.BaseUri -Path $path -Parameter $parameterSpecs -PathParameters $PathParameters -QueryParameters $pageQuery
                }
            }
            $uri = $next
            continue
        }
        if ($pagingKind -ne 'linkHeader' -and -not [string]::IsNullOrEmpty($nextLinkProperty) -and $parsedOk -and $null -ne $parsed -and -not $Raw) {
            $link = [string](Get-OpenApiMember -InputObject $parsed -Name $nextLinkProperty)
        }
        elseif ($pagingKind -ne 'linkHeader' -and -not [string]::IsNullOrEmpty($nextLinkProperty) -and $Raw -and $isText) {
            try {
                $link = [string](Get-OpenApiMember -InputObject (ConvertFrom-OpenApiResponseJson -Text $content) -Name $nextLinkProperty)
            }
            catch {
                $link = $null
            }
        }
        if ([string]::IsNullOrEmpty($link)) {
            $link = Get-OpenApiLinkHeaderNext -LinkHeader $response.Headers['Link']
        }
        if (-not [string]::IsNullOrEmpty($link)) {
            if ($All) {
                $next = Resolve-OpenApiNextPageUri -Link $link -CurrentUri $uri -OriginUri $originUri
                if ($null -ne $next -and $visited.ContainsKey($next)) {
                    Write-Warning -Message "Paging stopped: the next-page link '$next' was already requested."
                    $next = $null
                }
            }
            else {
                Write-Verbose -Message 'More results are available; use -All to get every page.'
            }
        }
        $uri = $next
        # Next pages are fetched with GET and no body
        $pageMethod = 'GET'
        $pageFactory = $null
    }
}