Repository: hetznercloud/hcloud-go
Pull request #617 (2025-03-12 13:20:42)

refactor: use generic request for `AllWithOpts` calls
Uses the new generic requests function to reduce the code duplication for the `AllWithOpts` calls.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) Report
Attention: Patch coverage is `75.51020%` with `12 lines` in your changes missing coverage. Please review.
> Project coverage is 69.24%. Comparing base [(`3c3d2e4`)](https://app.codecov.io/gh/hetznercloud/hcloud-go/commit/3c3d2e4cd865d342f2353407d06fd32f617b25d9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) to head [(`3ea612c`)](https://app.codecov.io/gh/hetznercloud/hcloud-go/commit/3ea612c42a45fd27a786685d5348320869e05287?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).

| [Files with missing lines](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | Patch % | Lines |
|---|---|---|
| [hcloud/certificate.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&filepath=hcloud%2Fcertificate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL2NlcnRpZmljYXRlLmdv) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [hcloud/firewall.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&filepath=hcloud%2Ffirewall.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL2ZpcmV3YWxsLmdv) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [hcloud/load\_balancer.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&filepath=hcloud%2Fload_balancer.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL2xvYWRfYmFsYW5jZXIuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [hcloud/network.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&filepath=hcloud%2Fnetwork.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL25ldHdvcmsuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [hcloud/placement\_group.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&filepath=hcloud%2Fplacement_group.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL3BsYWNlbWVudF9ncm91cC5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |
| [hcloud/volume.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&filepath=hcloud%2Fvolume.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL3ZvbHVtZS5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #617      +/-   ##
==========================================
+ Coverage   67.74%   69.24%   +1.50%     
==========================================
  Files          48       49       +1     
  Lines        5020     4852     -168     
==========================================
- Hits         3401     3360      -41     
+ Misses       1198     1093     -105     
+ Partials      421      399      -22     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/617?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/hcloud/action.go b/hcloud/action.go
index fb720db..d8c3c39 100644
--- a/hcloud/action.go
+++ b/hcloud/action.go
@@ -182,20 +182,8 @@ func (c *ResourceActionClient) List(ctx context.Context, opts ActionListOpts) ([
 
 // All returns all actions for the given options.
 func (c *ResourceActionClient) All(ctx context.Context, opts ActionListOpts) ([]*Action, error) {
-	allActions := []*Action{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Action, *Response, error) {
 		opts.Page = page
-		actions, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allActions = append(allActions, actions...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allActions, nil
 }
diff --git a/hcloud/certificate.go b/hcloud/certificate.go
index 12c84d3..65c0cd2 100644
--- a/hcloud/certificate.go
+++ b/hcloud/certificate.go
@@ -174,22 +174,10 @@ func (c *CertificateClient) All(ctx context.Context) ([]*Certificate, error) {
 
 // AllWithOpts returns all Certificates for the given options.
 func (c *CertificateClient) AllWithOpts(ctx context.Context, opts CertificateListOpts) ([]*Certificate, error) {
-	allCertificates := []*Certificate{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Certificate, *Response, error) {
 		opts.Page = page
-		Certificates, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allCertificates = append(allCertificates, Certificates...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allCertificates, nil
 }
 
 // CertificateCreateOpts specifies options for creating a new Certificate.
diff --git a/hcloud/client.go b/hcloud/client.go
index 9abf631..baac293 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -325,22 +325,6 @@ func (c *Client) Do(req *http.Request, v any) (*Response, error) {
 	return c.handler.Do(req, v)
 }
 
-func (c *Client) all(f func(int) (*Response, error)) error {
-	var (
-		page = 1
-	)
-	for {
-		resp, err := f(page)
-		if err != nil {
-			return err
-		}
-		if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 {
-			return nil
-		}
-		page = resp.Meta.Pagination.NextPage
-	}
-}
-
 func (c *Client) buildUserAgent() {
 	switch {
 	case c.applicationName != "" && c.applicationVersion != "":
diff --git a/hcloud/client_helper.go b/hcloud/client_helper.go
index 8c765f6..79fa712 100644
--- a/hcloud/client_helper.go
+++ b/hcloud/client_helper.go
@@ -10,3 +10,23 @@ func allFromSchemaFunc[T, V any](all []T, fn func(T) V) []V {
 
 	return result
 }
+
+// iterPages fetches each pages using the list function, and returns the result.
+func iterPages[T any](listFn func(int) ([]*T, *Response, error)) ([]*T, error) {
+	page := 1
+	result := []*T{}
+
+	for {
+		pageResult, resp, err := listFn(page)
+		if err != nil {
+			return nil, err
+		}
+
+		result = append(result, pageResult...)
+
+		if resp.Meta.Pagination == nil || resp.Meta.Pagination.NextPage == 0 {
+			return result, nil
+		}
+		page = resp.Meta.Pagination.NextPage
+	}
+}
diff --git a/hcloud/datacenter.go b/hcloud/datacenter.go
index d96317c..14f2779 100644
--- a/hcloud/datacenter.go
+++ b/hcloud/datacenter.go
@@ -106,20 +106,8 @@ func (c *DatacenterClient) All(ctx context.Context) ([]*Datacenter, error) {
 
 // AllWithOpts returns all datacenters for the given options.
 func (c *DatacenterClient) AllWithOpts(ctx context.Context, opts DatacenterListOpts) ([]*Datacenter, error) {
-	allDatacenters := []*Datacenter{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Datacenter, *Response, error) {
 		opts.Page = page
-		datacenters, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allDatacenters = append(allDatacenters, datacenters...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allDatacenters, nil
 }
diff --git a/hcloud/firewall.go b/hcloud/firewall.go
index 0c70530..d39a15e 100644
--- a/hcloud/firewall.go
+++ b/hcloud/firewall.go
@@ -173,22 +173,10 @@ func (c *FirewallClient) All(ctx context.Context) ([]*Firewall, error) {
 
 // AllWithOpts returns all Firewalls for the given options.
 func (c *FirewallClient) AllWithOpts(ctx context.Context, opts FirewallListOpts) ([]*Firewall, error) {
-	allFirewalls := []*Firewall{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Firewall, *Response, error) {
 		opts.Page = page
-		firewalls, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allFirewalls = append(allFirewalls, firewalls...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allFirewalls, nil
 }
 
 // FirewallCreateOpts specifies options for creating a new Firewall.
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index 4e00147..aef4682 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -174,22 +174,10 @@ func (c *FloatingIPClient) All(ctx context.Context) ([]*FloatingIP, error) {
 
 // AllWithOpts returns all Floating IPs for the given options.
 func (c *FloatingIPClient) AllWithOpts(ctx context.Context, opts FloatingIPListOpts) ([]*FloatingIP, error) {
-	allFloatingIPs := []*FloatingIP{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*FloatingIP, *Response, error) {
 		opts.Page = page
-		floatingIPs, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allFloatingIPs = append(allFloatingIPs, floatingIPs...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allFloatingIPs, nil
 }
 
 // FloatingIPCreateOpts specifies options for creating a Floating IP.
diff --git a/hcloud/image.go b/hcloud/image.go
index ba20327..cf1e31c 100644
--- a/hcloud/image.go
+++ b/hcloud/image.go
@@ -214,22 +214,10 @@ func (c *ImageClient) All(ctx context.Context) ([]*Image, error) {
 
 // AllWithOpts returns all images for the given options.
 func (c *ImageClient) AllWithOpts(ctx context.Context, opts ImageListOpts) ([]*Image, error) {
-	allImages := []*Image{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Image, *Response, error) {
 		opts.Page = page
-		images, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allImages = append(allImages, images...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allImages, nil
 }
 
 // Delete deletes an image.
diff --git a/hcloud/iso.go b/hcloud/iso.go
index 41a438a..8bc3872 100644
--- a/hcloud/iso.go
+++ b/hcloud/iso.go
@@ -132,20 +132,8 @@ func (c *ISOClient) All(ctx context.Context) ([]*ISO, error) {
 
 // AllWithOpts returns all ISOs for the given options.
 func (c *ISOClient) AllWithOpts(ctx context.Context, opts ISOListOpts) ([]*ISO, error) {
-	allISOs := []*ISO{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*ISO, *Response, error) {
 		opts.Page = page
-		isos, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allISOs = append(allISOs, isos...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allISOs, nil
 }
diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go
index 8215b8a..97b20ce 100644
--- a/hcloud/load_balancer.go
+++ b/hcloud/load_balancer.go
@@ -320,22 +320,10 @@ func (c *LoadBalancerClient) All(ctx context.Context) ([]*LoadBalancer, error) {
 
 // AllWithOpts returns all Load Balancers for the given options.
 func (c *LoadBalancerClient) AllWithOpts(ctx context.Context, opts LoadBalancerListOpts) ([]*LoadBalancer, error) {
-	allLoadBalancers := []*LoadBalancer{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*LoadBalancer, *Response, error) {
 		opts.Page = page
-		LoadBalancers, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allLoadBalancers = append(allLoadBalancers, LoadBalancers...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allLoadBalancers, nil
 }
 
 // LoadBalancerUpdateOpts specifies options for updating a Load Balancer.
diff --git a/hcloud/load_balancer_type.go b/hcloud/load_balancer_type.go
index 55197b8..7fad4ae 100644
--- a/hcloud/load_balancer_type.go
+++ b/hcloud/load_balancer_type.go
@@ -103,20 +103,8 @@ func (c *LoadBalancerTypeClient) All(ctx context.Context) ([]*LoadBalancerType,
 
 // AllWithOpts returns all Load Balancer types for the given options.
 func (c *LoadBalancerTypeClient) AllWithOpts(ctx context.Context, opts LoadBalancerTypeListOpts) ([]*LoadBalancerType, error) {
-	allLoadBalancerTypes := []*LoadBalancerType{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*LoadBalancerType, *Response, error) {
 		opts.Page = page
-		LoadBalancerTypes, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allLoadBalancerTypes = append(allLoadBalancerTypes, LoadBalancerTypes...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allLoadBalancerTypes, nil
 }
diff --git a/hcloud/location.go b/hcloud/location.go
index a25654b..089894b 100644
--- a/hcloud/location.go
+++ b/hcloud/location.go
@@ -102,20 +102,8 @@ func (c *LocationClient) All(ctx context.Context) ([]*Location, error) {
 
 // AllWithOpts returns all locations for the given options.
 func (c *LocationClient) AllWithOpts(ctx context.Context, opts LocationListOpts) ([]*Location, error) {
-	allLocations := []*Location{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Location, *Response, error) {
 		opts.Page = page
-		locations, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allLocations = append(allLocations, locations...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allLocations, nil
 }
diff --git a/hcloud/network.go b/hcloud/network.go
index 7dce773..318ea13 100644
--- a/hcloud/network.go
+++ b/hcloud/network.go
@@ -164,22 +164,10 @@ func (c *NetworkClient) All(ctx context.Context) ([]*Network, error) {
 
 // AllWithOpts returns all networks for the given options.
 func (c *NetworkClient) AllWithOpts(ctx context.Context, opts NetworkListOpts) ([]*Network, error) {
-	allNetworks := []*Network{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Network, *Response, error) {
 		opts.Page = page
-		Networks, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allNetworks = append(allNetworks, Networks...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allNetworks, nil
 }
 
 // Delete deletes a network.
diff --git a/hcloud/placement_group.go b/hcloud/placement_group.go
index e3556b9..7b86c69 100644
--- a/hcloud/placement_group.go
+++ b/hcloud/placement_group.go
@@ -125,22 +125,10 @@ func (c *PlacementGroupClient) All(ctx context.Context) ([]*PlacementGroup, erro
 
 // AllWithOpts returns all PlacementGroups for the given options.
 func (c *PlacementGroupClient) AllWithOpts(ctx context.Context, opts PlacementGroupListOpts) ([]*PlacementGroup, error) {
-	allPlacementGroups := []*PlacementGroup{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*PlacementGroup, *Response, error) {
 		opts.Page = page
-		placementGroups, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allPlacementGroups = append(allPlacementGroups, placementGroups...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allPlacementGroups, nil
 }
 
 // PlacementGroupCreateOpts specifies options for creating a new PlacementGroup.
diff --git a/hcloud/primary_ip.go b/hcloud/primary_ip.go
index 268af19..b67a6f4 100644
--- a/hcloud/primary_ip.go
+++ b/hcloud/primary_ip.go
@@ -258,22 +258,10 @@ func (c *PrimaryIPClient) All(ctx context.Context) ([]*PrimaryIP, error) {
 
 // AllWithOpts returns all Primary IPs for the given options.
 func (c *PrimaryIPClient) AllWithOpts(ctx context.Context, opts PrimaryIPListOpts) ([]*PrimaryIP, error) {
-	allPrimaryIPs := []*PrimaryIP{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*PrimaryIP, *Response, error) {
 		opts.Page = page
-		primaryIPs, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allPrimaryIPs = append(allPrimaryIPs, primaryIPs...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allPrimaryIPs, nil
 }
 
 // Create creates a Primary IP.
diff --git a/hcloud/server.go b/hcloud/server.go
index b23f4c4..eadf645 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -279,22 +279,10 @@ func (c *ServerClient) All(ctx context.Context) ([]*Server, error) {
 
 // AllWithOpts returns all servers for the given options.
 func (c *ServerClient) AllWithOpts(ctx context.Context, opts ServerListOpts) ([]*Server, error) {
-	allServers := []*Server{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Server, *Response, error) {
 		opts.Page = page
-		servers, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allServers = append(allServers, servers...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allServers, nil
 }
 
 // ServerCreateOpts specifies options for creating a new server.
diff --git a/hcloud/server_type.go b/hcloud/server_type.go
index 36a6af0..00c06a5 100644
--- a/hcloud/server_type.go
+++ b/hcloud/server_type.go
@@ -131,20 +131,8 @@ func (c *ServerTypeClient) All(ctx context.Context) ([]*ServerType, error) {
 
 // AllWithOpts returns all server types for the given options.
 func (c *ServerTypeClient) AllWithOpts(ctx context.Context, opts ServerTypeListOpts) ([]*ServerType, error) {
-	allServerTypes := []*ServerType{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*ServerType, *Response, error) {
 		opts.Page = page
-		serverTypes, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allServerTypes = append(allServerTypes, serverTypes...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allServerTypes, nil
 }
diff --git a/hcloud/ssh_key.go b/hcloud/ssh_key.go
index 57716d5..4c20e32 100644
--- a/hcloud/ssh_key.go
+++ b/hcloud/ssh_key.go
@@ -120,22 +120,10 @@ func (c *SSHKeyClient) All(ctx context.Context) ([]*SSHKey, error) {
 
 // AllWithOpts returns all SSH keys with the given options.
 func (c *SSHKeyClient) AllWithOpts(ctx context.Context, opts SSHKeyListOpts) ([]*SSHKey, error) {
-	allSSHKeys := []*SSHKey{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*SSHKey, *Response, error) {
 		opts.Page = page
-		sshKeys, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allSSHKeys = append(allSSHKeys, sshKeys...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allSSHKeys, nil
 }
 
 // SSHKeyCreateOpts specifies parameters for creating a SSH key.
diff --git a/hcloud/volume.go b/hcloud/volume.go
index 4231fdc..eaf9f36 100644
--- a/hcloud/volume.go
+++ b/hcloud/volume.go
@@ -138,22 +138,10 @@ func (c *VolumeClient) All(ctx context.Context) ([]*Volume, error) {
 
 // AllWithOpts returns all volumes with the given options.
 func (c *VolumeClient) AllWithOpts(ctx context.Context, opts VolumeListOpts) ([]*Volume, error) {
-	allVolumes := []*Volume{}
-
-	err := c.client.all(func(page int) (*Response, error) {
+	return iterPages(func(page int) ([]*Volume, *Response, error) {
 		opts.Page = page
-		volumes, resp, err := c.List(ctx, opts)
-		if err != nil {
-			return resp, err
-		}
-		allVolumes = append(allVolumes, volumes...)
-		return resp, nil
+		return c.List(ctx, opts)
 	})
-	if err != nil {
-		return nil, err
-	}
-
-	return allVolumes, nil
 }
 
 // VolumeCreateOpts specifies parameters for creating a volume.


Test patch:
diff --git a/hcloud/client_helper_test.go b/hcloud/client_helper_test.go
new file mode 100644
index 0000000..84d27fe
--- /dev/null
+++ b/hcloud/client_helper_test.go
@@ -0,0 +1,32 @@
+package hcloud
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIterPages(t *testing.T) {
+	t.Run("succeed", func(t *testing.T) {
+		result, err := iterPages(func(page int) ([]*int, *Response, error) {
+			if page < 4 {
+				return []*int{Ptr(page)}, &Response{Meta: Meta{Pagination: &Pagination{NextPage: page + 1}}}, nil
+			}
+			return []*int{Ptr(page)}, &Response{}, nil
+		})
+		require.NoError(t, err)
+		require.Equal(t, []*int{Ptr(1), Ptr(2), Ptr(3), Ptr(4)}, result)
+	})
+
+	t.Run("failed", func(t *testing.T) {
+		result, err := iterPages(func(page int) ([]*int, *Response, error) {
+			if page < 4 {
+				return []*int{Ptr(page)}, &Response{Meta: Meta{Pagination: &Pagination{NextPage: page + 1}}}, nil
+			}
+			return nil, &Response{}, fmt.Errorf("failure")
+		})
+		require.EqualError(t, err, "failure")
+		require.Nil(t, result)
+	})
+}
diff --git a/hcloud/client_test.go b/hcloud/client_test.go
index 743699e..e2253d0 100644
--- a/hcloud/client_test.go
+++ b/hcloud/client_test.go
@@ -208,81 +208,6 @@ func TestClientMetaNonJSON(t *testing.T) {
 	}
 }
 
-func TestClientAll(t *testing.T) {
-	env := newTestEnv()
-	defer env.Teardown()
-
-	var (
-		ctx          = context.Background()
-		conflicting  bool
-		expectedPage = 1
-	)
-
-	env.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
-		w.Header().Set("Content-Type", "application/json")
-
-		respBody := schema.MetaResponse{
-			Meta: schema.Meta{
-				Pagination: &schema.MetaPagination{
-					LastPage:     3,
-					PerPage:      1,
-					TotalEntries: 3,
-				},
-			},
-		}
-
-		switch page := r.URL.Query().Get("page"); page {
-		case "", "1":
-			respBody.Meta.Pagination.Page = 1
-			respBody.Meta.Pagination.NextPage = 2
-		case "2":
-			if !conflicting {
-				conflicting = true
-				w.WriteHeader(http.StatusConflict)
-				json.NewEncoder(w).Encode(schema.ErrorResponse{
-					Error: schema.Error{
-						Code:    string(ErrorCodeConflict),
-						Message: "conflict",
-					},
-				})
-				return
-			}
-			respBody.Meta.Pagination.Page = 2
-			respBody.Meta.Pagination.PreviousPage = 1
-			respBody.Meta.Pagination.NextPage = 3
-		case "3":
-			respBody.Meta.Pagination.Page = 3
-			respBody.Meta.Pagination.PreviousPage = 2
-		default:
-			t.Errorf("bad page: %q", page)
-		}
-
-		json.NewEncoder(w).Encode(respBody)
-	})
-
-	_ = env.Client.all(func(page int) (*Response, error) {
-		if page != expectedPage {
-			t.Fatalf("expected page %d, but called for %d", expectedPage, page)
-		}
-
-		path := fmt.Sprintf("/?page=%d&per_page=1", page)
-		req, err := env.Client.NewRequest(ctx, "GET", path, nil)
-		if err != nil {
-			return nil, err
-		}
-		resp, err := env.Client.Do(req, nil)
-		if err != nil {
-			return resp, err
-		}
-		expectedPage++
-		return resp, err
-	})
-
-	if expectedPage != 4 {
-		t.Errorf("expected to have walked through 3 pages, but walked through %d pages", expectedPage-1)
-	}
-}
-
 func TestClientDo(t *testing.T) {
 	env := newTestEnv()
 	defer env.Teardown()