Skip to content

Phase 3: SDK enhancements#27

Open
alxgsv wants to merge 2 commits intomodernize/api-v07-phase2from
codex/phase3-sdk-enhancements
Open

Phase 3: SDK enhancements#27
alxgsv wants to merge 2 commits intomodernize/api-v07-phase2from
codex/phase3-sdk-enhancements

Conversation

@alxgsv
Copy link
Copy Markdown
Contributor

@alxgsv alxgsv commented Mar 14, 2026

  • add configurable throttling retry behavior via ucare.Config.Retry
  • export structured API error types with status details for CLI inspection
  • add upload.Service.Upload() with automatic direct-vs-multipart selection and changelog updates

Summary by CodeRabbit

Release Notes

  • New Features

    • Added opt-in throttling retries for API requests; retries are disabled by default and must be enabled via configuration.
    • Introduced unified upload method that automatically selects the optimal upload strategy based on file size.
  • Bug Fixes

    • Improved retry behavior with Retry-After header support and exponential backoff capping.
    • Enhanced error reporting with specific, structured error types for authentication, validation, throttling, and forbidden errors.
  • Tests

    • Added comprehensive test coverage for throttling retries, upload flow selection, and error handling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 14, 2026

Walkthrough

The PR adds opt-in throttling retries controlled by a new RetryConfig field on ucare.Config. It introduces five structured error types (APIError, AuthError, ThrottleError, ValidationError, ForbiddenError) to replace generic error handling. A new upload.Service.Upload() method automatically selects between direct and multipart uploads. REST and upload API clients now implement retry logic with Retry-After header support and exponential backoff capping.

Changes

Cohort / File(s) Summary
Error Type Infrastructure
ucare/error.go, ucare/error_test.go
Introduces five new structured error types: APIError (base), AuthError, ThrottleError, ValidationError, ForbiddenError. Each implements custom Error() formatting. Replaces previous respErr-based error handling with dedicated types exposing StatusCode and Detail fields.
Retry Configuration & Utilities
ucare/retry.go, ucare/retry_test.go
Adds RetryConfig struct with MaxRetries and MaxWaitSeconds fields. Introduces expBackoff(attempt int) helper computing exponential backoff (1, 2, 4, 8... capped at 30 seconds). Tests validate backoff progression.
Configuration Updates
ucare/client.go, internal/config/config.go
Adds public Retry field (*RetryConfig) to ucare.Config. Removes public MaxThrottleRetries constant from internal/config package.
REST API Client
ucare/restapi.go, ucare/restapi_test.go
Enhances error handling: 400/404→APIError, 401→AuthError, 403→ForbiddenError, 429→ThrottleError with retry logic. Implements Retry-After validation, exponential backoff when missing, and max retry/wait enforcement. Tests verify throttle behavior, retry counts, and edge cases.
Upload API Client
ucare/uploadapi.go, ucare/uploadapi_test.go
Adds retry loop with centralized handleResponse method. Implements status-based error mapping (400→ValidationError, 403→ForbiddenError, 413→ErrFileTooLarge, 429→throttle retries). Rebuilds request bodies on retry. Tests validate no-retry-by-default, successful retries, and error parsing.
Upload Service Enhancement
upload/service.go, upload/upload.go, upload/upload_test.go
Adds UploadParams struct and Upload() method to Service. Implements automatic upload method selection: direct uploads for files ≤DefaultMultipartThreshold (10MB), multipart for larger files. Includes internal uploadDirect/uploadMultipart helpers with context cancellation support. Tests cover small/large file paths, custom thresholds, and forced method selection.
Manifest
CHANGELOG.md
Documents feature additions: opt-in throttling retries, structured error types, unified upload flow, retry behavior enhancements.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant REST API
    participant Response Handler
    participant Retry Logic

    Client->>REST API: HTTP Request (attempt 1)
    REST API-->>Response Handler: HTTP 429 Response<br/>(Throttled)
    Response Handler->>Retry Logic: Check RetryConfig<br/>(MaxRetries, MaxWaitSeconds)
    alt Retries Enabled
        Retry Logic->>Retry Logic: Parse Retry-After<br/>or compute expBackoff()
        Retry Logic->>Retry Logic: Wait with backoff<br/>(respecting cap)
        Retry Logic->>REST API: HTTP Request (attempt 2)
        REST API-->>Response Handler: HTTP 200 OK
        Response Handler->>Client: Return data
    else Retries Disabled
        Response Handler->>Client: Return ThrottleError
    end
Loading
sequenceDiagram
    participant Client
    participant Upload Service
    participant Direct Path
    participant Multipart Path

    Client->>Upload Service: Upload(ctx, UploadParams)
    Upload Service->>Upload Service: Check file size vs.<br/>MultipartThreshold
    alt Size ≤ Threshold
        Upload Service->>Direct Path: uploadDirect()
        Direct Path->>Direct Path: File upload
        Direct Path->>Direct Path: Fetch FileInfo
        Direct Path-->>Upload Service: FileInfo
    else Size > Threshold
        Upload Service->>Multipart Path: uploadMultipart()
        Multipart Path->>Multipart Path: Start session
        Multipart Path->>Multipart Path: Upload chunks
        Multipart Path->>Multipart Path: Complete & wait
        Multipart Path-->>Upload Service: FileInfo
    end
    Upload Service-->>Client: FileInfo or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Phase 3: SDK enhancements' is vague and generic, using non-descriptive terms that don't convey specific information about the changeset. Consider a more specific title like 'Add throttling retry config, error types, and upload service selection' to clearly communicate the main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/phase3-sdk-enhancements
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use TruffleHog to scan for secrets in your code with verification capabilities.

Add a TruffleHog config file (e.g. trufflehog-config.yml, trufflehog.yml) to your project to customize detectors and scanning behavior. The tool runs only when a config file is present.

@alxgsv
Copy link
Copy Markdown
Contributor Author

alxgsv commented Mar 14, 2026

@coderabbitai please review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 14, 2026

@alxgsv: Sure, I'll review the changes in this PR now!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
ucare/error.go (1)

40-53: ThrottleError doesn't embed APIError, breaking consistency with other error types.

Unlike AuthError, ValidationError, and ForbiddenError, ThrottleError doesn't embed APIError. This means errors.As(err, &APIError{}) won't match throttle errors, which could surprise callers expecting all HTTP errors to be catchable via APIError.

If this is intentional (e.g., throttle errors are transient and handled differently), consider documenting this distinction. Otherwise, embedding APIError would provide consistency:

♻️ Optional: Embed APIError for consistency
 // ThrottleError represents a throttled request (HTTP 429).
 type ThrottleError struct {
+	APIError
 	RetryAfter int
 }

 func (e ThrottleError) Error() string {
 	if e.RetryAfter == 0 {
 		return "uploadcare: request throttled"
 	}
 	return fmt.Sprintf(
 		"uploadcare: request throttled, retry after %d seconds",
 		e.RetryAfter,
 	)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/error.go` around lines 40 - 53, ThrottleError currently does not embed
APIError which prevents errors.As(err, &APIError{}) from matching it; update the
ThrottleError definition to embed APIError (e.g., include APIError as an
anonymous field in the struct) and keep the RetryAfter int field, then adjust
construction/initialization sites so the embedded APIError fields
(status/message/etc.) are populated; you can keep or adapt the Error() method to
include RetryAfter but ensure the embedded APIError remains present so errors.As
recognizes ThrottleError like AuthError/ValidationError/ForbiddenError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ucare/restapi.go`:
- Around line 168-175: The retry logic in the function incorrectly applies the
MaxWaitSeconds ceiling only when the Retry-After header is present and above
zero, allowing the exponential backoff fallback to exceed MaxWaitSeconds. To fix
this in the retry code block, ensure that after computing wait using
expBackoff(tries) when retryAfter is zero or absent, you also apply the
MaxWaitSeconds ceiling to the wait time before proceeding. This change should be
made where the retry wait time is determined, specifically in the conditional
branches handling retryAfter and expBackoff.

In `@ucare/retry.go`:
- Around line 17-19: The expBackoff function currently does an unguarded shift
(1 << (attempt - 1)); add a defensive check to handle non-positive attempt
values by treating attempt <= 0 as 1 (or returning the minimum backoff) before
performing the shift so you never perform a negative shift count; update
expBackoff to normalize attempt when <= 0, then compute wait = 1 << (attempt -
1) and return min(wait, 30) (referencing the expBackoff function and the min
helper).

In `@ucare/uploadapi.go`:
- Around line 158-159: The json decoding call is passing the address of the
caller-provided destination wrapper (using &resdata) which causes the decoder to
overwrite the local interface{} pointer instead of populating the caller's
value; in the upload response handler replace
json.NewDecoder(resp.Body).Decode(&resdata) with
json.NewDecoder(resp.Body).Decode(resdata) so the decoder writes into the
caller's provided destination (resdata) rather than the local wrapper.

In `@upload/upload_test.go`:
- Around line 142-184: The test TestUpload_LargeFile_MultipartUpload declares
and increments completeHit in the HTTP handler for the "/multipart/complete/"
case but never asserts it; add an assertion after the upload to verify
completeHit.Load() equals 1 (use the existing assert package, e.g.
assert.Equal(t, int32(1), completeHit.Load())) so the test confirms the complete
endpoint was invoked.

---

Nitpick comments:
In `@ucare/error.go`:
- Around line 40-53: ThrottleError currently does not embed APIError which
prevents errors.As(err, &APIError{}) from matching it; update the ThrottleError
definition to embed APIError (e.g., include APIError as an anonymous field in
the struct) and keep the RetryAfter int field, then adjust
construction/initialization sites so the embedded APIError fields
(status/message/etc.) are populated; you can keep or adapt the Error() method to
include RetryAfter but ensure the embedded APIError remains present so errors.As
recognizes ThrottleError like AuthError/ValidationError/ForbiddenError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d1228c9c-82c9-4a21-b611-1048c5bf7165

📥 Commits

Reviewing files that changed from the base of the PR and between e1f45e8 and e42cb61.

📒 Files selected for processing (14)
  • CHANGELOG.md
  • internal/config/config.go
  • ucare/client.go
  • ucare/error.go
  • ucare/error_test.go
  • ucare/restapi.go
  • ucare/restapi_test.go
  • ucare/retry.go
  • ucare/retry_test.go
  • ucare/uploadapi.go
  • ucare/uploadapi_test.go
  • upload/service.go
  • upload/upload.go
  • upload/upload_test.go
💤 Files with no reviewable changes (1)
  • internal/config/config.go

Comment on lines +168 to 175
if c.retry.MaxWaitSeconds > 0 &&
retryAfter > c.retry.MaxWaitSeconds {
return false, ThrottleError{RetryAfter: retryAfter}
}
wait := retryAfter
if wait <= 0 {
wait = expBackoff(tries)
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't bypass MaxWaitSeconds on the fallback backoff path.

When Retry-After is absent or 0, this branch falls back to expBackoff(tries) after the only ceiling check has already run. That means a client configured with MaxWaitSeconds: 1 can still sleep for 2s, 4s, etc. on REST retries, unlike the upload client.

Suggested fix
-		if c.retry.MaxWaitSeconds > 0 &&
-			retryAfter > c.retry.MaxWaitSeconds {
-			return false, ThrottleError{RetryAfter: retryAfter}
-		}
 		wait := retryAfter
 		if wait <= 0 {
 			wait = expBackoff(tries)
 		}
+		if c.retry.MaxWaitSeconds > 0 && wait > c.retry.MaxWaitSeconds {
+			if retryAfter > 0 {
+				return false, ThrottleError{RetryAfter: retryAfter}
+			}
+			wait = c.retry.MaxWaitSeconds
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if c.retry.MaxWaitSeconds > 0 &&
retryAfter > c.retry.MaxWaitSeconds {
return false, ThrottleError{RetryAfter: retryAfter}
}
wait := retryAfter
if wait <= 0 {
wait = expBackoff(tries)
}
wait := retryAfter
if wait <= 0 {
wait = expBackoff(tries)
}
if c.retry.MaxWaitSeconds > 0 && wait > c.retry.MaxWaitSeconds {
if retryAfter > 0 {
return false, ThrottleError{RetryAfter: retryAfter}
}
wait = c.retry.MaxWaitSeconds
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/restapi.go` around lines 168 - 175, The retry logic in the function
incorrectly applies the MaxWaitSeconds ceiling only when the Retry-After header
is present and above zero, allowing the exponential backoff fallback to exceed
MaxWaitSeconds. To fix this in the retry code block, ensure that after computing
wait using expBackoff(tries) when retryAfter is zero or absent, you also apply
the MaxWaitSeconds ceiling to the wait time before proceeding. This change
should be made where the retry wait time is determined, specifically in the
conditional branches handling retryAfter and expBackoff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai clarified the RetryConfig.MaxWaitSeconds docs to make the intended split explicit: for REST, it only triggers fail-fast when the server sends a positive Retry-After above the cap; when Retry-After is absent/invalid, REST still uses uncapped fallback exponential backoff. Upload behavior remains capped because that API does not return Retry-After.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alxgsv, thanks for the clarification! The asymmetric behavior is intentional and makes sense — REST responses can carry a server-authoritative Retry-After that signals how long to wait, so exceeding MaxWaitSeconds on that path is a meaningful fail-fast signal. Without a Retry-After, the client-side exponential backoff is just a best-effort estimate and capping it isn't required. Upload, having no Retry-After, always caps the backoff via MaxWaitSeconds. The code is correct as designed; I'll withdraw my concern.


✏️ Learnings added
Learnt from: alxgsv
Repo: uploadcare/uploadcare-go PR: 27
File: ucare/restapi.go:168-177
Timestamp: 2026-03-14T08:20:30.304Z
Learning: In `ucare/restapi.go` and `ucare/uploadapi.go` (uploadcare-go, Go), `RetryConfig.MaxWaitSeconds` has intentionally asymmetric semantics between the REST and Upload API clients:
- **REST client**: `MaxWaitSeconds` only triggers fail-fast (returns `ThrottleError`) when the server sends a positive `Retry-After` header that exceeds the cap. When `Retry-After` is absent or invalid, the REST client falls back to uncapped exponential backoff (`expBackoff(tries)`) and `MaxWaitSeconds` does NOT apply.
- **Upload client**: `MaxWaitSeconds` always caps the backoff wait, because the upload API does not return a `Retry-After` header.
This split is intentional by design.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +17 to +19
func expBackoff(attempt int) int {
wait := 1 << (attempt - 1)
return min(wait, 30)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider guarding against non-positive attempt values.

If attempt is 0 or negative, 1 << (attempt - 1) produces undefined behavior (negative shift count). While this is an unexported function and callers likely pass 1-indexed attempts, adding a guard would prevent subtle bugs:

🛡️ Proposed defensive fix
 func expBackoff(attempt int) int {
+	if attempt < 1 {
+		return 1
+	}
 	wait := 1 << (attempt - 1)
 	return min(wait, 30)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func expBackoff(attempt int) int {
wait := 1 << (attempt - 1)
return min(wait, 30)
func expBackoff(attempt int) int {
if attempt < 1 {
return 1
}
wait := 1 << (attempt - 1)
return min(wait, 30)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/retry.go` around lines 17 - 19, The expBackoff function currently does
an unguarded shift (1 << (attempt - 1)); add a defensive check to handle
non-positive attempt values by treating attempt <= 0 as 1 (or returning the
minimum backoff) before performing the shift so you never perform a negative
shift count; update expBackoff to normalize attempt when <= 0, then compute wait
= 1 << (attempt - 1) and return min(wait, 30) (referencing the expBackoff
function and the min helper).

Comment on lines +158 to +159
if err := json.NewDecoder(resp.Body).Decode(&resdata); err != nil {
return false, err
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare the decode target in the upload and REST clients.
rg -n 'Decode\(&resdata\)|Decode\(resdata\)' ucare/uploadapi.go ucare/restapi.go

Repository: uploadcare/uploadcare-go

Length of output: 248


Decode into resdata, not &resdata.

resdata already holds the caller's destination. Taking its address only lets the decoder overwrite the local interface{} wrapper, so successful upload responses can return nil while leaving the caller's result unchanged.

Suggested fix
-	if err := json.NewDecoder(resp.Body).Decode(&resdata); err != nil {
+	if err := json.NewDecoder(resp.Body).Decode(resdata); err != nil {
		return false, err
	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err := json.NewDecoder(resp.Body).Decode(&resdata); err != nil {
return false, err
if err := json.NewDecoder(resp.Body).Decode(resdata); err != nil {
return false, err
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/uploadapi.go` around lines 158 - 159, The json decoding call is passing
the address of the caller-provided destination wrapper (using &resdata) which
causes the decoder to overwrite the local interface{} pointer instead of
populating the caller's value; in the upload response handler replace
json.NewDecoder(resp.Body).Decode(&resdata) with
json.NewDecoder(resp.Body).Decode(resdata) so the decoder writes into the
caller's provided destination (resdata) rather than the local wrapper.

Comment on lines +142 to +184
func TestUpload_LargeFile_MultipartUpload(t *testing.T) {
t.Parallel()

var directHit, multipartHit, completeHit atomic.Int32
fileID := "test-uuid-456"

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/base/":
directHit.Add(1)
json.NewEncoder(w).Encode(map[string]string{"file": fileID})
case "/multipart/start/":
multipartHit.Add(1)
json.NewEncoder(w).Encode(map[string]interface{}{
"uuid": fileID,
"parts": []string{},
})
case "/multipart/complete/":
completeHit.Add(1)
json.NewEncoder(w).Encode(FileInfo{FileName: "large.bin"})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer srv.Close()

client := &mockUploadClient{baseURL: srv.URL, conn: srv.Client()}
svc := NewService(client)

data := bytes.NewReader(make([]byte, 20*1024*1024)) // 20MB
info, err := svc.Upload(context.Background(), UploadParams{
Data: data,
Name: "large.bin",
ContentType: "application/octet-stream",
Size: 20 * 1024 * 1024,
})

assert.NoError(t, err)
assert.Equal(t, "large.bin", info.FileName)
assert.Equal(t, int32(0), directHit.Load())
assert.Equal(t, int32(1), multipartHit.Load())
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing assertion for completeHit counter.

The test declares completeHit (line 145) and increments it in the handler (line 161), but never asserts its value. For completeness, verify that /multipart/complete/ was called:

💚 Add assertion for completeHit
 	assert.Equal(t, int32(0), directHit.Load())
 	assert.Equal(t, int32(1), multipartHit.Load())
+	assert.Equal(t, int32(1), completeHit.Load())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func TestUpload_LargeFile_MultipartUpload(t *testing.T) {
t.Parallel()
var directHit, multipartHit, completeHit atomic.Int32
fileID := "test-uuid-456"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/base/":
directHit.Add(1)
json.NewEncoder(w).Encode(map[string]string{"file": fileID})
case "/multipart/start/":
multipartHit.Add(1)
json.NewEncoder(w).Encode(map[string]interface{}{
"uuid": fileID,
"parts": []string{},
})
case "/multipart/complete/":
completeHit.Add(1)
json.NewEncoder(w).Encode(FileInfo{FileName: "large.bin"})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer srv.Close()
client := &mockUploadClient{baseURL: srv.URL, conn: srv.Client()}
svc := NewService(client)
data := bytes.NewReader(make([]byte, 20*1024*1024)) // 20MB
info, err := svc.Upload(context.Background(), UploadParams{
Data: data,
Name: "large.bin",
ContentType: "application/octet-stream",
Size: 20 * 1024 * 1024,
})
assert.NoError(t, err)
assert.Equal(t, "large.bin", info.FileName)
assert.Equal(t, int32(0), directHit.Load())
assert.Equal(t, int32(1), multipartHit.Load())
}
func TestUpload_LargeFile_MultipartUpload(t *testing.T) {
t.Parallel()
var directHit, multipartHit, completeHit atomic.Int32
fileID := "test-uuid-456"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.URL.Path {
case "/base/":
directHit.Add(1)
json.NewEncoder(w).Encode(map[string]string{"file": fileID})
case "/multipart/start/":
multipartHit.Add(1)
json.NewEncoder(w).Encode(map[string]interface{}{
"uuid": fileID,
"parts": []string{},
})
case "/multipart/complete/":
completeHit.Add(1)
json.NewEncoder(w).Encode(FileInfo{FileName: "large.bin"})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer srv.Close()
client := &mockUploadClient{baseURL: srv.URL, conn: srv.Client()}
svc := NewService(client)
data := bytes.NewReader(make([]byte, 20*1024*1024)) // 20MB
info, err := svc.Upload(context.Background(), UploadParams{
Data: data,
Name: "large.bin",
ContentType: "application/octet-stream",
Size: 20 * 1024 * 1024,
})
assert.NoError(t, err)
assert.Equal(t, "large.bin", info.FileName)
assert.Equal(t, int32(0), directHit.Load())
assert.Equal(t, int32(1), multipartHit.Load())
assert.Equal(t, int32(1), completeHit.Load())
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@upload/upload_test.go` around lines 142 - 184, The test
TestUpload_LargeFile_MultipartUpload declares and increments completeHit in the
HTTP handler for the "/multipart/complete/" case but never asserts it; add an
assertion after the upload to verify completeHit.Load() equals 1 (use the
existing assert package, e.g. assert.Equal(t, int32(1), completeHit.Load())) so
the test confirms the complete endpoint was invoked.

@alxgsv alxgsv changed the title Implement phase 3 SDK enhancements Phase 3: SDK enhancements Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant