Skip to content

Phase 7: Upload API User Agent#31

Open
alxgsv wants to merge 20 commits into
masterfrom
phase7/upload-api-user-agent
Open

Phase 7: Upload API User Agent#31
alxgsv wants to merge 20 commits into
masterfrom
phase7/upload-api-user-agent

Conversation

@alxgsv

@alxgsv alxgsv commented Apr 9, 2026

Copy link
Copy Markdown
Contributor
  • Add userAgent field to uploadAPIClient and build it from UserAgentPrefix, ClientVersion, and PublicKey — matching the existing pattern in restAPIClient and projectAPIClient
  • Set the User-Agent header in uploadAPIClient.NewRequest, which was previously missing entirely
  • Honor Config.UserAgent for custom suffixes (e.g. UploadcareCLI/1.0.0)

Without this fix, Upload API requests send Go's default Go-http-client/2.0 as the User-Agent, making it impossible to identify the SDK or any consumer application.

Summary by CodeRabbit

  • New Features

    • Added Project API support with bearer token authentication for managing projects and secrets
    • Introduced file metadata API for key-value metadata operations
    • Added addons support for background removal, virus scanning, and label detection
    • Added automatic direct vs. multipart upload selection
    • Extended file and metadata APIs with optional parameters and bracket notation encoding
    • New conversion path builders for document and video operations
  • Improvements

    • Webhook deletion now uses webhook ID instead of target URL
    • Added group deletion capability
    • Configurable automatic retry logic for throttled requests
    • Enhanced error handling with typed error structures
    • Improved file info queries with optional include parameters

alxgsv added 20 commits March 13, 2026 16:19
Previously, any status not explicitly handled (e.g. 409, 403, 500, 502)
fell through to the JSON decode path, silently producing zero-value
results. Now all >= 400 statuses return an error.

Also fix Decode(&resdata) -> Decode(resdata) to avoid unnecessary
*interface{} indirection.
Extract handleResponse method with defer resp.Body.Close() to
guarantee the response body is closed on all paths including nil
resdata, error decoding, and retry. Replace goto with for loop.
Derive CDN base URL from the public key (SHA256 → base36 → first 10 chars).
Config.CDNBase can be set explicitly to override the default. Shallow-copy Config in resolveConfig to avoid mutating a shared template when creating clients for multiple projects.
New projectapi/ package covering projects CRUD, secret keys management,
and usage metrics. Adds ucare.NewBearerClient() for bearer token auth
used by the Project API.
Pagination next URLs point to a different host (app.uploadcare.com),
so List and ListSecrets now use codec.ResultBuf iterators that
automatically follow next links. Bearer client fallback routes
unknown hosts through the same auth backend.
Pagination next URLs point to a different host (app.uploadcare.com).
NewRequest was failing with errNoClient before Do/fallbackDo could
run. Add fallbackNewReq to route unknown endpoints through the bearer
auth client for both request construction and execution.
@coderabbitai

coderabbitai Bot commented Apr 9, 2026

Copy link
Copy Markdown

Walkthrough

This pull request introduces breaking API changes, expands SDK functionality with new packages for Project API, metadata CRUD, and addon services, implements retry configuration and typed errors, adds metadata support to uploads, updates webhook and file services, and includes comprehensive test coverage across all changes.

Changes

Cohort / File(s) Summary
File Service Signature Updates
file/file.go, file/service.go, file/list.go
Updated Info() signature to accept *InfoParams for optional include parameter; added Include field to ListParams for requesting additional fields like appdata.
Webhook Service Signature Updates
webhook/webhook.go, webhook/service.go, webhook/service_test.go, webhook/webhook_test.go
Changed Delete() from URL-based to ID-based deletion (int64); added new event constants (EventFileStored, EventFileDeleted, EventFileInfoUpdated) and deprecated EventFileInfected.
Project API Service
projectapi/projectapi.go, projectapi/service.go, projectapi/projects.go, projectapi/secrets.go, projectapi/usage.go, projectapi/params.go, projectapi/log.go, projectapi/service_test.go
New package providing full Project API implementation: project CRUD, secret management, usage metrics, bearer token authentication support.
Metadata Service
metadata/metadata.go, metadata/service.go, metadata/validation.go, metadata/log.go, metadata/service_test.go
New package for file metadata CRUD operations with key validation, URI escaping, and dedicated logger.
Addon Service
addon/addon.go, addon/service.go, addon/log.go, addon/service_test.go
New package for addon execution (background removal, ClamAV scanning, Rekognition) with status polling support.
Bearer Client & Error Handling
ucare/client.go, ucare/error.go, ucare/error_test.go, ucare/projectapi.go, ucare/projectapi_test.go
Added NewBearerClient() for Project API authentication; replaced internal error types with exported typed errors (APIError, AuthError, ThrottleError, ValidationError, ForbiddenError, ProjectAPIError).
Retry Configuration
ucare/retry.go, ucare/retry_test.go, ucare/restapi.go, ucare/restapi_test.go, ucare/uploadapi.go, ucare/uploadapi_test.go
Added RetryConfig for configurable throttle retry limits; refactored restAPIClient and uploadAPIClient to use exponential backoff with Retry-After support; removed hardcoded MaxThrottleRetries constant.
Upload Enhancements
upload/upload.go, upload/upload_test.go, upload/direct.go, upload/fromurl.go, upload/multipart.go, upload/service.go
Added Upload() method with automatic direct vs. multipart selection based on file size; added metadata support to all upload types via map[string]string field.
Conversion Path Helpers
conversion/conversion.go, conversion/path.go, conversion/path_test.go, conversion/conversion_test.go
Added BuildDocumentPath() and BuildVideoPath() for constructing conversion URLs; added SaveInGroup parameter to conversion requests.
Metadata Bracket Notation in Codec
internal/codec/codec.go, internal/codec/codec_test.go
Updated EncodeReqQuery and form encoding to support map[string]string fields with bracket notation (e.g., metadata[key1]=value1); added comprehensive test coverage.
Group Service Enhancement
group/group.go, group/service.go, group/group_test.go
Added Delete() method for group deletion by ID.
CDN Helpers
ucare/cdn.go, ucare/cdn_test.go
Added CDNBaseURL() and CnamePrefix() functions for computing per-project CDN URLs; added DefaultCDNDomain constant.
Configuration & Service Updates
ucare/client.go
Extended Config with Retry and CDNBase fields; updated NewClient() and client construction to resolve CDN base from public key.
Test Infrastructure
test/testenv/runner.go, test/addon.go, test/metadata.go, test/projectapi.go, test/file.go, test/group.go, test/webhook.go, test/integration_test.go
Extended test runner with Metadata and Addon service fields; added integration test helpers for all new services; updated existing tests to use new API signatures.
Documentation
CHANGELOG.md, README.md, ucare/doc.go
Updated examples for new file.Info() signature; added Project API bearer client usage example; documented breaking changes and new features in changelog.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant UAREClient as ucare.Client<br/>(Bearer)
    participant ProjectAPI as projectAPIClient
    participant HTTPServer as HTTP Server<br/>(Project API)

    Client->>UAREClient: NewBearerClient(token)
    UAREClient->>ProjectAPI: Initialize with token
    
    Client->>UAREClient: List Projects
    UAREClient->>ProjectAPI: NewRequest()
    ProjectAPI->>ProjectAPI: Set Authorization: Bearer <token>
    ProjectAPI->>HTTPServer: GET /projects/
    HTTPServer-->>ProjectAPI: JSON response
    ProjectAPI->>ProjectAPI: handleResponse()
    ProjectAPI-->>UAREClient: *ProjectList
    UAREClient-->>Client: *ProjectList
    
    Client->>UAREClient: Get Project by PubKey
    UAREClient->>ProjectAPI: NewRequest()
    ProjectAPI->>HTTPServer: GET /projects/{pubkey}/
    HTTPServer-->>ProjectAPI: JSON Project
    ProjectAPI->>UAREClient: Project
    UAREClient-->>Client: Project
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding User-Agent support to the Upload API client.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase7/upload-api-user-agent

Warning

Review ran into problems

🔥 Problems

Timed out fetching pipeline failures after 30000ms


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
file/service.go (1)

21-21: Consider compatibility impact of changing exported interface signatures.

Updating file.Service.Info to require *InfoParams forces all external implementers/mocks to change. If backward compatibility is a goal, consider keeping Info(ctx, id) and adding a new method for optional params.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@file/service.go` at line 21, Changing the exported method signature of
file.Service.Info to accept *InfoParams breaks backward compatibility for
external implementers and mocks; restore the original signature Info(ctx
context.Context, id string) (Info, error) on the file.Service interface and add
a new method (e.g., InfoWithParams(ctx context.Context, id string, params
*InfoParams) (Info, error)) or provide a default-wrapper implementation that
calls the new params-aware function so existing callers/implementers do not need
to change; update any internal uses to call the new method where params are
required and keep the original Info as a shim delegating to the new
implementation when params are nil/default.
test/integration_test.go (1)

110-113: Bound the Project API list call with a timeout context.

Using context.Background() here can hang CI indefinitely on network stalls. A short timeout makes failures deterministic.

⏱️ Suggested change
 import (
 	"context"
 	"os"
 	"testing"
+	"time"
@@
-	list, err := svc.List(context.Background(), nil)
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+	list, err := svc.List(ctx, nil)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration_test.go` around lines 110 - 113, Replace the use of
context.Background() when calling svc.List with a bounded context created via
context.WithTimeout to avoid hanging tests; create a ctx, cancel :=
context.WithTimeout(context.Background(), <reasonableDuration>) and defer
cancel(), then call svc.List(ctx, nil) and handle the error as before (t.Fatal
on error). Update the test function surrounding the svc.List call to
import/time-reference if needed and choose an appropriate timeout (e.g., a few
seconds) to make CI deterministic.
projectapi/usage.go (1)

32-42: Validate metric before building the request path.

GetUsageMetric documents allowed values but currently accepts any string. A fast client-side check gives clearer errors and avoids invalid requests.

✅ Suggested guard
 func (s service) GetUsageMetric(
 	ctx context.Context,
 	pubKey string,
 	metric string,
 	params UsageDateRange,
 ) (data UsageMetric, err error) {
+	switch metric {
+	case "traffic", "storage", "operations":
+		// ok
+	default:
+		return data, fmt.Errorf("invalid metric %q", metric)
+	}
+
 	err = s.svc.ResourceOp(
 		ctx,
 		http.MethodGet,
 		fmt.Sprintf(usageMetricPathFmt, pubKey, metric),
 		&params,
 		&data,
 	)
 	return
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projectapi/usage.go` around lines 32 - 42, Add a client-side validation in
GetUsageMetric to reject invalid metric names before calling
service.svc.ResourceOp: check the incoming metric string against the documented
allowed set (e.g., a small whitelist or map of allowed metric keys) and return a
descriptive error immediately if not present. Place this guard at the top of
GetUsageMetric (before fmt.Sprintf(usageMetricPathFmt, pubKey, metric)) so the
request path is never built for invalid metrics; use the same error type/format
conventions the service uses for input validation.
upload/upload_test.go (1)

70-85: Assert the expected HTTP verb in these handlers.

These test servers route only by path right now. A regression from POST to another method on /base/, /multipart/start/, or /multipart/complete/ would still pass, so the tests can miss a real request-construction bug.

Also applies to: 110-124, 150-168, 195-212, 240-254, 280-297, 323-337, 359-376

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@upload/upload_test.go` around lines 70 - 85, The test HTTP handler currently
only switches on r.URL.Path, so regressions in HTTP verbs will be missed; update
the httptest server handler (the function passed to httptest.NewServer where srv
is created) to check r.Method for each path (e.g., assert POST for "/base/",
"/multipart/start/", "/multipart/complete/" and GET for "/info/") and return a
405 Method Not Allowed (or fail the test) when the method is unexpected,
incrementing the appropriate counters only after method validation; apply the
same change to the other test server handler blocks referenced (lines handling
"/base/", "/info/", "/multipart/start/", "/multipart/complete/") so all paths
validate the expected HTTP verb.
ucare/uploadapi_test.go (1)

72-103: Assert the full Upload API User-Agent contract here.

These checks only verify substrings. They would still pass if the upload client dropped the public-key component or if the header stopped matching the internally constructed userAgent, which is the regression this PR is meant to prevent.

🔍 Suggested assertion upgrade
 func TestUploadAPIClient_UserAgent(t *testing.T) {
 	t.Parallel()
 
 	client := newUploadAPIClient(testCreds(), resolveConfig(nil, testCreds()))
@@
 	)
 	assert.NoError(t, err)
-	assert.Contains(t, req.Header.Get("User-Agent"), "UploadcareGo/")
+	ua := req.Header.Get("User-Agent")
+	assert.Equal(t, client.userAgent, ua)
+	assert.Contains(t, ua, testCreds().PublicKey)
 }
 
 func TestUploadAPIClient_CustomUserAgent(t *testing.T) {
@@
 	)
 	assert.NoError(t, err)
-	assert.Contains(t, req.Header.Get("User-Agent"), "UploadcareGo/")
-	assert.Contains(t, req.Header.Get("User-Agent"), "MyApp/1.0")
+	ua := req.Header.Get("User-Agent")
+	assert.Equal(t, client.userAgent, ua)
+	assert.Contains(t, ua, testCreds().PublicKey)
+	assert.Contains(t, ua, "MyApp/1.0")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/uploadapi_test.go` around lines 72 - 103, Update the two tests
TestUploadAPIClient_UserAgent and TestUploadAPIClient_CustomUserAgent to assert
the full User-Agent header equals the constructed userAgent string rather than
only checking substrings: retrieve the client’s configured userAgent (the same
value built by newUploadAPIClient/resolveConfig using Config.UserAgent and the
internal UploadcareGo prefix/public-key component) and compare
req.Header.Get("User-Agent") to that exact expected string (include both the
UploadcareGo/<version> + public-key component and, for the custom case, the
appended " MyApp/1.0" piece) so the tests fail on any deviation from the
intended userAgent contract.
CHANGELOG.md (1)

39-40: Call out the Upload API header fix explicitly.

This notes the new config field, but not the behavior change from this PR: Upload API requests now send the SDK/application User-Agent instead of Go's default header. Adding that release note would make the change much easier to spot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG.md` around lines 39 - 40, Add a concise release note line calling
out that Upload API requests now send the SDK/application User-Agent instead of
Go's default header; reference the new UserAgent field on ucare.Config and
mention "Upload API" and "User-Agent header" so readers see the behavioral
change introduced by this PR.
projectapi/projects.go (1)

25-26: Return nil when List fails.

List currently returns a non-nil *ProjectList even when s.svc.List errors. That wrapper has a nil iterator underneath, so Next() will panic if the caller keeps it around after an error.

Suggested change
 	resbuf, err := s.svc.List(ctx, projectsPath, enc)
-	return &ProjectList{raw: resbuf}, err
+	if err != nil {
+		return nil, err
+	}
+	return &ProjectList{raw: resbuf}, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projectapi/projects.go` around lines 25 - 26, The wrapper always returns
&ProjectList{raw: resbuf} even when s.svc.List returns an error, which leaves
ProjectList.raw nil and causes Next() to panic; change the return path in the
function that calls s.svc.List so that if err != nil you return nil, err
(instead of returning a non-nil *ProjectList). Update the block around the
s.svc.List call to check err and only construct &ProjectList{raw: resbuf} when
err == nil so callers do not receive a dangling ProjectList with a nil iterator.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@conversion/path.go`:
- Around line 60-76: The size and thumbs path fragments are being built
incorrectly: instead of emitting ResizeMode as a separate trailing segment after
the size, combine it into the size fragment (e.g. produce
"-/size/{size}/{resizeMode}/" in the same fmt call) so the transformation string
remains well-formed (adjust the fmt.Fprintf usage that writes "-/size/%s/" and
the conditional that writes opts.ResizeMode), and change the thumbs fragment to
use slashes instead of a tilde by replacing the "-/thumbs~%d/" fmt with
"-/thumbs/%d/" (update the fmt.Fprintf that references opts.Thumbs accordingly).

In `@projectapi/projectapi.go`:
- Around line 58-63: The UploadSettings struct is causing null serializations
for unset optional limits; update the JSON tags for FilesizeLimit and
ImageResolutionLimit in UploadSettings so they include `omitempty` (similar to
Autostore and IsSignedUploadEnabled) so that when UpdateProjectParams sends
partial updates those unset pointer fields are omitted instead of serializing as
null; locate the UploadSettings type and modify the `json:"filesize_limit"` and
`json:"image_resolution_limit"` tags to use `omitempty`.

In `@ucare/client.go`:
- Around line 147-157: The current resolveBearerConfig (and the similar
resolveConfig helper used for NewClient) shallow-copies the Config struct so the
Retry pointer is shared; change these functions to deep-copy the Retry field by
creating a new RetryConfig when conf.Retry != nil and assigning its value to the
cloned config (e.g., allocate new variable like copied := *conf; if conf.Retry
!= nil { r := *conf.Retry; copied.Retry = &r } ), and ensure HTTPClient
defaulting remains as-is so the returned *Config is fully independent of the
caller's Retry pointer.

In `@ucare/projectapi.go`:
- Around line 116-123: The current logic enforces c.retry.MaxWaitSeconds only
when Retry-After is present, but the fallback expBackoff(tries) can exceed the
cap; update the retry path in the function that computes wait (variables
retryAfter, wait and call to expBackoff(tries)) to enforce the cap for both
cases by applying the MaxWaitSeconds limit after computing wait (e.g., if
c.retry.MaxWaitSeconds > 0 then set wait = min(wait, c.retry.MaxWaitSeconds))
and return ThrottleError{RetryAfter: wait} or proceed with sleeping using the
capped wait so the configured maximum is always honored.

In `@ucare/restapi.go`:
- Around line 135-155: The 400/401/403/404 case branches currently return the
json.Decoder error directly when json.NewDecoder(resp.Body).Decode(...) fails,
losing the HTTP status and typed error info; update each branch (the 400/404
APIError handling, the 401 AuthError handling, and the 403 ForbiddenError
handling) to catch Decode errors and instead return the corresponding typed
error with StatusCode set (and a sensible default Message/Detail populated, e.g.
from a fallback string or resp.Status) similar to how the generic ">=400" path
builds a fallback error; do not let decoder errors escape as-is—only return
decode errors if you still want to wrap them, otherwise return the typed error
with StatusCode/default detail.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Around line 39-40: Add a concise release note line calling out that Upload API
requests now send the SDK/application User-Agent instead of Go's default header;
reference the new UserAgent field on ucare.Config and mention "Upload API" and
"User-Agent header" so readers see the behavioral change introduced by this PR.

In `@file/service.go`:
- Line 21: Changing the exported method signature of file.Service.Info to accept
*InfoParams breaks backward compatibility for external implementers and mocks;
restore the original signature Info(ctx context.Context, id string) (Info,
error) on the file.Service interface and add a new method (e.g.,
InfoWithParams(ctx context.Context, id string, params *InfoParams) (Info,
error)) or provide a default-wrapper implementation that calls the new
params-aware function so existing callers/implementers do not need to change;
update any internal uses to call the new method where params are required and
keep the original Info as a shim delegating to the new implementation when
params are nil/default.

In `@projectapi/projects.go`:
- Around line 25-26: The wrapper always returns &ProjectList{raw: resbuf} even
when s.svc.List returns an error, which leaves ProjectList.raw nil and causes
Next() to panic; change the return path in the function that calls s.svc.List so
that if err != nil you return nil, err (instead of returning a non-nil
*ProjectList). Update the block around the s.svc.List call to check err and only
construct &ProjectList{raw: resbuf} when err == nil so callers do not receive a
dangling ProjectList with a nil iterator.

In `@projectapi/usage.go`:
- Around line 32-42: Add a client-side validation in GetUsageMetric to reject
invalid metric names before calling service.svc.ResourceOp: check the incoming
metric string against the documented allowed set (e.g., a small whitelist or map
of allowed metric keys) and return a descriptive error immediately if not
present. Place this guard at the top of GetUsageMetric (before
fmt.Sprintf(usageMetricPathFmt, pubKey, metric)) so the request path is never
built for invalid metrics; use the same error type/format conventions the
service uses for input validation.

In `@test/integration_test.go`:
- Around line 110-113: Replace the use of context.Background() when calling
svc.List with a bounded context created via context.WithTimeout to avoid hanging
tests; create a ctx, cancel := context.WithTimeout(context.Background(),
<reasonableDuration>) and defer cancel(), then call svc.List(ctx, nil) and
handle the error as before (t.Fatal on error). Update the test function
surrounding the svc.List call to import/time-reference if needed and choose an
appropriate timeout (e.g., a few seconds) to make CI deterministic.

In `@ucare/uploadapi_test.go`:
- Around line 72-103: Update the two tests TestUploadAPIClient_UserAgent and
TestUploadAPIClient_CustomUserAgent to assert the full User-Agent header equals
the constructed userAgent string rather than only checking substrings: retrieve
the client’s configured userAgent (the same value built by
newUploadAPIClient/resolveConfig using Config.UserAgent and the internal
UploadcareGo prefix/public-key component) and compare
req.Header.Get("User-Agent") to that exact expected string (include both the
UploadcareGo/<version> + public-key component and, for the custom case, the
appended " MyApp/1.0" piece) so the tests fail on any deviation from the
intended userAgent contract.

In `@upload/upload_test.go`:
- Around line 70-85: The test HTTP handler currently only switches on
r.URL.Path, so regressions in HTTP verbs will be missed; update the httptest
server handler (the function passed to httptest.NewServer where srv is created)
to check r.Method for each path (e.g., assert POST for "/base/",
"/multipart/start/", "/multipart/complete/" and GET for "/info/") and return a
405 Method Not Allowed (or fail the test) when the method is unexpected,
incrementing the appropriate counters only after method validation; apply the
same change to the other test server handler blocks referenced (lines handling
"/base/", "/info/", "/multipart/start/", "/multipart/complete/") so all paths
validate the expected HTTP verb.
🪄 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: 15e475e3-ed5e-427e-a617-1409e68d28c6

📥 Commits

Reviewing files that changed from the base of the PR and between 766afd6 and d58ba67.

📒 Files selected for processing (65)
  • CHANGELOG.md
  • README.md
  • addon/addon.go
  • addon/log.go
  • addon/service.go
  • addon/service_test.go
  • conversion/conversion.go
  • conversion/conversion_test.go
  • conversion/path.go
  • conversion/path_test.go
  • file/file.go
  • file/list.go
  • file/service.go
  • file/service_test.go
  • group/group.go
  • group/group_test.go
  • group/service.go
  • internal/codec/codec.go
  • internal/codec/codec_test.go
  • internal/config/config.go
  • metadata/log.go
  • metadata/metadata.go
  • metadata/service.go
  • metadata/service_test.go
  • metadata/validation.go
  • projectapi/log.go
  • projectapi/params.go
  • projectapi/projectapi.go
  • projectapi/projects.go
  • projectapi/secrets.go
  • projectapi/service.go
  • projectapi/service_test.go
  • projectapi/usage.go
  • test/addon.go
  • test/file.go
  • test/group.go
  • test/integration_test.go
  • test/metadata.go
  • test/projectapi.go
  • test/testenv/runner.go
  • test/webhook.go
  • ucare/cdn.go
  • ucare/cdn_test.go
  • ucare/client.go
  • ucare/doc.go
  • ucare/error.go
  • ucare/error_test.go
  • ucare/projectapi.go
  • ucare/projectapi_test.go
  • ucare/restapi.go
  • ucare/restapi_test.go
  • ucare/retry.go
  • ucare/retry_test.go
  • ucare/uploadapi.go
  • ucare/uploadapi_test.go
  • upload/direct.go
  • upload/fromurl.go
  • upload/multipart.go
  • upload/service.go
  • upload/upload.go
  • upload/upload_test.go
  • webhook/service.go
  • webhook/service_test.go
  • webhook/webhook.go
  • webhook/webhook_test.go
💤 Files with no reviewable changes (1)
  • internal/config/config.go

Comment thread conversion/path.go
Comment on lines +60 to +76
if opts.Size != "" {
fmt.Fprintf(&b, "-/size/%s/", opts.Size)
if opts.ResizeMode != "" {
fmt.Fprintf(&b, "%s/", opts.ResizeMode)
}
}

if opts.Quality != "" {
fmt.Fprintf(&b, "-/quality/%s/", opts.Quality)
}

if opts.CutStart != "" && opts.CutLength != "" {
fmt.Fprintf(&b, "-/cut/%s/%s/", opts.CutStart, opts.CutLength)
}

if opts.Thumbs > 0 {
fmt.Fprintf(&b, "-/thumbs~%d/", opts.Thumbs)

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 | 🟠 Major

ResizeMode and Thumbs currently generate malformed video paths.

ResizeMode is appended as a bare segment after size, and thumbs uses ~ instead of /. Both options will build invalid transformation strings when set.

🔧 Proposed fix
 	if opts.Size != "" {
 		fmt.Fprintf(&b, "-/size/%s/", opts.Size)
 		if opts.ResizeMode != "" {
-			fmt.Fprintf(&b, "%s/", opts.ResizeMode)
+			fmt.Fprintf(&b, "-/resize_mode/%s/", opts.ResizeMode)
 		}
 	}
@@
 	if opts.Thumbs > 0 {
-		fmt.Fprintf(&b, "-/thumbs~%d/", opts.Thumbs)
+		fmt.Fprintf(&b, "-/thumbs/%d/", opts.Thumbs)
 	}
📝 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 opts.Size != "" {
fmt.Fprintf(&b, "-/size/%s/", opts.Size)
if opts.ResizeMode != "" {
fmt.Fprintf(&b, "%s/", opts.ResizeMode)
}
}
if opts.Quality != "" {
fmt.Fprintf(&b, "-/quality/%s/", opts.Quality)
}
if opts.CutStart != "" && opts.CutLength != "" {
fmt.Fprintf(&b, "-/cut/%s/%s/", opts.CutStart, opts.CutLength)
}
if opts.Thumbs > 0 {
fmt.Fprintf(&b, "-/thumbs~%d/", opts.Thumbs)
if opts.Size != "" {
fmt.Fprintf(&b, "-/size/%s/", opts.Size)
if opts.ResizeMode != "" {
fmt.Fprintf(&b, "-/resize_mode/%s/", opts.ResizeMode)
}
}
if opts.Quality != "" {
fmt.Fprintf(&b, "-/quality/%s/", opts.Quality)
}
if opts.CutStart != "" && opts.CutLength != "" {
fmt.Fprintf(&b, "-/cut/%s/%s/", opts.CutStart, opts.CutLength)
}
if opts.Thumbs > 0 {
fmt.Fprintf(&b, "-/thumbs/%d/", opts.Thumbs)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@conversion/path.go` around lines 60 - 76, The size and thumbs path fragments
are being built incorrectly: instead of emitting ResizeMode as a separate
trailing segment after the size, combine it into the size fragment (e.g. produce
"-/size/{size}/{resizeMode}/" in the same fmt call) so the transformation string
remains well-formed (adjust the fmt.Fprintf usage that writes "-/size/%s/" and
the conditional that writes opts.ResizeMode), and change the thumbs fragment to
use slashes instead of a tilde by replacing the "-/thumbs~%d/" fmt with
"-/thumbs/%d/" (update the fmt.Fprintf that references opts.Thumbs accordingly).

Comment thread projectapi/projectapi.go
Comment on lines +58 to +63
// UploadSettings holds file upload settings for a project.
type UploadSettings struct {
FilesizeLimit *int64 `json:"filesize_limit"`
Autostore *bool `json:"autostore,omitempty"`
ImageResolutionLimit *int64 `json:"image_resolution_limit"`
IsSignedUploadEnabled *bool `json:"is_signed_upload_enabled,omitempty"`

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 | 🟠 Major

Add omitempty to the optional upload limits.

UpdateProjectParams is pointer-based so callers can send partial updates, but these two fields will still serialize as null whenever Features.Uploads is present and the limits are left unset. That turns a focused update like toggling Autostore into an explicit write for unrelated fields.

Suggested change
 type UploadSettings struct {
-	FilesizeLimit         *int64 `json:"filesize_limit"`
+	FilesizeLimit         *int64 `json:"filesize_limit,omitempty"`
 	Autostore             *bool  `json:"autostore,omitempty"`
-	ImageResolutionLimit  *int64 `json:"image_resolution_limit"`
+	ImageResolutionLimit  *int64 `json:"image_resolution_limit,omitempty"`
 	IsSignedUploadEnabled *bool  `json:"is_signed_upload_enabled,omitempty"`
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@projectapi/projectapi.go` around lines 58 - 63, The UploadSettings struct is
causing null serializations for unset optional limits; update the JSON tags for
FilesizeLimit and ImageResolutionLimit in UploadSettings so they include
`omitempty` (similar to Autostore and IsSignedUploadEnabled) so that when
UpdateProjectParams sends partial updates those unset pointer fields are omitted
instead of serializing as null; locate the UploadSettings type and modify the
`json:"filesize_limit"` and `json:"image_resolution_limit"` tags to use
`omitempty`.

Comment thread ucare/client.go
Comment on lines +147 to +157
func resolveBearerConfig(conf *Config) *Config {
if conf == nil {
conf = &Config{}
} else {
copied := *conf
conf = &copied
}
if conf.HTTPClient == nil {
conf.HTTPClient = http.DefaultClient
}
return conf

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 | 🟠 Major

Deep-copy RetryConfig when cloning Config.

These helpers only copy the top-level struct. Since Retry is a pointer, mutating the caller's conf.Retry after NewClient or NewBearerClient still changes the live client's retry policy and can race with in-flight requests.

🔧 Proposed fix
 func resolveBearerConfig(conf *Config) *Config {
 	if conf == nil {
 		conf = &Config{}
 	} else {
 		copied := *conf
 		conf = &copied
 	}
+	if conf.Retry != nil {
+		retryCopy := *conf.Retry
+		conf.Retry = &retryCopy
+	}
 	if conf.HTTPClient == nil {
 		conf.HTTPClient = http.DefaultClient
 	}
 	return conf
 }
 
 func resolveConfig(conf *Config, creds APICreds) *Config {
 	if conf == nil {
 		conf = &Config{}
 	} else {
 		copied := *conf
 		conf = &copied
 	}
+	if conf.Retry != nil {
+		retryCopy := *conf.Retry
+		conf.Retry = &retryCopy
+	}
 	if conf.APIVersion == "" {
 		conf.APIVersion = defaultAPIVersion
 	}

Also applies to: 160-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/client.go` around lines 147 - 157, The current resolveBearerConfig (and
the similar resolveConfig helper used for NewClient) shallow-copies the Config
struct so the Retry pointer is shared; change these functions to deep-copy the
Retry field by creating a new RetryConfig when conf.Retry != nil and assigning
its value to the cloned config (e.g., allocate new variable like copied :=
*conf; if conf.Retry != nil { r := *conf.Retry; copied.Retry = &r } ), and
ensure HTTPClient defaulting remains as-is so the returned *Config is fully
independent of the caller's Retry pointer.

Comment thread ucare/projectapi.go
Comment on lines +116 to +123
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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor MaxWaitSeconds for fallback backoff too.

MaxWaitSeconds is only enforced when Retry-After is present. If that header is missing or invalid, expBackoff(tries) can still exceed the configured cap and sleep longer than the caller allowed.

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

In `@ucare/projectapi.go` around lines 116 - 123, The current logic enforces
c.retry.MaxWaitSeconds only when Retry-After is present, but the fallback
expBackoff(tries) can exceed the cap; update the retry path in the function that
computes wait (variables retryAfter, wait and call to expBackoff(tries)) to
enforce the cap for both cases by applying the MaxWaitSeconds limit after
computing wait (e.g., if c.retry.MaxWaitSeconds > 0 then set wait = min(wait,
c.retry.MaxWaitSeconds)) and return ThrottleError{RetryAfter: wait} or proceed
with sleeping using the capped wait so the configured maximum is always honored.

Comment thread ucare/restapi.go
Comment on lines 135 to +155
case 400, 404:
var err respErr
if e := json.NewDecoder(resp.Body).Decode(&err); e != nil {
return e
var apiErr APIError
if e := json.NewDecoder(resp.Body).Decode(&apiErr); e != nil {
return false, e
}
resp.Body.Close()
return err
apiErr.StatusCode = resp.StatusCode
return false, apiErr
case 401:
var err authErr
if e := json.NewDecoder(resp.Body).Decode(&err); e != nil {
return e
var authErr AuthError
if e := json.NewDecoder(resp.Body).Decode(&authErr); e != nil {
return false, e
}
authErr.StatusCode = 401
return false, authErr
case 403:
var forbiddenErr ForbiddenError
if e := json.NewDecoder(resp.Body).Decode(&forbiddenErr); e != nil {
return false, e
}
resp.Body.Close()
return err
forbiddenErr.StatusCode = 403
return false, forbiddenErr

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 | 🟠 Major

Don't let empty or non-JSON error bodies escape as decoder errors.

These branches return the json.Decoder error directly for 400/401/403/404, so callers lose the HTTP status and typed error information if the upstream body is empty, truncated, or plain text. Please fall back to a typed error with StatusCode/default detail here, the same way the generic >=400 path already does.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ucare/restapi.go` around lines 135 - 155, The 400/401/403/404 case branches
currently return the json.Decoder error directly when
json.NewDecoder(resp.Body).Decode(...) fails, losing the HTTP status and typed
error info; update each branch (the 400/404 APIError handling, the 401 AuthError
handling, and the 403 ForbiddenError handling) to catch Decode errors and
instead return the corresponding typed error with StatusCode set (and a sensible
default Message/Detail populated, e.g. from a fallback string or resp.Status)
similar to how the generic ">=400" path builds a fallback error; do not let
decoder errors escape as-is—only return decode errors if you still want to wrap
them, otherwise return the typed error with StatusCode/default detail.

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