diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fb66db..111557f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/common.go b/common.go index 00be199..8938e8e 100644 --- a/common.go +++ b/common.go @@ -2,6 +2,7 @@ package apify import ( "encoding/json" + "net/http" "net/url" "runtime" "strconv" @@ -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 { diff --git a/dataset.go b/dataset.go index ff9437a..c1a39b2 100644 --- a/dataset.go +++ b/dataset.go @@ -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) } diff --git a/key_value_store.go b/key_value_store.go index 45eeaee..f8a5bc6 100644 --- a/key_value_store.go +++ b/key_value_store.go @@ -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) } @@ -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) } diff --git a/request_queue.go b/request_queue.go index d2e2e99..ea76524 100644 --- a/request_queue.go +++ b/request_queue.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "net/http" ) // ListRequestsOptions configures [RequestQueueClient.ListRequests]. @@ -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 } @@ -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 } @@ -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 } @@ -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 } diff --git a/run.go b/run.go index f4b5637..9224e1b 100644 --- a/run.go +++ b/run.go @@ -4,6 +4,7 @@ import ( "context" "io" "math/rand" + "net/http" "strconv" "time" ) @@ -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 } diff --git a/signature.go b/signature.go index 8beb905..fab58d5 100644 --- a/signature.go +++ b/signature.go @@ -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 diff --git a/task.go b/task.go index 6a958ae..747de65 100644 --- a/task.go +++ b/task.go @@ -3,6 +3,7 @@ package apify import ( "context" "encoding/json" + "net/http" ) // TaskClient is a client for a specific Actor task. @@ -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}) } @@ -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 } diff --git a/user.go b/user.go index fdd6c17..88c0fcf 100644 --- a/user.go +++ b/user.go @@ -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). @@ -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 } diff --git a/version.go b/version.go index c5653f4..1e88da2 100644 --- a/version.go +++ b/version.go @@ -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.