Skip to content

feat!: sync .NET client with latest spec and apify-client-js v3 behavior fixes - #15

Merged
Pijukatel merged 7 commits into
masterfrom
claude/compassionate-babbage-ciag1b
Oct 6, 2026
Merged

Pijukatel merged 7 commits into
masterfrom
claude/compassionate-babbage-ciag1b

Conversation

@Pijukatel

@Pijukatel Pijukatel commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Syncs the .NET client to OpenAPI spec v2-2026-10-01T153946Z (apify/apify-docs#3024, apify/apify-docs#3037) and ports the behavior-relevant changes from the apify-client-js v2 → v3 rewrite (apify/apify-client-js#985 … #1104). The version is bumped to 0.4.0, and CHANGELOG.md calls out each breaking change.

Spec

  • Build.ImageDigest.
  • Task-publish remarks and docs/tasks.md now match the corrected spec wording.

Reference-client parity

  • Breaking: per-status ApifyApiException subclasses (InvalidRequestException, UnauthorizedException, ForbiddenException, NotFoundException, ConflictException, RateLimitException, ServerException). All of them are still caught by catch (ApifyApiException).
  • Breaking: a 404 on a resource reached without its own ID (RunClient.Dataset(), .KeyValueStore(), .RequestQueue(), .Log(), BuildClient.Log()) now throws instead of returning null. DatasetClient.GetStatisticsAsync, ScheduleClient.GetLogAsync and TaskClient.GetInputAsync now throw when the parent resource is missing. LogClient.StreamAsync returns Stream?.
  • Breaking: timeout tiers (Short 5s / Medium 30s / Long 360s) are assigned per method. ApifyClientOptions.TimeoutShortSecs and TimeoutMediumSecs are new, and TimeoutSecs stays the overall cap. Every resource client gets a WithTimeout(TimeSpan) override.
  • Breaking: the ID-addressed accessors on ApifyClient, plus ActorClient.Version and ActorVersionClient.EnvVar, reject an empty ID.
  • Breaking: ActorVersionCollectionClient.ListAsync / IterateAsync drop the unused ListOptions parameter.
  • DatasetClient.IterateItemsAsync paginates by X-Apify-Pagination-Count. Before, a filtered page could stall or re-yield items, and an Unwind page could skip rows.
  • CreateItemsPublicUrlAsync gets a format parameter.
  • Request compression is skipped for content types that are already compressed.
  • BaseUrl / PublicBaseUrl no longer double /v2.
  • ActorClient.StartAsync / ValidateInputAsync and RunClient.MetamorphAsync accept raw byte[] input. Task input stays JSON-only, as in the reference client.
  • format: uri response fields (ActorRun.ContainerUrl, Webhook.RequestUrl) are normalized to their RFC 3986 form.

Review

It took seven /review-client iterations to get here. The rejections caught:

  • a missing timeout-tier port
  • wrong tier classifications
  • a timeout-cap regression
  • three call sites ignoring WithTimeout
  • stale doc comments

Iteration 7 was Approved with no open items.

Test plan

  • dotnet format --verify-no-changes
  • dotnet build -c Release (0 warnings/errors)
  • dotnet test -c Release with live APIFY_TOKEN: 221/221 pass (unit, integration, examples)

…ior fixes

Bumps the OpenAPI spec baseline to v2-2026-10-01T153946Z (adds Build.ImageDigest;
drops a stale "contact support" sentence from task-publish docs) and ports the
behavior-relevant fixes from apify-client-js's v2->v3 rewrite (PRs #985-#1104):

- Per-status ApifyApiException subclasses (InvalidRequestException, UnauthorizedException,
  ForbiddenException, NotFoundException, ConflictException, RateLimitException, ServerException).
- A 404 reached through a chained client with no id of its own (run.Dataset()/.KeyValueStore()/
  .RequestQueue()/.Log(), build.Log()) now throws instead of resolving null/no-op, since the
  ambiguity (parent vs. nested resource) made the old null misleading; id-addressed lookups are
  unaffected. DatasetClient.GetStatisticsAsync/ScheduleClient.GetLogAsync/TaskClient.GetInputAsync
  now throw rather than resolve null for a missing parent.
- Dataset item iteration now paginates by the API's scanned-row count instead of the returned item
  count, fixing early stops/re-yields on filtered pages and skips on Unwind-expanded ones.
- CreateItemsPublicUrlAsync gained a `format` parameter.
- Request-body compression now skips already-compressed content types (images, audio, video,
  archives, office/ZIP formats, web fonts).
- BaseUrl/PublicBaseUrl accept a URL that already ends with /v2 without doubling it.
- Actor/task start, Actor input validation and run metamorph accept byte[] input sent as raw bytes.
- ActorVersionCollectionClient drops its non-functional ListOptions parameter (the endpoint never
  supported pagination); ActorClient.Version/ActorVersionClient.EnvVar/ApifyClient.Build reject an
  empty id.

Verified path traversal (#1011), custom HTTP client (#1058), configurable compression (#1065) and
CancellationToken-as-AbortSignal (#1081) were already correctly implemented; URL-field normalization
(#1045) and the full timeout-tier redesign (#1046) are deliberately out of scope for this PR (see
notes.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
@apify-service-account apify-service-account added the tested Temporary label used only programatically for some analytics. label Oct 2, 2026
@apify-service-account apify-service-account added the t-tooling Issues with this label are in the ownership of the tooling team. label Oct 2, 2026
claude added 6 commits October 2, 2026 17:06
…tion

Addresses the three open review items from PR #15's first review round:

- Added the reference client's timeout-tier system: Short/Medium/Long request
  tiers with constructor-configurable durations (ApifyClientOptions.TimeoutShortSecs
  default 5, TimeoutMediumSecs default 30, TimeoutSecs default 360 now also the
  Long tier and overall clamp). Every call site across every resource client now
  picks its tier explicitly, classified from the actual apify-client-js source
  per method. A per-call literal `timeoutSecs` parameter on every method was not
  added: CancellationToken + CancelAfter() is .NET's idiomatic per-call deadline
  mechanism, and the existing per-attempt-doubling-capped-at-overall algorithm
  already matches the reference's own timeout semantics.
- Reverted TaskClient.StartAsync to JSON-only input: the reference's
  TaskStartOptions has no contentType to pair raw bytes with, so the earlier
  byte[] support there was an over-port that mislabeled raw bytes as JSON.
- Added URL normalization (RFC 3986, via System.Uri) to the client's two typed
  properties among the reference's 17 normalized fields (ActorRun.ContainerUrl,
  Webhook.RequestUrl); RequestQueueRequest.Url is correctly left unnormalized,
  matching the reference's explicit exclusion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
Addresses the two open items from PR #15's second review round:

- Fixed 5 tier-classification mismatches found against the reference source:
  ActorCollectionClient/TaskCollectionClient.CreateAsync (Short -> Medium,
  matching the reference's non-idempotent-create tier, avoiding a duplicate
  Actor/Task on a retried timeout), RunClient.ChargeAsync (was silently
  defaulting to Long, now explicit Short), BuildClient.GetOpenApiDefinitionAsync
  (Short -> Medium), BuildClient.AbortAsync (Medium -> Short).
- Added WithTimeout(TimeSpan) to every resource client, a general
  per-call(-instance) timeout override matching the reference client's
  per-call `timeoutSecs` option (TimeSpan.Zero for no timeout, matching
  'noTimeout'). Implementing this surfaced and fixed a latent bug: an
  explicit override longer than the configured overall budget was silently
  clamped back down to it on the very first attempt, so asking for more time
  than the default had no effect. The retry-growth cap for a call now also
  rises to cover an explicit override that exceeds it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
…call sites

Addresses the two new defects from PR #15's third review round:

- Reverted the Iteration-3 change that let an explicit WithTimeout override
  exceed ApifyClientOptions.TimeoutSecs: the reference client always caps a
  per-call timeout at timeoutMaxSecs (logging a warning when exceeded) rather
  than letting it win, and the client's own RetryConfig.TimeoutSecs doc already
  promised an overall cap. WithTimeout now behaves like the reference: a value
  above TimeoutSecs is capped at it; raise TimeoutSecs itself for a longer
  per-call timeout. TimeSpan.Zero (no timeout) is unaffected, as before.
- RunClient.ChargeAsync, TaskClient.UpdateInputAsync and
  UserClient.UpdateLimitsAsync call HttpClientCore directly rather than
  through a ResourceContext CRUD primitive, and each omitted the context's
  timeout override, so WithTimeout silently had no effect on them. Fixed all
  three to pass it through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
Addresses the two open items from PR #15's fourth review round:

- docs/README.md's "Timeout tiers" section still described the reverted
  Iteration-3 behavior ("asking for more time is honored rather than
  silently clamped"). Corrected to state a WithTimeout override above
  TimeoutSecs is capped at it, matching the code, the XML docs and the
  CHANGELOG.
- The empty-id guard added to ApifyClient.Build was never applied to its 11
  sibling id-addressed accessors (Actor, Run, Dataset, KeyValueStore,
  RequestQueue, Task, Schedule, Webhook, WebhookDispatch, Log, User), so an
  empty id on any of them silently mis-addressed a collection-shaped URL
  instead of throwing. Extracted a shared RequireNonEmptyId helper and
  applied it to all 12 accessors, matching the reference client's uniform
  id.min(1) validation.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
Addresses the single open item from PR #15's fifth review round: the
AttemptTimeout XML doc still described the reverted Iteration-3 behavior
(an explicit override raising the overall cap). The code already caps an
override at the overall budget (fixed in efebae2); only the comment was
stale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
… happens

The previous reword (95141d1) claimed an explicit WithTimeout override is
"capped to by the caller before reaching" AttemptTimeout, but the caller
(CallAsync) does no capping -- baseTimeout is passed through uncapped and
the cap is applied inside AttemptTimeout itself (including on the first,
unscaled attempt). Corrected the comment to describe the actual control
flow precisely.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STUGToGEaEFoBGRbWTvF4m
@Pijukatel Pijukatel added the adhoc Ad-hoc unplanned task added during the sprint. label Oct 2, 2026 — with Claude
@Pijukatel
Pijukatel merged commit 3b94f42 into master Oct 6, 2026
2 of 5 checks passed
@Pijukatel
Pijukatel deleted the claude/compassionate-babbage-ciag1b branch October 6, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-tooling Issues with this label are in the ownership of the tooling team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants