📌 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
🔒 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
📌 Description
internal/didit/client.go'sClientis configured withPollInterval: 2 * time.SecondandMaxPolls: 15by default, andGetSessionDecisionblocks until either a terminal status is reached or those polls are exhausted:— 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:Both
StartandStatusare 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, whichinternal/handlers/didit_webhook.goalready 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'sStart/Statushandlers 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.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
internal/didit/client.go(e.g. reuse the already-existinggetSessionDecisionOnceunexported method, or export a thin wrapper around it) that performs exactly one check against Didit with no internal sleep/retry loop.kyc.go'sStartandStatushandlers to call the single-attempt variant instead ofGetSessionDecision, returning whatever status comes back (including a "still pending" status) immediately rather than blocking for up to 30 seconds waiting for a terminal one.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'sStart/Statushandlers no longer block for up to 30 seconds waiting on Didit's decision polling loop.GetSessionDecision's existing multi-poll behavior remains available/unchanged for any legitimate waiting use case elsewhere.🔒 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