Skip to content

Debugging

@moose/provider-sdk ships a handful of opt-in debugging aids — none of them run unless you wire them up, and none add a runtime dependency. This page covers three of them; see the @moose/provider-sdk reference for exact signatures, and Testing with createMockPlatform for the fourth (running your integration with no network at all).

Diagnosing an unexplained 401

A 401 almost always means the platform recomputed a different signature than the one you sent — see Signing & authentication for the exact contract. explainSignature computes exactly what signRequest computes, but also returns the raw canonical string it hashed, so you can diff it against what the platform's own logs show it computed for the same request:

ts
import { explainSignature } from '@moose/provider-sdk'

const explanation = explainSignature({ method: 'POST', path: '/v1/wallet/balance', tenantId, secret, body, now: new Date(), nonce })
console.log(explanation.canonicalString) // "POST\n/v1/wallet/balance\n<ts>\n<nonce>\n<body>"
console.log(explanation.signature)

A mismatched canonicalString usually means the body sent over the wire isn't byte-for-byte what was signed (e.g. re-serialized JSON with different key order or whitespace), or path carries a query string it shouldn't. A matching canonicalString but a different signature means the secret itself is wrong. PlatformApiError.hint (see below) gives the same pointer automatically the moment a real 401 comes back — you don't need explainSignature for every failure, only when the hint alone isn't enough to spot the cause.

Watching requests and retries

ProviderClient retries network errors, 409s, and 5xxs silently by default — that's the point of the SDK, but it means a slow or flaky integration can look like nothing is happening. onDebug receives a DebugEvent for every outbound request, response, and retry:

ts
const client = new ProviderClient({
  baseUrl, tenantId, secret,
  onDebug: (event) => {
    if (event.phase === 'retry') {
      console.warn(`retrying ${event.path} (attempt ${event.attempt}): ${event.reason}`)
    } else {
      console.debug(event)
    }
  },
})

A response event's clockSkewMs (the client's own clock minus the platform's Date response header) is worth watching if you're seeing intermittent 401s with no other obvious cause — a skewed server or container clock is a common, otherwise-invisible culprit, since the platform rejects a signed timestamp too far from its own clock.

Reading a thrown error

PlatformApiError carries three things beyond status/body that exist specifically to shorten a debugging loop:

ts
try {
  await client.submitTransaction(req)
} catch (err) {
  if (err instanceof PlatformApiError) {
    console.error(err.status, err.hint)
    if (err.requestId) console.error('hand this to platform support:', err.requestId)
    if (err.retryAfter) console.error('platform asked us to wait:', err.retryAfter)
  }
}

requestId mirrors the platform's X-Request-Id response header — the single most useful thing to hand platform support, since it lets them find the exact request in server-side logs without you needing to describe timing or payload. hint is a one-line pointer at the likely cause for that specific status (not exhaustive — a starting point). See Errors & retry for the full status code reference.

Running without the full platform stack

Every integration test today needs the full Postgres/Redis/platform/operator stack running — unless you use createMockPlatform, a signable, in-memory stand-in for the platform's wallet/RGS endpoints. See Testing with createMockPlatform for a full walkthrough.