fix: handle async 202 job polling for analyze#16
fix: handle async 202 job polling for analyze#16greynewell merged 1 commit intosupermodeltools:mainfrom
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThe API client was refactored to an asynchronous job flow: Changes
Sequence DiagramsequenceDiagram
participant Client
participant Analyze
participant postZip
participant Server
Client->>Analyze: Analyze(ctx, zip)
loop poll until done or error
Analyze->>postZip: postZip(ctx, zip)
postZip->>Server: POST /.../analyze (multipart ZIP)
Server-->>postZip: JobResponse(status, jobId, retryAfter, error?, result?)
postZip-->>Analyze: return JobResponse
alt status == "completed"
Analyze->>Analyze: unmarshal JobResponse.Result -> jobResult.Graph
Analyze-->>Client: return *Graph
else status == "pending" or "processing"
Analyze->>Analyze: wait retryAfter (fallback 5s) or ctx.Done()
else error set or unexpected status
Analyze-->>Client: return error
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/api/client.go`:
- Around line 72-76: Check for null/empty payloads before attempting to
unmarshal the job result: in the code that uses json.Unmarshal into jobResult
(the jobResult type and the result variable), first verify job.Result is not nil
and not an empty/whitespace/"null" payload (e.g., len==0 or trimmed equals
"null") and return a descriptive error if it is; after unmarshalling, also
validate that result.Graph is present/non-empty and return an error if the graph
is missing so a completed job with result:null cannot silently succeed.
- Around line 47-63: The polling loop in internal/api/client.go that checks
job.Status and calls c.postZip can hang indefinitely; add a hard stop by
introducing a max poll guard (e.g., maxPollDuration or maxPollAttempts) measured
from a start time (time.Now()) or a counter, and after the limit is exceeded
return a clear error (or context.DeadlineExceeded) instead of looping forever;
keep existing handling of job.RetryAfter and the ctx.Done() case, but before
each sleep/iteration check the elapsed time or attempts and bail out with an
error referencing the job/idempotency context so callers can handle it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 67f8d546-e468-4ded-8491-b68a54874a37
📒 Files selected for processing (2)
internal/api/client.gointernal/api/types.go
| // Poll until the job completes. | ||
| for job.Status == "pending" || job.Status == "processing" { | ||
| wait := time.Duration(job.RetryAfter) * time.Second | ||
| if wait <= 0 { | ||
| wait = 5 * time.Second | ||
| } | ||
| select { | ||
| case <-ctx.Done(): | ||
| return nil, ctx.Err() | ||
| case <-time.After(wait): | ||
| } | ||
|
|
||
| job, err = c.postZip(ctx, zipPath, idempotencyKey) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } |
There was a problem hiding this comment.
Add a hard stop for polling to avoid indefinite hangs.
Right now, Line 48 can loop forever if the API never reaches a terminal state. At call sites (internal/analyze/handler.go Line 57, internal/find/handler.go Line 161), ctx is passed through without a timeout wrapper, so this can block indefinitely.
Suggested guardrail
func (c *Client) Analyze(ctx context.Context, zipPath, idempotencyKey string) (*Graph, error) {
+ if _, hasDeadline := ctx.Deadline(); !hasDeadline {
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, defaultTimeout)
+ defer cancel()
+ }
+
job, err := c.postZip(ctx, zipPath, idempotencyKey)
if err != nil {
return nil, err
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/api/client.go` around lines 47 - 63, The polling loop in
internal/api/client.go that checks job.Status and calls c.postZip can hang
indefinitely; add a hard stop by introducing a max poll guard (e.g.,
maxPollDuration or maxPollAttempts) measured from a start time (time.Now()) or a
counter, and after the limit is exceeded return a clear error (or
context.DeadlineExceeded) instead of looping forever; keep existing handling of
job.RetryAfter and the ctx.Done() case, but before each sleep/iteration check
the elapsed time or attempts and bail out with an error referencing the
job/idempotency context so callers can handle it.
| var result jobResult | ||
| if err := json.Unmarshal(job.Result, &result); err != nil { | ||
| return nil, fmt.Errorf("decode graph result: %w", err) | ||
| } | ||
| return &result.Graph, nil |
There was a problem hiding this comment.
Guard against completed responses with null/empty result payload.
A completed job with result: null would currently decode to an empty graph and look like success. Fail fast here so bad server payloads don’t silently pass.
Suggested validation
var result jobResult
+ if trimmed := bytes.TrimSpace(job.Result); len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
+ return nil, fmt.Errorf("analysis completed without graph result")
+ }
if err := json.Unmarshal(job.Result, &result); err != nil {
return nil, fmt.Errorf("decode graph result: %w", err)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/api/client.go` around lines 72 - 76, Check for null/empty payloads
before attempting to unmarshal the job result: in the code that uses
json.Unmarshal into jobResult (the jobResult type and the result variable),
first verify job.Result is not nil and not an empty/whitespace/"null" payload
(e.g., len==0 or trimmed equals "null") and return a descriptive error if it is;
after unmarshalling, also validate that result.Graph is present/non-empty and
return an error if the graph is missing so a completed job with result:null
cannot silently succeed.
cb38984 to
1a81429
Compare
The /v1/graphs/supermodel endpoint returns HTTP 202 with an async job envelope (status, jobId, retryAfter, result). The client was treating this as a completed response and unmarshalling directly into Graph, resulting in 0 nodes/relationships. Changes: - Add JobResponse and jobResult types for the async envelope - Poll with the same idempotency key until status is "completed" - Extract graph from the nested result.graph path - Extract postZip helper to avoid duplicating multipart construction Tested live against api.supermodeltools.com — correctly returns 362 files, 1527 functions, 7741 relationships for supermodel-public-api.
1a81429 to
ae6a423
Compare
Summary
{"status":"pending","jobId":"...","retryAfter":10,"result":null}) but the client was deserializing it directly intoGraph, producing 0 nodes/relationshipsstatus: "completed"result.graphpathChanges
internal/api/types.go— addJobResponseandjobResulttypesinternal/api/client.go— rewriteAnalyze()with poll loop, extractpostZiphelperTest results
Note
Depends on #15 for the correct endpoint path (
/v1/graphs/supermodel). This PR keeps the endpoint as-is to avoid conflicts — once #15 merges, theanalyzeEndpointconst just needs updating.Test plan
supermodel analyzeon a real repo returns non-zero node/relationship counts--forcebypasses cache and re-runs analysisSummary by CodeRabbit