Skip to main content

Debugging de problemas de auth con WSProxy

Las primeras 1.000 llamadas de WSProxy funcionan, después todo lo que pasa el minuto 20 devuelve 'Token has expired'. O la auth falla en la llamada uno sin razón obvia. Cinco queries contra tus DE de log que separan el patrón timeout del patrón credenciales del patrón multi-BU.

Cómo hacerlo·Actualizado 2026-05-13·Escrito por Lira · Editado por German Medina

Escribiste una Script Activity que usa WSProxy para recuperar subscribers vía la SOAP API. La probaste en preview contra un batch chico — funcionó. La promoviste a producción con una audiencia real — bien para las primeras mil filas o algo así, después todo lo que pasa el minuto 20 devuelve Token has expired o Unauthorized, el bloque catch del script escribe errores a de_log_ssjs_errors, y las Activities downstream corren contra la mitad del data. La falla es el token de auth de WSProxy que expira en la marca de los ~20 minutos y un script que no re-instanció el proxy. Ver gotchas — #8.

Esta página es el playbook de diagnóstico para esa forma exacta — cinco queries que confirman el fingerprint del timeout de auth y descartan las otras formas (credenciales malas, setClientId faltante para acceso cross-BU, Installed Package vieja, red).

El lifecycle de auth

[ new Script.Util.WSProxy() ]
        ↓ llamada OAuth implícita → access token cacheado en el prox
[ primer .retrieve() / .create() / .update() — usa el token cacheado ]
        ↓ ... hasta ~20 minutos ...
[ token expira server-side ]
        ↓ próxima llamada → 'Token has expired' / 'Unauthorized'
[ o: re-instanciar el prox (durable) ]
[ o: fallar el resto del loop (lo que estás debuggeando) ]

El fingerprint de esta falla exacta: una corrida larga donde el primer batch de llamadas anduvo, la falla empieza en un corte limpio a los ~20 minutos, y todo después de ese punto falla con el mismo mensaje de error.

Las queries abajo confirman el fingerprint y te dicen qué arreglar.

Paso 1 — ¿La primera llamada anduvo?

Si la auth de WSProxy falló en la primera llamada, el bug no es el timeout de 20 minutos — son credenciales, scopes, o red. Confirmá que al menos una llamada SOAP anduvo bajo este RunId antes de leer cualquier otra cosa.

-- Reemplazá 'a1b2c3d4-...' con el RUN_ID de la corrida fallada
SELECT
  COUNT(*)                AS SuccessSteps,
  MIN(Ts)                 AS FirstSuccessAt,
  MAX(Ts)                 AS LastSuccessAt
FROM de_log_ssjs_runs
WHERE RunId = 'a1b2c3d4-e5f6-...'
  AND Step LIKE '%wsproxy%'
  AND Step NOT LIKE '%fail%';

Tres números, una pregunta:

  • SuccessSteps = 0: WSProxy nunca anduvo en esta corrida. Saltate el resto de esta página — tu problema está en la configuración de Installed Package / API Integration. Verificá que el script lea clientId / clientSecret del DE de config correcto, que los API scopes de la Installed Package incluyan Subscribers Read/Write (o la superficie que estés llamando), y que la Business Unit de la package matchee la BU donde corre el script.
  • SuccessSteps > 0 y LastSuccessAt - FirstSuccessAt menos de 20 minutos: el script no corrió lo suficiente para testear el timeout. La falla es otra cosa — probablemente un error de payload o el data de un registro específico. Usá Debugging de Script Activities trabadas — paso 3.
  • SuccessSteps > 0 y LastSuccessAt - FirstSuccessAt cerca o pasados los 20 minutos: el patrón de 20 minutos está en juego. Seguí al paso 2.

Paso 2 — ¿Cuándo arrancó a fallar la auth?

Encontrá la primera fila de auth-failure para el RunId y calculá el gap desde la primera llamada exitosa.

WITH
  first_success AS (
    SELECT MIN(Ts) AS Ts
    FROM de_log_ssjs_runs
    WHERE RunId = 'a1b2c3d4-e5f6-...'
      AND Step LIKE '%wsproxy%'
      AND Step NOT LIKE '%fail%'
  ),
  first_failure AS (
    SELECT MIN(Ts) AS Ts
    FROM de_log_ssjs_errors
    WHERE RunId = 'a1b2c3d4-e5f6-...'
      AND (Msg LIKE '%Token has expired%'
        OR Msg LIKE '%Unauthorized%'
        OR Msg LIKE '%401%')
  )
SELECT
  f.Ts                                 AS FirstFailureAt,
  s.Ts                                 AS FirstSuccessAt,
  DATEDIFF(minute, s.Ts, f.Ts)         AS GapMinutes
FROM first_success s, first_failure f;

Si GapMinutes es 19, 20, o 21, tenés el fingerprint del timeout de auth. El fix está en WSProxy — re-instanciá el prox cada 15 minutos adentro del loop largo.

Si GapMinutes es mucho más chico (menos de 5), no es un timeout. Causas posibles:

  • Un registro específico disparó un error de permisos que devuelve un mensaje con forma 401 (raro pero real para referencias cross-org).
  • La Installed Package fue rotada a mitad de corrida por un admin (muy raro, pero chequeá el audit trail).
  • El script switcheó de Business Unit vía setClientId y la BU nueva tiene credenciales distintas — ver paso 5.

Paso 3 — ¿Es el patrón de 20 minutos?

Confirmá mirando el timeline completo de filas success-vs-failure para el RunId. El patrón: todas las filas de success caen dentro de los primeros 20 minutos; todas las de failure caen después.

SELECT
  Ts,
  'SUCCESS' AS Outcome,
  Step
FROM de_log_ssjs_runs
WHERE RunId = 'a1b2c3d4-e5f6-...'
  AND Step LIKE '%wsproxy%'
  AND Step NOT LIKE '%fail%'

UNION ALL

SELECT
  Ts,
  'FAILURE' AS Outcome,
  Step
FROM de_log_ssjs_errors
WHERE RunId = 'a1b2c3d4-e5f6-...'
  AND (Msg LIKE '%Token has expired%'
    OR Msg LIKE '%Unauthorized%'
    OR Msg LIKE '%401%')

ORDER BY Ts;

El output es un timeline cronológico. La firma del timeout de auth se ve así:

02:00:14  SUCCESS  wsproxy-retrieve-subs
02:05:22  SUCCESS  wsproxy-retrieve-subs
02:11:38  SUCCESS  wsproxy-retrieve-subs
02:18:47  SUCCESS  wsproxy-retrieve-subs
02:21:03  FAILURE  wsproxy-retrieve-subs   ← corte limpio en el minuto 20
02:21:08  FAILURE  wsproxy-retrieve-subs
02:21:14  FAILURE  wsproxy-retrieve-subs
...

Una forma distinta — failures intercaladas con successes a lo largo del tiempo — significa que no es el timeout de auth; es algo per-registro (forma del data, permisos, outage parcial).

Paso 4 — ¿El script re-instancia?

Si se supone que el script refresque el prox cada 15 minutos siguiendo la disciplina del Style Guide, debería loggear un paso wsproxy-refresh en cada refresh. Verificá que las entradas de refresh se estén escribiendo realmente.

SELECT
  RunId,
  COUNT(*)                                   AS RefreshCount,
  MIN(Ts)                                    AS FirstRefreshAt,
  MAX(Ts)                                    AS LastRefreshAt,
  DATEDIFF(minute, MIN(Ts), MAX(Ts))         AS RefreshSpanMinutes
FROM de_log_ssjs_runs
WHERE RunId = 'a1b2c3d4-e5f6-...'
  AND Step = 'wsproxy-refresh'
GROUP BY RunId;

Tres formas de falla acá:

  • RefreshCount = 0: el script nunca refrescó. Confirma el bug — el prox fue instanciado una vez al tope y vivió la corrida entera. Fix según el patrón de re-instanciación de WSProxy.
  • RefreshCount > 0 pero RefreshSpanMinutes más corto que el runtime total del script: el script refrescó al principio pero paró después de algún punto. Causa común: la lógica de refresh está adentro de un try cuyo catch no continúa el loop — un error en el medio paró los refreshes posteriores.
  • RefreshCount > 0 distribuido parejo pero la auth igual falló: o el intervalo de refresh es demasiado largo (más de 15 min entre resets), o el script está llamando al prox desde un code path donde la variable que tiene la referencia del proxy está vieja (ej. capturada por un closure que arrancó antes del refresh).

Paso 5 — Escenarios cross-BU

Si el script usa prox.setClientId(clientId) para switchear de Business Unit a mitad de corrida, el patrón de auth es distinto. Las credenciales de cada BU se evalúan contra la configuración de Installed Package de esa BU, y una BU que no era parte del scope original de auth devuelve Unauthorized inmediatamente.

SELECT
  Step,
  Message,
  Ts
FROM de_log_ssjs_runs
WHERE RunId = 'a1b2c3d4-e5f6-...'
  AND Step LIKE '%setClientId%'
ORDER BY Ts;

Si ves llamadas setClientId seguidas rápidamente por errores Unauthorized:

  • Verificá que la Installed Package esté habilitada para esa BU destino (Setup → Installed Packages → tab Components → "Available in this Business Unit").
  • Verificá que los API scopes que el script requiere (Subscribers Read/Write, Data Extensions Read/Write, etc.) estén habilitados para la Installed Package de la BU destino, no solo para la BU padre.
  • Si la BU padre es MID 1000 y estás llamando setClientId(2000), el API user atrás de la Installed Package necesita acceso de Marketing Cloud Connect a la BU 2000 — no solo al padre.

Esto no es lo mismo que el timeout de 20 minutos. Un setClientId fallido falla en la primera llamada hacia la BU nueva; el timeout de 20 minutos falla después de 20 minutos de trabajo exitoso.

Paso 6 — Escribí el postmortem

Una vez encontrado el bug, escribí el diagnóstico a de_log_ssjs_postmortems (mismo DE usado por Debugging de Script Activities trabadas — paso 6).

INSERT INTO de_log_ssjs_postmortems
SELECT
  GETDATE()                                AS DiagnosedAt,
  'SA_NightlyEnrichment'                   AS ActivityName,
  'a1b2c3d4-e5f6-...'                      AS RunId,
  (SELECT MIN(Ts) FROM de_log_ssjs_runs WHERE RunId = 'a1b2c3d4-e5f6-...')                 AS StartedAt,
  (SELECT MAX(Ts) FROM de_log_ssjs_runs WHERE RunId = 'a1b2c3d4-e5f6-...')                 AS LastWriteAt,
  (SELECT TOP 1 Step FROM de_log_ssjs_runs WHERE RunId = 'a1b2c3d4-e5f6-...' ORDER BY Ts DESC) AS LastStep,
  (SELECT COUNT(*) FROM de_log_ssjs_errors
     WHERE RunId = 'a1b2c3d4-e5f6-...'
       AND (Msg LIKE '%Token has expired%' OR Msg LIKE '%Unauthorized%'))                  AS AuthFailureCount,
  'WSProxy 20-min token expiry; script did not refresh prox in loop'                       AS RootCause;

Un RootCause escrito en pasado fuerza claridad. "Expiración de token de 20 min" es una hipótesis; "el script no refrescó el prox en el loop" es el fix.

Causas comunes rankeadas por frecuencia

| Causa | Cómo detectarla | Fix en | |---|---|---| | Timeout de token de 20 min, sin refresh | El paso 3 muestra un corte limpio en el minuto 20 + el paso 4 devuelve RefreshCount = 0 | WSProxy; re-instanciar cada 15 min | | Lógica de refresh rota a mitad de corrida | RefreshCount > 0 del paso 4 pero RefreshSpanMinutes más corto que el runtime total | Style Guide; auditar el try/catch del refresh | | Auth falló en la llamada uno | SuccessSteps = 0 del paso 1 | Auditar Installed Package: BUs habilitadas, API scopes, MID matchea | | setClientId a una BU no autorizada | El paso 5 encuentra filas setClientId seguidas por Unauthorized | Marketing Cloud Setup → Installed Package → habilitar para BU destino | | Credenciales de Installed Package rotadas a mitad de corrida | GapMinutes del paso 2 es chico, audit trail muestra edición reciente | Coordinar con el equipo de admin; pinear rotación a ventanas de maintenance | | Referencia de proxy vieja capturada en un closure | El paso 4 tiene refreshes pero la auth igual falló | Auditar el script por closures que capturan prox antes de un refresh | | API scope mal configurado (ej. List Send falta) | El paso 1 muestra successes para un tipo de operación, failures para otro | Tab Components de la Installed Package; verificar todos los scopes necesarios | | Acceso cross-org de MID no concedido | setClientId a un MID fuera del acceso MCC del API user | Matriz de acceso de Marketing Cloud Connect; otorgar el MID al user |

Relacionado

  • WSProxy — la página de referencia con el patrón de re-instanciación al que apunta esta sesión de debug
  • Platform.Function — el wrapper de más alto nivel; a veces el fix es dejar WSProxy y usar Platform.Function.* en su lugar
  • Gotchas de MC SSJS — #8 (el gotcha del timeout de auth que esta página diagnostica)
  • Debugging de Script Activities trabadas — el how-to hermano que comparte el DE de postmortem
  • Style Guide — la disciplina de instrumentación (RunId + Step + log-en-catch + refresh de 15 minutos) que hace que estas queries sean posibles