Token Endpoint Response Listener#

TokenResponse carries only the parsed accessToken, refreshToken, and error fields. When you need access to raw HTTP headers (for example, the dpop-nonce header for audit logging) or the unparsed response body, register a TokenEndpointResponseListener in the HaapiConfiguration. The listener fires on every call to OAuthTokenManager.fetchAccessToken, refreshAccessToken, and revokeRefreshToken.

When to Use a Listener#

Most apps do not need a TokenEndpointResponseListener. The parsed TokenResponse carries the access token, refresh token, expiry, and OAuth-protocol errors — enough for the authentication flow itself. Reach for the listener when:

  • You need raw HTTP headers — most commonly dpop-nonce for audit logging, x-request-id for correlation, or a custom header that your deployment adds.
  • You need the unparsed JSON body — to forward it to a separate audit pipeline, or because your backend returns extension fields the SDK does not deserialize.
  • You need transport-level metadata — content type, status code, server timing — that the parsed model does not expose.
  • You want to react to all token-endpoint calls uniformly — fetch, refresh, and revoke — for example, to invalidate a cached resource that depends on the current access token, or to emit a metric per call.

If none of these apply, skip the listener and work with TokenResponse directly. The API is leaner, and you avoid the double-handling pitfall described below.

Implementing a Listener#

On React Native: the listener implementation lives in native code, not JavaScript. The React Native config carries only a flag (hasTokenEndpointResponseListener: true on iOS / tokenEndpointResponseListenerOption: TokenEndpointResponseListenerOption.Custom on Android) that activates a listener registered on the native side. There is no JS event emitter or callback — pure-JS RN apps cannot subscribe to token-endpoint responses. Use this only when you are willing to write Swift / Kotlin in the host app’s native modules. The listener registration is part of the same Native Registry mechanism described in Native Resolvers .

class MyTokenEndpointResponseListener: OAuthTokenManager.TokenEndpointResponseListener {
    func onSuccess(_ value: OAuthTokenManager.SuccessTokenHTTPURLResponseContent) {
        // value.httpURLResponse — raw HTTPURLResponse
        // value.headerFields["dpop-nonce"] — DPoP nonce header
        // value.dataAsDictionary["access_token"] — raw JSON access_token
    }

    func onError(_ value: ErrorHTTPURLResponseContent) {
        // raw error response
    }

    func onTokenError(_ value: OAuthTokenManager.ErrorTokenHTTPURLResponseContent) {
        // parsed ErrorTokenResponse with raw access too
    }
}

let haapiConfiguration = HaapiConfiguration(
    // ...other configuration
    tokenEndpointResponseListener: MyTokenEndpointResponseListener()
)

Three callbacks fire on both platforms — successful token response, OAuth-protocol error response, and transport-level failure. Each callback receives the parsed model plus accessors for the raw HTTP response headers, content type, and unparsed body.

Avoiding Double-Handling#

When a listener is configured and an exception is raised, the framework delivers the failure to both the listener and the calling method. The same error reaches your code twice — once through the callback, once through the throw or completion handler — which can produce duplicate UX (two error dialogs) or duplicate metric emissions.

Avoid double-handling by treating the listener as raw-logging-only (audit, telemetry, header capture) and letting the caller handle the user-facing flow. Or, if the listener must take action on errors, mark handled errors via a shared flag the caller checks.

A defensive pattern — a thread-safe flag the listener flips on every callback, which the caller checks afterward:

final class HandledFlag {
    private var handled = false
    private let lock = NSLock()

    func reset() {
        lock.lock(); defer { lock.unlock() }
        handled = false
    }

    func set() {
        lock.lock(); defer { lock.unlock() }
        handled = true
    }

    var isHandled: Bool {
        lock.lock(); defer { lock.unlock() }
        return handled
    }
}

let listenerHandled = HandledFlag()

class HandlingListener: OAuthTokenManager.TokenEndpointResponseListener {
    let flag: HandledFlag
    init(flag: HandledFlag) { self.flag = flag }

    func onSuccess(_ value: OAuthTokenManager.SuccessTokenHTTPURLResponseContent) {
        // audit + handle
        flag.set()
    }
    // onError / onTokenError set the flag the same way
}

// Before each request, reset:
listenerHandled.reset()

oAuthTokenManager.refreshAccessToken(with: refreshToken) { [weak self] tokenResponse in
    if listenerHandled.isHandled {
        // Listener already handled — avoid duplicate UX
    } else {
        // Caller-side handling
    }
}

Was this helpful?