Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ All notable changes to the Apify Go client are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.8.1] - 2026-08-11

### Changed

- Internal consistency cleanup: request methods that used raw HTTP-method string literals
(`"PUT"`, `"DELETE"`, `"POST"`) now use the standard `net/http` method constants, matching
the rest of the client. No public interface change.
- Replaced the non-idiomatic `http_MethodHead` constant with `http.MethodHead`.
- Extracted the repeated `"urlSigningSecretKey"` literal (used by the dataset and key-value-
store public-URL builders) into a single named constant.
- Bumped `ClientVersion` to `0.8.1`.

### Fixed

- Corrected the `TaskClient.Unpublish` doc comment, which incorrectly claimed write permission
to both the task and its Actor is required; verified against the live API that only write
permission to the task itself is needed.

## [0.8.0] - 2026-08-10

### Added
Expand Down
6 changes: 2 additions & 4 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apify

import (
"encoding/json"
"net/http"
"net/url"
"runtime"
"strconv"
Expand Down Expand Up @@ -40,12 +41,9 @@ func isNotFound(err error) bool {
}
return apiErr.Type == recordNotFoundType ||
apiErr.Type == recordOrTokenNotFoundType ||
apiErr.HTTPMethod == http_MethodHead
apiErr.HTTPMethod == http.MethodHead
}

// http_MethodHead avoids importing net/http here just for the constant.
const http_MethodHead = "HEAD"

// QueryParams is an ordered collection of query parameters that omits absent values and
// encodes booleans as 1/0, matching the Apify API conventions.
type QueryParams struct {
Expand Down
2 changes: 1 addition & 1 deletion dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ func (c *DatasetClient) CreateItemsPublicURL(ctx context.Context, options Datase
return "", err
}
if present {
if secret := extractString(dataset.Extra, "urlSigningSecretKey"); secret != "" {
if secret := extractString(dataset.Extra, urlSigningSecretExtraKey); secret != "" {
sig := signStorageContent(secret, dataset.ID, expiresInSecs)
params.AddString("signature", &sig)
}
Expand Down
4 changes: 2 additions & 2 deletions key_value_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ func (c *KeyValueStoreClient) GetRecordPublicURL(ctx context.Context, key string
return "", err
}
if present {
if secret := extractString(store.Extra, "urlSigningSecretKey"); secret != "" {
if secret := extractString(store.Extra, urlSigningSecretExtraKey); secret != "" {
sig := createHmacSignature(secret, key)
params.AddString("signature", &sig)
}
Expand All @@ -278,7 +278,7 @@ func (c *KeyValueStoreClient) CreateKeysPublicURL(ctx context.Context, expiresIn
return "", err
}
if present {
if secret := extractString(store.Extra, "urlSigningSecretKey"); secret != "" {
if secret := extractString(store.Extra, urlSigningSecretExtraKey); secret != "" {
sig := signStorageContent(secret, store.ID, expiresInSecs)
params.AddString("signature", &sig)
}
Expand Down
9 changes: 5 additions & 4 deletions request_queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
)

// ListRequestsOptions configures [RequestQueueClient.ListRequests].
Expand Down Expand Up @@ -119,7 +120,7 @@ func (c *RequestQueueClient) UpdateRequest(ctx context.Context, request RequestQ
if err != nil {
return RequestQueueOperationInfo{}, err
}
resp, err := c.ctx.http.call(ctx, "PUT", url, body, contentTypeJSON, defaultRequestTimeout)
resp, err := c.ctx.http.call(ctx, http.MethodPut, url, body, contentTypeJSON, defaultRequestTimeout)
if err != nil {
return RequestQueueOperationInfo{}, err
}
Expand All @@ -129,7 +130,7 @@ func (c *RequestQueueClient) UpdateRequest(ctx context.Context, request RequestQ
// DeleteRequest deletes a request by ID.
func (c *RequestQueueClient) DeleteRequest(ctx context.Context, id string) error {
url := c.ctx.mergedParams(c.withClientKey(NewQueryParams())).applyToURL(c.ctx.subURL("requests/" + encodePathSegment(id)))
_, err := c.ctx.http.call(ctx, "DELETE", url, nil, "", defaultRequestTimeout)
_, err := c.ctx.http.call(ctx, http.MethodDelete, url, nil, "", defaultRequestTimeout)
if err != nil && !isNotFound(err) {
return err
}
Expand Down Expand Up @@ -240,7 +241,7 @@ func (c *RequestQueueClient) ProlongRequestLock(ctx context.Context, id string,
params.AddInt("lockSecs", &lockSecs).AddBool("forefront", &forefront)
c.withClientKey(params)
url := c.ctx.mergedParams(params).applyToURL(c.ctx.subURL("requests/" + encodePathSegment(id) + "/lock"))
resp, err := c.ctx.http.call(ctx, "PUT", url, nil, "", defaultRequestTimeout)
resp, err := c.ctx.http.call(ctx, http.MethodPut, url, nil, "", defaultRequestTimeout)
if err != nil {
return nil, err
}
Expand All @@ -254,7 +255,7 @@ func (c *RequestQueueClient) DeleteRequestLock(ctx context.Context, id string, f
params.AddBool("forefront", &forefront)
c.withClientKey(params)
url := c.ctx.mergedParams(params).applyToURL(c.ctx.subURL("requests/" + encodePathSegment(id) + "/lock"))
_, err := c.ctx.http.call(ctx, "DELETE", url, nil, "", defaultRequestTimeout)
_, err := c.ctx.http.call(ctx, http.MethodDelete, url, nil, "", defaultRequestTimeout)
if err != nil && !isNotFound(err) {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"io"
"math/rand"
"net/http"
"strconv"
"time"
)
Expand Down Expand Up @@ -176,7 +177,7 @@ func (c *RunClient) Charge(ctx context.Context, options RunChargeOptions) error
body := mustMarshal(map[string]any{"eventName": options.EventName, "count": count})
url := c.ctx.subURL("charge")
headers := map[string]string{chargeIdempotencyHeader: idempotencyKey}
_, err := c.ctx.http.callWithHeaders(ctx, "POST", url, body, contentTypeJSON, headers, defaultRequestTimeout)
_, err := c.ctx.http.callWithHeaders(ctx, http.MethodPost, url, body, contentTypeJSON, headers, defaultRequestTimeout)
return err
}

Expand Down
4 changes: 4 additions & 0 deletions signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const (
hmacSignatureHexLen = 30
// base62Alphabet is the alphabet (lowercase first) used to encode the truncated HMAC.
base62Alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// urlSigningSecretExtraKey is the Extra field name the API uses to expose a private
// storage's (dataset or key-value store) HMAC signing secret. Consulted by the
// dataset/key-value-store public-URL builders to decide whether a URL needs a signature.
urlSigningSecretExtraKey = "urlSigningSecretKey"
)

// createHmacSignature computes an Apify URL-signing signature, byte-for-byte compatible
Expand Down
8 changes: 5 additions & 3 deletions task.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package apify
import (
"context"
"encoding/json"
"net/http"
)

// TaskClient is a client for a specific Actor task.
Expand Down Expand Up @@ -46,8 +47,9 @@ func (c *TaskClient) Publish(ctx context.Context) (Task, error) {
// Update.
//
// The public display configuration (PublicConfig) is preserved, so the task can be published
// again without re-entering it. Requires write permission to both the task and its Actor.
// Unpublishing a task that is not published does nothing.
// again without re-entering it. Unlike Publish, Unpublish only requires write permission to
// the task itself - it succeeds even if the task's Actor is unowned or private. Unpublishing
// a task that is not published does nothing.
func (c *TaskClient) Unpublish(ctx context.Context) (Task, error) {
return c.Update(ctx, map[string]any{"isPublic": false})
}
Expand Down Expand Up @@ -129,7 +131,7 @@ func (c *TaskClient) UpdateInput(ctx context.Context, input any) (json.RawMessag
return nil, err
}
url := c.ctx.subURL("input")
resp, err := c.ctx.http.call(ctx, "PUT", url, data, contentTypeJSON, defaultRequestTimeout)
resp, err := c.ctx.http.call(ctx, http.MethodPut, url, data, contentTypeJSON, defaultRequestTimeout)
if err != nil {
return nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion user.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"net/http"
)

// UserClient is a client for accessing user data (/v2/users/{userId} or /v2/users/me).
Expand Down Expand Up @@ -61,6 +62,6 @@ func (c *UserClient) UpdateLimits(ctx context.Context, newLimits any) error {
return err
}
url := c.ctx.subURL("limits")
_, err = c.ctx.http.call(ctx, "PUT", url, data, contentTypeJSON, defaultRequestTimeout)
_, err = c.ctx.http.call(ctx, http.MethodPut, url, data, contentTypeJSON, defaultRequestTimeout)
return err
}
2 changes: 1 addition & 1 deletion version.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package apify
//
// It follows Semantic Versioning (https://semver.org/). Changes to the public
// interface (other than additive ones) are considered breaking changes.
const ClientVersion = "0.8.0"
const ClientVersion = "0.8.1"

// APISpecVersion is the version of the Apify OpenAPI specification that this
// client was generated and verified against.
Expand Down
Loading