Skip to content

GetSessionDecision in internal/didit/client.go runs up to 30 seconds of synchronous polling inline on the HTTP request goroutine in kyc.go #227

Description

@Jagadeeshftw

📌 Description

internal/didit/client.go's Client is configured with PollInterval: 2 * time.Second and MaxPolls: 15 by default, and GetSessionDecision blocks until either a terminal status is reached or those polls are exhausted:

func (c *Client) GetSessionDecision(ctx context.Context, sessionID string) (SessionDecisionResponse, error) {
    maxPolls := c.MaxPolls
    if maxPolls <= 0 {
        maxPolls = 1
    }
    for attempt := 0; attempt < maxPolls; attempt++ {
        ...
        if attempt == maxPolls-1 {
            ...
        }
        ...
    }
    ...
}

— i.e. up to 15 × 2s = 30 seconds of blocking, synchronous work per call. This is invoked directly, inline, from two HTTP handlers in internal/handlers/kyc.go:

decision, err := h.didit.GetSessionDecision(c.Context(), *existingSessionID)   // kyc.go:182, Start()
...
decision, err := h.didit.GetSessionDecision(c.Context(), *kycSessionID)        // kyc.go:393, Status()

Both Start and Status are meant to be responsive, user-facing endpoints (a status-check endpoint in particular is typically expected to be fast, often polled by a frontend). Here, a single request to either endpoint can tie up a request-handling goroutine (and, under fasthttp/Fiber's model, hold onto whatever resources are attached to that request context) for up to 30 seconds waiting on an external service's polling loop, rather than returning the current known status immediately and letting the frontend poll (or relying on Didit's webhook, which internal/handlers/didit_webhook.go already receives and processes for exactly this purpose). Under any reasonable request volume to these endpoints, this risks exhausting available request-handling capacity and hitting client-side or load-balancer timeouts well before the 30-second internal poll completes.

🧩 Requirements and context

  • kyc.go's Start/Status handlers should not block the HTTP response on a multi-second-to-30-second internal polling loop; the endpoint should return promptly with the best currently-known status (from the database and/or a single, non-looping check against Didit) and let the client re-poll the endpoint or rely on the webhook-driven update path (internal/handlers/didit_webhook.go) for the eventual terminal status.
  • If a single up-to-date check against Didit's decision API is still desired synchronously (as opposed to zero external calls), it should be a single request/attempt, not the full multi-attempt polling loop meant for a different use case (e.g. a background job or CLI tool that legitimately wants to wait for a terminal status).
  • Preserve GetSessionDecision's existing multi-poll behavior for any caller that legitimately wants to wait for a terminal result (e.g. if there's a background/worker use case for it) — this is about not calling the waiting variant from a user-facing HTTP request path, not about removing the polling capability from the client altogether.

🛠️ Suggested execution

  • Add a single-attempt variant to internal/didit/client.go (e.g. reuse the already-existing getSessionDecisionOnce unexported method, or export a thin wrapper around it) that performs exactly one check against Didit with no internal sleep/retry loop.
  • Update kyc.go's Start and Status handlers to call the single-attempt variant instead of GetSessionDecision, returning whatever status comes back (including a "still pending" status) immediately rather than blocking for up to 30 seconds waiting for a terminal one.
  • Add a test in internal/handlers/kyc_test.go (extend if it exists) using a fake Didit client that would take 30 seconds to reach a terminal status via the multi-poll path, asserting the handler now returns in roughly one call's worth of latency instead of blocking for the full polling duration.

✅ Acceptance criteria

  • kyc.go's Start/Status handlers no longer block for up to 30 seconds waiting on Didit's decision polling loop.
  • The handlers still return the best currently-available KYC status promptly (pending/approved/declined/expired as applicable), relying on the existing webhook path for eventual terminal-status updates.
  • GetSessionDecision's existing multi-poll behavior remains available/unchanged for any legitimate waiting use case elsewhere.
  • A test demonstrates the handler's latency is no longer tied to the full polling duration.

🔒 Security notes

No new attack surface; this is a reliability/availability fix. Tying up request-handling capacity for up to 30 seconds per call on a user-facing endpoint is a self-inflicted resource-exhaustion risk — a modest burst of legitimate KYC status checks (e.g. a frontend polling this endpoint while a user waits) could saturate available request handlers well before any of the underlying Didit polling completes, degrading or blocking unrelated requests to this service.

📋 Guidelines

  • Minimum 95% test coverage
  • Clear documentation
  • Timeframe: 96 hours

Metadata

Metadata

Assignees

Labels

GrantFox OSSGrantFox open-source programMaybe RewardedGrantFox: potentially rewarded contributionOfficial Campaign | FWC26GrantFox official campaign issuebackendBackend / API workbugSomething isn't workingperformancePerformance / gas / resource use

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions