Integrar PowerShell con una API REST ── el uso práctico de Invoke-RestMethod

· Actualizado el: · · PowerShell, REST API, Windows, Automatización, Sistemas empresariales, Integración, JSON, Mejora operativa

«Obtener los datos de pedidos desde la API web del sistema principal y volcarlos en un informe de Excel de la empresa» o «llamar cada mañana a la API de asistencia de un SaaS para generar la previsión de entradas del día» ── entre los escenarios en los que PowerShell se usa en el trabajo diario, la integración con APIs REST es uno de los que más ha crecido en los últimos años. No justifica comprar una herramienta específica, pero tampoco se puede sostener a mano. Como herramienta para cubrir ese hueco, Invoke-RestMethod resulta muy potente.

Al mismo tiempo, aunque conseguir que funcione es sencillo, la integración con una API se complica de golpe en cuanto se lleva a producción, y esa es también una de sus características. El japonés se corrompe, no se puede leer el contenido de las respuestas de error, de vez en cuando falla con un 429, no pasa a través del proxy, o solo en Windows PowerShell 5.1 el TLS lo rechaza. Son problemas propios de tratar con «un sistema que está al otro lado».

En este artículo, dirigido al personal de sistemas y a los desarrolladores que llaman APIs desde PowerShell dentro de la empresa, organizamos en el orden en que se necesitan en la práctica: la autenticación, el envío y la recepción de JSON, el tratamiento de errores, los reintentos, la paginación y, por último, las trampas propias de la versión 5.1.

Entorno previo y entorno de verificación

Elemento Contenido
Versión objetivo El artículo cubre tanto Windows PowerShell 5.1 como PowerShell 7. Las funciones que solo existen en PowerShell 6 o posterior, como -SkipHttpErrorCheck, -Authentication, -MaximumRetryCount o -NoProxy, se indican explícitamente cada vez que aparecen (las restricciones propias de 5.1 se resumen en el §8)1
Entorno de verificación de los ejemplos El código de ejemplo que se distribuye al final del artículo se ha ejecutado y verificado con PowerShell 7.6 (21 pruebas Pester). Las indicaciones específicas de 5.1 del §8 son una recopilación basada en las especificaciones de esa versión
API usada en los ejemplos https://api.example.co.jp/... es un endpoint ficticio; no funcionará tal cual. Los ejemplos de JSON de respuesta también son ficticios, incluidos solo con fines explicativos

1. La conclusión, por adelantado

  • Para una API de JSON/XML, use Invoke-RestMethod. Convierte automáticamente la respuesta en un objeto. Si necesita el código de estado o las cabeceras, use Invoke-WebRequest, o bien -StatusCodeVariable / -ResponseHeadersVariable.23
  • La forma más versátil de pasar la autenticación es incluirla en la cabecera. A partir de PowerShell 6 también puede usar -Authentication Bearer -Token (con SecureString).2
  • La forma segura de enviar JSON en japonés es como un array de bytes UTF-8. Declare explícitamente charset=utf-8 en -ContentType.
  • La profundidad predeterminada de ConvertTo-Json es 2. Si el objeto tiene una anidación más profunda, se trunca a menos que especifique -Depth.4
  • Un 4xx/5xx se convierte en un error de terminación. A partir de PowerShell 7 puede leer el cuerpo con $_.ErrorDetails.Message. También puede optar por -SkipHttpErrorCheck para evitar que se convierta en excepción.2
  • Ante un 429, espere según indique Retry-After. Si especifica -MaximumRetryCount, la función integrada lo sigue automáticamente. Sin embargo, el reintento integrado cubre todo el rango 400-599 (y 304), por lo que también reenvía en un 401 o un 404. Implemente su propia lógica solo cuando necesite tratar cada código de forma distinta o aplicar retroceso exponencial.2
  • No reintente automáticamente solicitudes no idempotentes como POST. La idempotencia es la propiedad por la que el resultado no cambia sin importar cuántas veces se envíe la misma solicitud; GET, PUT y DELETE son idempotentes, mientras que POST no lo es. Incluso ante un error de comunicación o un 5xx, es posible que el servidor ya haya procesado la solicitud, y reenviarla provoca un registro duplicado.
  • La paginación mediante cabecera Link se puede automatizar con -FollowRelLink. La paginación por cursor requiere un bucle propio.2
  • Windows PowerShell 5.1 tiene trampas propias. Son tres: si hace falta o no -UseBasicParsing, la activación explícita de TLS 1.2 y el tratamiento de la codificación.1
  • No use -SkipCertificateCheck en un uso permanente. Lo correcto es hacer que se confíe en la CA interna.

2. Invoke-RestMethod frente a Invoke-WebRequest

Primero, veamos la diferencia entre ambos.23

  Invoke-RestMethod Invoke-WebRequest
Tratamiento de la respuesta JSON/XML se convierte automáticamente en objeto WebResponseObject (cuerpo sin procesar, cabeceras, código)
Uso principal API REST Obtención de HTML, consulta de estado y cabeceras
Código de estado Se obtiene con -StatusCodeVariable (PS7+) .StatusCode
Cabeceras Se obtienen con -ResponseHeadersVariable (PS6+) .Headers

Para llamar a una API, lo básico es Invoke-RestMethod. Incluso cuando necesite cabeceras o el código, puede obtenerlos con los parámetros de variable dedicados.

$data = Invoke-RestMethod -Uri 'https://api.example.co.jp/v1/orders' `
        -Headers @{ Authorization = "Bearer $token" } `
        -StatusCodeVariable status -ResponseHeadersVariable headers -TimeoutSec 30

"HTTP $status / Solicitudes restantes: $($headers['X-RateLimit-Remaining'])"
$data.items | Select-Object orderId, customerName, amount

3. Cómo pasar la autenticación

La forma más versátil es escribirla directamente en la cabecera; funciona igual tanto en 5.1 como en 7.

# (1) Token Bearer (el más habitual)
$headers = @{ Authorization = "Bearer $accessToken"; Accept = 'application/json' }
Invoke-RestMethod -Uri $uri -Headers $headers

# (2) Clave de API (el nombre de la cabecera depende del proveedor)
$headers = @{ 'X-Api-Key' = $apiKey }

# (3) Autenticación Basic (desde PowerShell 6 se puede usar -Authentication)
Invoke-RestMethod -Uri $uri -Authentication Basic -Credential $cred

# (4) Pasar el Bearer con -Token (desde PowerShell 6; admite SecureString)
Invoke-RestMethod -Uri $uri -Authentication Bearer -Token $secureToken

# (5) Certificado de cliente
Invoke-RestMethod -Uri $uri -Certificate $cert

Cuando usa -Authentication, PowerShell rechaza de forma predeterminada su uso fuera de HTTPS (puede evitarlo con -AllowUnencryptedAuthentication, pero no debería usarlo, porque las credenciales viajarían en texto claro).2

No escribir tokens ni claves de API directamente en el script es una premisa básica. La forma de guardarlos con SecretManagement se explica en «Cómo tratar las credenciales de forma segura en PowerShell».

4. Enviar JSON ── la trampa del japonés y de -Depth

Al enviar, los dos escollos en los que se tropieza con seguridad son la codificación rota y el truncado de la anidación.

El -Depth de ConvertTo-Json tiene un valor predeterminado de 2, y los niveles más profundos no se expanden: se sustituyen por una cadena con el nombre del tipo, entre otras cosas.4 Especifíquelo siempre que el cuerpo de la solicitud tenga anidación.

$body = @{
    order = @{
        customer = @{ code = 'C001'; name = '株式会社サンプル' }   # tercer nivel de anidación
        lines    = @( @{ item = 'A-100'; qty = 3 } )
    }
}

# 【MAL】con el -Depth 2 predeterminado se pierde el contenido de customer y lines
$json = $body | ConvertTo-Json

# 【BIEN】especifique una profundidad suficiente
$json = $body | ConvertTo-Json -Depth 10

La medida más segura contra la codificación rota es convertir el cuerpo en un array de bytes UTF-8 antes de enviarlo.

$json  = $body | ConvertTo-Json -Depth 10
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)

$res = Invoke-RestMethod -Uri $uri -Method Post `
       -Headers @{ Authorization = "Bearer $token" } `
       -ContentType 'application/json; charset=utf-8' `
       -Body $bytes -TimeoutSec 60

En PowerShell 7, aunque pase la cadena tal cual, se envía en UTF-8, pero en un script que también deba funcionar en 5.1, convertirla en un array de bytes absorbe la diferencia entre entornos. Para el tema general de la codificación de caracteres en Windows, consulte «Codificación de caracteres y fin de línea en Windows».

Compruebe la ida y la vuelta: antes de enviar y después de recibir. El JSON que se pretende construir a partir del $body anterior tiene esta forma.

{
  "order": {
    "customer": { "code": "C001", "name": "株式会社サンプル" },
    "lines": [ { "item": "A-100", "qty": 3 } ]
  }
}

Si olvida especificar -Depth, el contenido de customer y lines, que están en el tercer nivel, no se expande y no se obtiene esta forma. La forma más rápida de comprobarlo es mostrar $json en pantalla antes de enviarlo y verificar a simple vista si la jerarquía sigue presente (como se construye a partir de una tabla hash, el orden de las claves depende del orden de enumeración; si quiere fijar el orden, use [ordered]@{}).

Suponga que, ante esta solicitud, la API responde así (ejemplo ficticio).

{
  "orderId": "2026-000123",
  "status": "accepted",
  "customer": { "code": "C001", "name": "株式会社サンプル" }
}

Como Invoke-RestMethod convierte automáticamente la respuesta en un objeto, en el lado receptor puede escribir lo siguiente.

$res.orderId              # 2026-000123
$res.customer.name        # 株式会社サンプル ── si esto aparece corrupto, el problema está en la recepción

El diagnóstico de la codificación rota en la recepción se hace comprobando si $res.customer.name se lee correctamente. Si aparece corrupto, sospeche de que el charset del Content-Type de la respuesta no esté declarado correctamente (puede obtener las cabeceras con -ResponseHeadersVariable). Esta comprobación de ida y vuelta permite distinguir si el origen del problema está en el envío o en la recepción.

5. Tratamiento de errores ── sin poder leer el cuerpo no hay forma de investigar

Invoke-RestMethod trata las respuestas 4xx/5xx como un error de terminación. Es decir, puede capturarlas con try/catch, pero el problema pasa a ser cómo leer el cuerpo del mensaje de error que devolvió la API.

try {
    $res = Invoke-RestMethod -Uri $uri -Method Post -Body $bytes `
           -ContentType 'application/json; charset=utf-8' -TimeoutSec 30
}
catch {
    $status = $_.Exception.Response.StatusCode      # ejemplo: BadRequest / 400
    # a partir de PowerShell 7, aquí se guarda el cuerpo de la respuesta (el mensaje de error de la API)
    $detail = $_.ErrorDetails.Message
    Write-Warning "Fallo de la API ($status): $detail"
    throw
}

El punto que hay que comprobar. En $status se guarda el estado HTTP como un valor de enumeración (como indica el comentario, se muestra como BadRequest), y en $detail se guarda tal cual, como cadena, el cuerpo que devolvió la API. Si la API devuelve JSON, puede extraer los campos con $detail | ConvertFrom-Json y bifurcar según el código de error o el nombre del campo. Si, por el contrario, $detail queda vacío, es que la API no devuelve cuerpo o que está ejecutando en Windows PowerShell 5.1 (en 5.1 hay que leer usted mismo el flujo de la respuesta; véase el §8).

Para no perder tiempo investigando un «me devolvió HTTP 400, pero no sé por qué lo rechazó», diseñe el sistema de forma que el cuerpo del error se registre siempre en el log.

Cuando el proceso necesita bifurcar según el código de estado, resulta más directo usar -SkipHttpErrorCheck para evitar que se convierta en excepción (desde PowerShell 7).2

$res = Invoke-RestMethod -Uri $uri -SkipHttpErrorCheck -StatusCodeVariable code -TimeoutSec 30
switch ($code) {
    200     { $res.items }
    404     { Write-Warning 'El recurso no existe'; @() }
    { $_ -ge 500 } { throw "Error del lado del servidor: $code" }
    default { throw "Respuesta inesperada: $code" }
}

6. Reintentos ── el 429 y los fallos temporales

A partir de PowerShell 6, Invoke-RestMethod cuenta con -MaximumRetryCount y -RetryIntervalSec, que reintentan en caso de fallo. Además, cuando la respuesta 429 incluye Retry-After, se usa el valor de esa cabecera en lugar del intervalo especificado.2 Es decir, si lo único que necesita es «al chocar con el límite de frecuencia, esperar lo indicado y reintentar», la función integrada basta.

# si solo necesita responder al límite de frecuencia, esto suele bastar
Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRetryCount 4 -RetryIntervalSec 5

Sin embargo, el objetivo del reintento no se limita al 429. La documentación establece que se reintenta «al recibir un código de fallo entre 400 y 599 (y 304)».2 Es decir, también se reintentan solicitudes que, por más veces que se envíen, nunca se van a corregir, como un 401 (error de autenticación), un 403 (permisos insuficientes) o un 404 (URL incorrecta). Si ejecuta el script sin supervisión con el token mal configurado, se desperdician -MaximumRetryCount × -RetryIntervalSec segundos hasta descubrir el fallo, y la API recibe repetidamente la misma solicitud inválida. Si quiere detenerse de inmediato ante un error permanente, necesita la implementación propia que se muestra a continuación.

Necesita una función de reintento propia cuando tiene requisitos como los siguientes.

  • Quiere tratar cada código de estado de forma distinta (fallo inmediato para los 400, reintento solo para los 5xx, etc.)
  • Quiere un retroceso exponencial (la función integrada reintenta con el intervalo indicado)
  • Quiere registrar en el log el cuerpo del error de la API cuando falla

A continuación se muestra un ejemplo de esa forma.

function Invoke-KsApi {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Uri,
        [string] $Method = 'Get',
        [object] $Body,
        [hashtable] $Headers = @{},
        [ValidateRange(1, 10)] [int] $MaxAttempts = 4,
        # si Retry-After indica una espera más larga que esto, se interrumpe sin esperar
        [ValidateRange(1, 86400)] [int] $MaxWaitSeconds = 300,
        # permite reintentar POST y similares solo si la API admite clave de idempotencia
        [string] $IdempotencyKey
    )

    # solo se puede reintentar cuando el resultado no cambia aunque el servidor
    # reciba la misma solicitud dos veces. POST/PATCH pueden darse en el caso de
    # que el servidor haya tenido éxito pero la respuesta no llegara, y reenviar
    # sin más provoca un registro duplicado
    $idempotentMethods = 'Get', 'Head', 'Options', 'Put', 'Delete'
    $canRetry = ($Method -in $idempotentMethods) -or $IdempotencyKey
    if ($IdempotencyKey) { $Headers = $Headers + @{ 'Idempotency-Key' = $IdempotencyKey } }

    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        $params = @{
            Uri                     = $Uri
            Method                  = $Method
            Headers                 = $Headers
            TimeoutSec              = 60
            SkipHttpErrorCheck      = $true          # no debe convertirse en excepción, porque bifurcamos por código
            StatusCodeVariable      = 'code'
            ResponseHeadersVariable = 'resHeaders'
        }
        # para poder enviar $Body como cuerpo aunque su valor sea $false, 0 o
        # una cadena vacía, se comprueba si se pasó el argumento, no su valor de verdad
        if ($PSBoundParameters.ContainsKey('Body')) {
            # si se pasa por la canalización, un array vacío @() se interpreta
            # como «0 elementos de entrada» y devuelve $null, así que se convierte
            # directamente con -InputObject sin enumerarlo (@() se convierte en [])
            $json = ConvertTo-Json -InputObject $Body -Depth 10
            $params.Body        = [System.Text.Encoding]::UTF8.GetBytes($json)
            $params.ContentType = 'application/json; charset=utf-8'
        }

        # -SkipHttpErrorCheck solo suprime las respuestas de error HTTP. Los
        # errores de comunicación en los que no llega respuesta -tiempo de espera
        # agotado, fallo de resolución de nombres, reinicio de conexión, error de
        # TLS- se lanzan como excepción, así que aquí se capturan y se reintenta
        try {
            $code = $null
            $res  = Invoke-RestMethod @params
        }
        catch {
            # en una solicitud no idempotente, es posible que el servidor haya
            # tenido éxito y solo no haya llegado la respuesta. No se reenvía
            # automáticamente; se deja la decisión al llamador
            if (-not $canRetry) {
                throw "Error de comunicación ($Method no se reintenta. Compruebe si ya se procesó): $($_.Exception.Message)"
            }
            if ($attempt -eq $MaxAttempts) { throw }
            $wait = [math]::Min([math]::Pow(2, $attempt), 60)
            Write-Warning "Error de comunicación: $($_.Exception.Message) ── se reintentará en $wait segundos ($attempt/$MaxAttempts)"
            Start-Sleep -Seconds $wait
            continue
        }

        if ($code -lt 400) { return $res }                    # éxito

        # solo vale la pena reintentar los errores temporales; se declara
        # explícitamente con una lista de permitidos (si se reintenta un error
        # permanente como 405 o 415, se pierde tiempo y al final solo queda un
        # mensaje irrelevante de «límite de reintentos»)
        $retryable = @(408, 429, 500, 502, 503, 504)
        if ($code -notin $retryable) {
            throw "Error de la API ($code): $($res | ConvertTo-Json -Compress -Depth 3)"
        }
        # un 5xx puede deberse a que el fallo ocurriera después de que el
        # servidor procesara la solicitud, así que no se reenvía en solicitudes
        # no idempotentes; lo mismo aplica al 429 (no hay garantía de que se
        # rechazara antes de procesarla)
        if (-not $canRetry) {
            throw "Error de la API ($code). $Method no se reintenta automáticamente: $($res | ConvertTo-Json -Compress -Depth 3)"
        }

        if ($attempt -eq $MaxAttempts) {
            throw "Se alcanzó el límite de reintentos ($code): $($res | ConvertTo-Json -Compress -Depth 3)"
        }

        # Retry-After puede devolverse no solo como «número de segundos», sino
        # también en formato de fecha HTTP. Convertirlo a [int] directamente
        # provoca una excepción, y falla en cada reintento
        $wait = $null
        $retryAfter = if ($resHeaders) { $resHeaders['Retry-After'] | Select-Object -First 1 }
        if ($retryAfter) {
            $seconds = 0
            $date    = [datetime]::MinValue
            if ([int]::TryParse($retryAfter, [ref] $seconds)) {
                $wait = $seconds
            }
            elseif ([datetime]::TryParse($retryAfter,
                        [cultureinfo]::InvariantCulture,
                        [System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref] $date)) {
                # la fracción siempre debe redondearse hacia arriba. [int] redondea
                # al más cercano (con desempate al par), así que redondearía 0,4
                # segundos restantes a 0 y reenviaría antes del plazo del servidor
                $remaining = ($date - [datetime]::UtcNow).TotalSeconds
                $wait = if ($remaining -gt 0) { [int][math]::Ceiling($remaining) } else { 0 }
            }
        }
        if ($null -eq $wait) {
            $wait = [math]::Min([math]::Pow(2, $attempt), 60)             # retroceso exponencial (tope de 60 segundos)
        }
        elseif ($wait -gt $MaxWaitSeconds) {
            # si se recorta por cuenta propia la indicación del servidor para
            # reenviar antes, solo se repetirá el 429 hasta alcanzar el límite.
            # Si la espera es demasiado larga, se devuelve al llamador con ella
            throw "Está limitado por frecuencia. Se interrumpió porque el tiempo de espera indicado por el servidor, $wait segundos, supera el límite de $MaxWaitSeconds segundos. Vuelva a ejecutarlo más tarde ($code)"
        }
        Write-Warning "HTTP $code ── se reintentará en $wait segundos ($attempt/$MaxAttempts)"
        Start-Sleep -Seconds $wait
    }
}

Compruebe si de verdad está reintentando. Cuando esta función entra en el ciclo de reintento, cada intento produce una línea de Write-Warning (con la forma HTTP 429 ── se reintentará en 30 segundos (1/4)). Hay tres puntos que revisar: si los segundos de espera coinciden con lo indicado por el servidor, si el número de intentos se mantiene dentro del rango de -MaxAttempts, y si ante un error permanente como 401 o 404 se produce de inmediato una excepción sin reintentar. En una ejecución sin supervisión no queda pantalla, así que envíe también el flujo de advertencias al log («Deje de usar Write-Host ── diseño de flujos de salida y logs en PowerShell»).

El segundo punto clave es el tratamiento de la idempotencia (la propiedad de que el resultado no cambia sin importar cuántas veces se envíe la misma solicitud). GET y PUT no cambian de resultado aunque el servidor reciba la misma solicitud dos veces, pero POST es distinto. En especial, en el caso de que «el servidor haya tenido éxito registrando los datos, pero la comunicación se cortara antes de que llegara la respuesta», reenviar sin más provoca un registro duplicado. En la implementación anterior, solo se permite el reintento automático cuando el método es idempotente o cuando la API admite una clave de idempotencia (Idempotency-Key); en cualquier otro caso, se detiene indicando explícitamente «compruebe si ya se procesó».

Cuando Retry-After llega en formato de fecha HTTP, redondee hacia arriba el tiempo restante. La conversión a [int] no trunca, sino que redondea al valor más cercano (con desempate al lado par cuando la fracción es exactamente 0,5), así que, si el tiempo restante tras descontar lo que tarda en llegar la respuesta baja de 0,5 segundos, se convierte en 0. Como Start-Sleep -Seconds 0 devuelve el control de inmediato, esto provoca que se reenvíe antes de la hora indicada por el servidor, y lo que vuelve es otro 429. Esto se repite tantas veces como intentos haya y termina en «límite de reintentos». Por eso, en la implementación anterior se intercala [math]::Ceiling.

No recortar Retry-After es otro punto importante. Si el servidor indica «vuelva en 30 minutos» y usted reenvía a los 5 minutos alegando el límite propio, lo único que obtiene es el mismo 429. Agota los intentos con solicitudes inútiles y al final solo queda el mensaje «límite de reintentos». En la implementación anterior, cuando el tiempo de espera indicado supera -MaxWaitSeconds, en lugar de esperar un poco menos y reintentar, se falla de inmediato incluyendo el tiempo de espera. En un proceso por lotes, al recibir esta excepción, el llamador puede elegir entre posponerlo a la siguiente ejecución o esperar exactamente el tiempo indicado.

Otro punto es que el objetivo del reintento se declara explícitamente mediante una lista de permitidos. Si se escribe al revés -«enumerar y excluir solo los errores permanentes»-, cualquier código que se olvide incluir ahí (como 405 Method Not Allowed o 415 Unsupported Media Type) se trata como un error temporal, se repite una solicitud que nunca se va a corregir, y al final solo queda un mensaje de causa desconocida: «límite de reintentos». Lo correcto es reintentar solo los códigos que se sabe que son temporales, y hacer fallar de inmediato todos los demás, junto con el contenido del error de la API. Para el diseño general de reintentos, consulte «Diseño de tratamiento de errores y reintentos en PowerShell».

7. Paginación

Es raro que una API devuelva todos los registros de una vez. Hay principalmente dos métodos.

(1) El método de la cabecera Link (el que usa GitHub, entre otros) permite recorrer automáticamente las páginas siguientes con -FollowRelLink.2

# recorre automáticamente las páginas siguientes (también se puede limitar el número de páginas)
$all = Invoke-RestMethod -Uri $uri -Headers $headers -FollowRelLink -MaximumFollowRelLink 20

(2) El método de cursor u offset requiere un bucle propio. Tenga en cuenta que el siguiente código usa el Invoke-KsApi definido en el §6. Si solo quiere probar esta sección, también funciona sustituir Invoke-KsApi -Uri $u -Headers $headers por Invoke-RestMethod -Uri $u -Headers $headers (simplemente se pierden el reintento y el registro del cuerpo del error).

$items    = [System.Collections.Generic.List[object]]::new()
$cursor   = $null
$page     = 0
$maxPages = 100

do {
    $page++

    # el separador cambia según si la URI original ya tiene o no una cadena de
    # consulta. Si siempre se añade ?, queda .../items?status=active?cursor=...,
    # y el servidor interpreta el cursor como parte del valor de status.
    # Además, el propio cursor es una cadena opaca (puede incluir +, &, =, # y
    # similares), así que hay que codificarlo siempre
    $u = if ($cursor) {
        $sep = if ($uri.Contains('?')) { '&' } else { '?' }
        "$uri$sep" + "cursor=$([uri]::EscapeDataString($cursor))"
    }
    else { $uri }

    $res = Invoke-KsApi -Uri $u -Headers $headers
    $items.AddRange([object[]]$res.items)
    $cursor = $res.nextCursor

    # la interrupción siempre debe notificarse; si se sale en silencio, se
    # malinterpreta como que se obtuvieron todos los registros
    if ($page -ge $maxPages -and $cursor) {
        Write-Warning "Se interrumpió porque se alcanzó el límite de páginas ($maxPages). Es posible que falten registros por obtener"
        break
    }
} while ($cursor)

"Registros obtenidos: $($items.Count)"

El uso de List[T] para $items es para evitar la recreación del array que provoca +=Dónde mirar cuando un script de PowerShell va lento»).

La comprobación se hace con la última línea. Contraste el número que aparece en Registros obtenidos: ... con el total del lado de la API (muchas APIs incluyen en la respuesta un campo como total) o con el número que muestra el panel de administración. Si no coinciden, mire primero si apareció el aviso de interrupción. Si el número es insuficiente y no salió ningún aviso, sospeche de que el nombre del campo nextCursor no coincide con la especificación.

No olvide tampoco fijar siempre un límite de páginas. En la práctica ocurren fallos como que el servidor siga devolviendo el mismo cursor, o que se olvide de dejar nextCursor vacío. Sin un límite, el script seguiría lanzando solicitudes sin detenerse. Y el hecho de haberse interrumpido debe mostrarse como una advertencia. Si se hace un break en silencio, el llamador procesará un resultado incompleto creyendo que son todos los registros.

8. Trampas propias de Windows PowerShell 5.1

En entornos donde todavía queda la versión 5.1, sospeche primero de estos tres puntos.1

(1) TLS 1.2 no está habilitado. Si se deja la configuración predeterminada antigua, no se puede conectar a una API que solo acepta TLS 1.2 o superior, y aparece un error del tipo «se ha cerrado la conexión subyacente».

# fórmula mágica para Windows PowerShell 5.1 (al inicio del script)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

(2) A veces hace falta -UseBasicParsing. El Invoke-WebRequest de 5.1 usa de forma predeterminada el motor de Internet Explorer para analizar el HTML, así que falla con cuentas en las que IE no está inicializado (como las cuentas de servicio). A partir de PowerShell 6 desaparece esta dependencia, y -UseBasicParsing se ignora aunque se especifique.1

(3) No existen -SkipHttpErrorCheck ni -Authentication. En 5.1, para leer el cuerpo del error hay que leer usted mismo el flujo de la respuesta. Si va a integrar APIs en serio, la opción más económica es adoptar PowerShell 7 («Diferencias entre Windows PowerShell 5.1 y PowerShell 7»).

9. Proxy y certificados

Cuando se llama desde la red interna a una API en internet, atravesar el proxy es el primer obstáculo. Si no especifica -Proxy, se usa el proxy configurado en la configuración de Internet (Opciones de Internet) o en las variables de entorno.2 Es decir, «no especificar nada» no equivale a «no usar proxy». Este es el motivo por el que el resultado varía según la cuenta que ejecuta el script.

Síntoma Dónde buscar la causa Solución
407 Proxy Authentication Required El proxy exige autenticación Especifique -ProxyCredential o -ProxyUseDefaultCredentials junto con -Proxy2
Funciona en el puesto local pero solo falla el proceso por lotes nocturno Se está tomando una configuración de Internet distinta según la cuenta de ejecución Especifique -Proxy explícitamente en el script. Verifique igualando la cuenta de ejecución
Hasta las APIs internas se enrutan por el proxy La configuración de proxy predeterminada también se aplica al tráfico interno Rodéelo explícitamente con -NoProxy (desde PowerShell 6)2
Dónde guardar las credenciales del proxy Escritas directamente en el script Extráigalas del almacén de SecretManagement
# especifica el proxy explícitamente y se autentica con las credenciales del usuario que inició sesión
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyUseDefaultCredentials

# pasa por un proxy con autenticación usando una cuenta dedicada.
# -ProxyCredential se usa junto con -Proxy. No se puede combinar con -ProxyUseDefaultCredentials
$proxyCred = Get-Secret -Name 'ProxyAccount'   # se extrae del almacén
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyCredential $proxyCred

# las APIs internas no pasan por el proxy (desde PowerShell 6)
Invoke-RestMethod -Uri 'https://api.internal.example.local/v1/ping' -NoProxy

Tanto -ProxyCredential como -ProxyUseDefaultCredentials presuponen que se ha especificado -Proxy, y no se pueden usar a la vez.2 Windows PowerShell 5.1 no tiene -NoProxy. Si en 5.1 quiere excluir del proxy solo las comunicaciones dirigidas a la red interna, tendrá que resolverlo mediante la lista de excepciones (direcciones que no usan proxy) de la configuración de Internet de la cuenta de ejecución.

Cuando se ejecuta con la cuenta de servicio del Programador de tareas, la configuración de proxy puede ser distinta a la de un inicio de sesión interactivo. Es el patrón típico de «funciona en mi puesto pero solo falla el proceso por lotes nocturno». Verifique igualando la cuenta de ejecución («Las tareas del Programador de tareas no se ejecutan»). No escribir las credenciales del proxy directamente en el script es tan importante como con los tokens de la API («Cómo tratar las credenciales de forma segura en PowerShell»).

Limite el uso de -SkipCertificateCheck ante un error de certificado exclusivamente a una solución temporal en el entorno de verificación. El tratamiento permanente consiste en colocar el certificado de la CA interna en el almacén de entidades de certificación raíz de confianza y emitir correctamente el certificado del servidor.

10. Reglas prácticas (tabla de decisión)

Cuestión Opciones Criterio de decisión
Cmdlet Invoke-RestMethod / Invoke-WebRequest Para una API de JSON, el primero. Las cabeceras y el código se obtienen con variables dedicadas23
Autenticación Escrita directamente en la cabecera / -Authentication Si se comparte con 5.1, el método de cabecera. Guarde los valores con SecretManagement
Envío de JSON Cadena / Array de bytes UTF-8 + charset explícito Evita la codificación rota debida a diferencias de entorno
ConvertTo-Json Predeterminado / -Depth explícito El valor predeterminado es 2. Especifíquelo siempre que haya anidación4
Errores Solo try/catch / registrar también el cuerpo en el log $_.ErrorDetails.Message (PS7). Determina si se puede investigar la causa2
Muchas bifurcaciones catch / -SkipHttpErrorCheck + bifurcación por código Si va a dividir el proceso por estado, lo segundo es más directo2
Reintento -MaximumRetryCount / propio La función integrada sigue automáticamente el Retry-After del 429. Pero como reintenta todo el rango 400-599, use el propio si quiere fallar de inmediato ante un error permanente2
Reintento de POST Solo si hay clave de idempotencia Es posible que el registro se completara y solo faltara la respuesta; un reenvío ingenuo provoca un registro duplicado
Paginación -FollowRelLink / bucle propio Si es el método de cabecera Link, lo primero2
Entorno 5.1 Tal cual / TLS 1.2 explícito + valorar adoptar PS7 La mayoría de los errores de conexión se deben a la configuración de TLS1
Error de certificado -SkipCertificateCheck / confiar en la CA interna No desactive la validación en un uso permanente

11. Resumen

  • Para una API de JSON, lo básico es Invoke-RestMethod. Las cabeceras y el código de estado se obtienen con -ResponseHeadersVariable / -StatusCodeVariable.
  • Envíe el JSON en japonés como array de bytes UTF-8 y declare explícitamente charset=utf-8. Olvidar especificar ConvertTo-Json -Depth provoca la pérdida de la anidación.
  • Un 4xx/5xx es un error de terminación. Lea el cuerpo con $_.ErrorDetails.Message y regístrelo en el log. Si hay muchas bifurcaciones, -SkipHttpErrorCheck resulta más manejable.
  • No reintente automáticamente solicitudes no idempotentes como POST. Un error de comunicación o un 5xx puede darse cuando «el servidor ya lo procesó», y reenviar provoca un registro duplicado. Si quiere reintentar, necesita un mecanismo de clave de idempotencia.
  • Ante un 429, lo básico es esperar según indique Retry-After. Si usa -MaximumRetryCount, este seguimiento lo hace la función integrada. Pero como esta reintenta todo el rango 400-599, si quiere que un 401 o un 404 fallen de inmediato necesita un reintento propio. Lo mismo aplica cuando necesita un tratamiento distinto por código o retroceso exponencial: los errores permanentes deben fallar de inmediato, sin reintentar.
  • Para la paginación, use -FollowRelLink si es el método de cabecera Link, y un bucle propio si es por cursor. No use += para acumular.
  • En un entorno 5.1, los obstáculos son tres: la activación explícita de TLS 1.2, la dependencia del motor de IE y la falta de funciones. Si va a mantener la integración con APIs, adoptar PowerShell 7 es la solución más rápida.

Descarga del código de ejemplo

El código tratado en este artículo se distribuye ya empaquetado y listo para ejecutar. Incluye una llamada a la API con reintento, idempotencia y paginación implementados, junto con un servidor HTTP para verificación.

Descargar el código de ejemplo (zip)

Los ejemplos de este artículo se han ejecutado y verificado realmente con PowerShell 7.6 (21 pruebas Pester). Si ejecuta Invoke-SampleTests.ps1, incluido en el zip, puede reproducir la misma verificación en su propio equipo.

# análisis de sintaxis + análisis estático + pruebas Pester
./Invoke-SampleTests.ps1

El zip incluye un servidor HTTP para verificación, así que puede reproducir la ida y vuelta en su equipo sin llamar a ninguna API externa. Pruebe directamente los puntos de comprobación mencionados en el §5 (el cuerpo del error), el §6 (el aviso de reintento ante el 429) y el §7 (el número de registros obtenidos). Comprobar estos tres puntos antes de apuntar a la API real reduce las probabilidades de llegar a producción sin saber «por qué no funciona».

Los valores de configuración (rutas, nombres de servidor, ID de inquilino, etc.) son ejemplos. No los ejecute tal cual en un entorno de producción: adáptelos al entorno de su empresa.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa del diseño y la implementación de integraciones internas mediante APIs de sistemas principales o de SaaS, la automatización que sustituye el trabajo manual existente por integración de APIs, y la investigación de fallos relacionados con las comunicaciones.

Referencias

  1. Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. Sobre las diferencias de comportamiento de los cmdlets relacionados con la web, las restricciones propias de Windows PowerShell 5.1 y las funciones añadidas en PowerShell 7. Junto con la especificación de la versión de TLS mediante la propiedad ServicePointManager.SecurityProtocol 2 3 4 5

  2. Microsoft Learn, Invoke-RestMethod. Sobre el envío de solicitudes a un endpoint REST y la conversión de la respuesta JSON/XML en un objeto de PowerShell; la especificación de -Headers / -Body / -ContentType / -Method; -Authentication (Basic / Bearer / OAuth) y -Token; el rechazo predeterminado de la autenticación fuera de HTTPS; la desactivación de la conversión en excepción de 4xx/5xx mediante -SkipHttpErrorCheck; la obtención mediante -StatusCodeVariable y -ResponseHeadersVariable; el reintento mediante -MaximumRetryCount / -RetryIntervalSec; la paginación de la cabecera Link mediante -FollowRelLink / -MaximumFollowRelLink; -Proxy / -ProxyCredential / -ProxyUseDefaultCredentials (todas presuponen -Proxy, y -ProxyCredential y -ProxyUseDefaultCredentials no se pueden combinar); -NoProxy, añadido en PowerShell 6.0 (para rodear el proxy configurado en la configuración de Internet o en variables de entorno); -SkipCertificateCheck; y -TimeoutSec.  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

  3. Microsoft Learn, Invoke-WebRequest. Sobre la devolución de la respuesta como WebResponseObject, con acceso a StatusCode, Headers y Content; sobre el uso predeterminado del motor de Internet Explorer para analizar HTML en Windows PowerShell 5.1, que se puede evitar con -UseBasicParsing; y sobre la desaparición de la dependencia de IE a partir de PowerShell 6, donde -UseBasicParsing se ignora.  2 3

  4. Microsoft Learn, ConvertTo-Json. Sobre el valor predeterminado de -Depth, que es 2, y sobre cómo los niveles más profundos no se convierten; y sobre la eliminación de espacios en blanco mediante -Compress.  2 3

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Cómo se decide entre usar Invoke-RestMethod o Invoke-WebRequest?
Si va a tratar JSON o XML de una API, use Invoke-RestMethod. Analiza automáticamente el cuerpo de la respuesta y lo convierte en un objeto de PowerShell, por lo que no necesita llamar usted mismo a ConvertFrom-Json. Invoke-WebRequest, en cambio, devuelve la respuesta como un HtmlWebResponseObject y le permite acceder al código de estado, a las cabeceras y al cuerpo sin procesar. Elija Invoke-WebRequest cuando necesite consultar el código de estado o las cabeceras, o cuando quiera tratar el HTML directamente. Tenga en cuenta que, a partir de PowerShell 6, Invoke-RestMethod incorpora -ResponseHeadersVariable y -StatusCodeVariable, de modo que si solo necesita las cabeceras o el código, puede obtenerlos sin cambiar de cmdlet.
Al enviar un JSON que contiene japonés, el texto llega con la codificación rota en el otro extremo.
La causa habitual es un desajuste entre los bytes reales del cuerpo y lo que declara el Content-Type. La forma segura de evitarlo es convertir la cadena generada por ConvertTo-Json en un array de bytes UTF-8, pasarlo en -Body y declarar explícitamente charset=utf-8 en -ContentType. En Windows PowerShell 5.1, si se pasa la cadena tal cual, a veces se envía con la codificación predeterminada del sistema, así que este ajuste resulta especialmente útil ahí. Si el problema aparece al recibir la respuesta, sospeche de lo mismo: compruebe que el charset de la respuesta esté declarado correctamente.
Cuando la API devuelve un 404 o un 500, quiero leer el mensaje de error del cuerpo de la respuesta.
A partir de PowerShell 7, dentro del bloque catch puede consultar $_.ErrorDetails.Message, que contiene el cuerpo de la respuesta. El código de estado se obtiene con $_.Exception.Response.StatusCode. Además, si añade -SkipHttpErrorCheck, las respuestas 4xx/5xx dejan de convertirse en excepción y se reciben como una respuesta normal, lo que facilita el tratamiento cuando quiere bifurcar según el código de estado. En Windows PowerShell 5.1 hay que leer usted mismo el flujo de la respuesta, otro motivo más para recomendar el uso de PowerShell 7.
La API me devuelve un 429 (límite de frecuencia). ¿Cómo debo tratarlo?
Lo básico es esperar el número de segundos que indique la cabecera Retry-After de la respuesta y reintentar después. Si no hay cabecera, amplíe el intervalo con un retroceso exponencial (2 segundos, 4, 8…). A partir de PowerShell 6 existen -MaximumRetryCount y -RetryIntervalSec, y cuando la respuesta 429 incluye Retry-After, se usa el valor de esa cabecera en lugar del intervalo indicado, así que si solo necesita responder al límite de frecuencia, la función integrada basta. Recurra a una función de reintento propia únicamente si necesita tratar cada código de estado de forma distinta o si necesita retroceso exponencial. Además, evite reintentar solicitudes no idempotentes como POST, salvo que exista un mecanismo de clave de idempotencia, porque hacerlo puede provocar un registro duplicado. En el fondo, lo más fiable es reducir el propio número de llamadas (pedir solo los campos necesarios, usar APIs de obtención en lote).
Obtengo un error por el certificado autofirmado de una API interna. ¿Puedo usar -SkipCertificateCheck?
Evítelo en un uso permanente. Detener la validación del certificado significa dejar de comprobar si el interlocutor es realmente quien dice ser, y eso deja margen para un ataque de intermediario incluso dentro de la red corporativa. El tratamiento correcto es colocar el certificado de la CA interna en el almacén de entidades de certificación raíz de confianza del entorno de ejecución y emitir correctamente el certificado del servidor. Aunque lo use de forma temporal en un entorno de verificación, gestione el cambio de forma explícita mediante un archivo de configuración o un parámetro, para que no se filtre nunca a los scripts de producción.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog