From c03341e2dcba64f82e0115417e02c630cf2d4645 Mon Sep 17 00:00:00 2001 From: Peter Landoll Date: Fri, 20 Oct 2023 13:02:50 -0400 Subject: [PATCH 1/2] feat: add keyset pagination options --- examples/pagination.go | 43 ++++++++ gitlab.go | 67 ++++++++++++- gitlab_test.go | 167 ++++++++++++++++++++++++++++++++ invites_test.go | 4 +- notifications_test.go | 2 +- project_vulnerabilities_test.go | 2 +- projects_test.go | 22 ++--- request_options.go | 25 +++++ request_options_test.go | 21 ++++ 9 files changed, 336 insertions(+), 17 deletions(-) diff --git a/examples/pagination.go b/examples/pagination.go index 3d4d62f94..9da6f815c 100644 --- a/examples/pagination.go +++ b/examples/pagination.go @@ -56,3 +56,46 @@ func pagination() { opt.Page = resp.NextPage } } + +func keysetPagination() { + git, err := gitlab.NewClient("yourtokengoeshere") + if err != nil { + log.Fatal(err) + } + + opt := &gitlab.ListProjectsOptions{ + ListOptions: gitlab.ListOptions{ + OrderBy: "id", + Pagination: "keyset", + PerPage: 5, + Sort: "asc", + }, + Owned: gitlab.Bool(true), + } + + options := []gitlab.RequestOptionFunc{} + + for { + // Get the first page with projects. + ps, resp, err := git.Projects.ListProjects(opt, options...) + if err != nil { + log.Fatal(err) + } + + // List all the projects we've found so far. + for _, p := range ps { + log.Printf("Found project: %s", p.Name) + } + + // Exit the loop when we've seen all pages. + if resp.NextLink == "" { + break + } + + // Set all query parameters in the next request to values in the + // returned parameters. This could go along with any existing options. + options = []gitlab.RequestOptionFunc{ + gitlab.WithKeysetPaginationParameters(resp.NextLink), + } + } +} diff --git a/gitlab.go b/gitlab.go index 88d55057c..fa1a1e430 100644 --- a/gitlab.go +++ b/gitlab.go @@ -227,11 +227,20 @@ type Client struct { // ListOptions specifies the optional parameters to various List methods that // support pagination. type ListOptions struct { - // For paginated result sets, page of results to retrieve. + // For keyset-based paginated result sets, name of the column by which to order + OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` + + // For offset-based paginated result sets, page of results to retrieve. Page int `url:"page,omitempty" json:"page,omitempty"` - // For paginated result sets, the number of results to include per page. + // For keyset-based paginated result sets, the value must be `"keyset"` + Pagination string `url:"pagination,omitempty" json:"pagination,omitempty"` + + // For offset-based and keyset-based paginated result sets, the number of results to include per page. PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"` + + // For keyset-based paginated result sets, sort order (`"asc"`` or `"desc"`) + Sort string `url:"sort,omitempty" json:"sort,omitempty"` } // RateLimiter describes the interface that all (custom) rate limiters must implement. @@ -720,12 +729,18 @@ type Response struct { CurrentPage int NextPage int PreviousPage int + + PreviousLink string + NextLink string + FirstLink string + LastLink string } // newResponse creates a new Response for the provided http.Response. func newResponse(r *http.Response) *Response { response := &Response{Response: r} response.populatePageValues() + response.populateLinkValues() return response } @@ -736,6 +751,11 @@ const ( xPage = "X-Page" xNextPage = "X-Next-Page" xPrevPage = "X-Prev-Page" + + linkPrev = "prev" + linkNext = "next" + linkFirst = "first" + linkLast = "last" ) // populatePageValues parses the HTTP Link response headers and populates the @@ -761,6 +781,16 @@ func (r *Response) populatePageValues() { } } +func (r *Response) populateLinkValues() { + if link := r.Header.Get("Link"); link != "" { + linkHeaders, _ := parseLinkHeader(link) + r.PreviousLink = linkHeaders[linkPrev] + r.NextLink = linkHeaders[linkNext] + r.FirstLink = linkHeaders[linkFirst] + r.LastLink = linkHeaders[linkLast] + } +} + // Do sends an API request and returns the API response. The API response is // JSON decoded and stored in the value pointed to by v, or returned as an // error if an API error has occurred. If v implements the io.Writer @@ -883,6 +913,39 @@ func parseID(id interface{}) (string, error) { } } +// parseLinkHeader splits a "Link" HTTP header into its "link" and "rel" +// components, formatted in a comma-joined string of `<%s>; rel="%s"` +// +// GitLab API docs: +// https://docs.gitlab.com/ee/api/rest/#pagination-link-header +func parseLinkHeader(link string) (map[string]string, error) { + result := make(map[string]string) + + if len(link) == 0 { + return result, nil + } + + // each link will be of the form: <%s>; rel="%s" + for _, link := range strings.Split(link, ",") { + pieces := strings.Split(link, ";") + if len(pieces) < 2 { + return result, fmt.Errorf("invalid format for link header component: %s", link) + } + linkType := strings.Trim(strings.Split(pieces[1], "=")[1], "\"") + // remove surrounding angle brackets _and_ whitespace + linkValue := strings.Trim(pieces[0], "< >") + + switch linkType { + case linkPrev, linkNext, linkFirst, linkLast: + result[linkType] = linkValue + default: + return result, fmt.Errorf("invalid link type %s", linkType) + } + } + + return result, nil +} + // Helper function to escape a project identifier. func PathEscape(s string) string { return strings.ReplaceAll(url.PathEscape(s), ".", "%2E") diff --git a/gitlab_test.go b/gitlab_test.go index d12defe7d..7dfd63e68 100644 --- a/gitlab_test.go +++ b/gitlab_test.go @@ -20,11 +20,13 @@ import ( "bytes" "context" "errors" + "fmt" "io" "log" "net/http" "net/http/httptest" "os" + "reflect" "strings" "testing" "time" @@ -236,3 +238,168 @@ func TestPathEscape(t *testing.T) { t.Errorf("Expected: %s, got %s", want, got) } } + +func TestParseLinkHeaderEmpty(t *testing.T) { + h := `` + want := map[string]string{} + got, err := parseLinkHeader(h) + if err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + + if !reflect.DeepEqual(want, got) { + t.Errorf("Expected %s, got %s", want, got) + } +} + +func TestPaginationPopulatePageValuesEmpty(t *testing.T) { + wantPageHeaders := map[string]int{ + xTotal: 0, + xTotalPages: 0, + xPerPage: 0, + xPage: 0, + xNextPage: 0, + xPrevPage: 0, + } + wantLinkHeaders := map[string]string{ + linkPrev: "", + linkNext: "", + linkFirst: "", + linkLast: "", + } + + r := newResponse(&http.Response{ + Header: http.Header{}, + }) + + gotPageHeaders := map[string]int{ + xTotal: r.TotalItems, + xTotalPages: r.TotalPages, + xPerPage: r.ItemsPerPage, + xPage: r.CurrentPage, + xNextPage: r.NextPage, + xPrevPage: r.PreviousPage, + } + for k, v := range wantPageHeaders { + if v != gotPageHeaders[k] { + t.Errorf("For %s, expected %d, got %d", k, v, gotPageHeaders[k]) + } + } + + gotLinkHeaders := map[string]string{ + linkPrev: r.PreviousLink, + linkNext: r.NextLink, + linkFirst: r.FirstLink, + linkLast: r.LastLink, + } + for k, v := range wantLinkHeaders { + if v != gotLinkHeaders[k] { + t.Errorf("For %s, expected %s, got %s", k, v, gotLinkHeaders[k]) + } + } +} + +func TestPaginationPopulatePageValuesOffset(t *testing.T) { + wantPageHeaders := map[string]int{ + xTotal: 100, + xTotalPages: 5, + xPerPage: 20, + xPage: 2, + xNextPage: 3, + xPrevPage: 1, + } + wantLinkHeaders := map[string]string{ + linkPrev: "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3", + linkNext: "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3", + linkFirst: "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=1&per_page=3", + linkLast: "https://gitlab.example.com/api/v4/projects/8/issues/8/notes?page=3&per_page=3", + } + + h := http.Header{} + for k, v := range wantPageHeaders { + h.Add(k, fmt.Sprint(v)) + } + var linkHeaderComponents []string + for k, v := range wantLinkHeaders { + if v != "" { + linkHeaderComponents = append(linkHeaderComponents, fmt.Sprintf("<%s>; rel=\"%s\"", v, k)) + } + } + h.Add("Link", strings.Join(linkHeaderComponents, ", ")) + + r := newResponse(&http.Response{ + Header: h, + }) + + gotPageHeaders := map[string]int{ + xTotal: r.TotalItems, + xTotalPages: r.TotalPages, + xPerPage: r.ItemsPerPage, + xPage: r.CurrentPage, + xNextPage: r.NextPage, + xPrevPage: r.PreviousPage, + } + for k, v := range wantPageHeaders { + if v != gotPageHeaders[k] { + t.Errorf("For %s, expected %d, got %d", k, v, gotPageHeaders[k]) + } + } + + gotLinkHeaders := map[string]string{ + linkPrev: r.PreviousLink, + linkNext: r.NextLink, + linkFirst: r.FirstLink, + linkLast: r.LastLink, + } + for k, v := range wantLinkHeaders { + if v != gotLinkHeaders[k] { + t.Errorf("For %s, expected %s, got %s", k, v, gotLinkHeaders[k]) + } + } +} + +func TestPaginationPopulatePageValuesKeyset(t *testing.T) { + wantPageHeaders := map[string]int{ + xTotal: 0, + xTotalPages: 0, + xPerPage: 0, + xPage: 0, + xNextPage: 0, + xPrevPage: 0, + } + wantLinkHeaders := map[string]string{ + linkPrev: "", + linkFirst: "", + linkLast: "", + } + + h := http.Header{} + for k, v := range wantPageHeaders { + h.Add(k, fmt.Sprint(v)) + } + var linkHeaderComponents []string + for k, v := range wantLinkHeaders { + if v != "" { + linkHeaderComponents = append(linkHeaderComponents, fmt.Sprintf("<%s>; rel=\"%s\"", v, k)) + } + } + h.Add("Link", strings.Join(linkHeaderComponents, ", ")) + + r := newResponse(&http.Response{ + Header: h, + }) + + gotPageHeaders := map[string]int{ + xTotal: r.TotalItems, + xTotalPages: r.TotalPages, + xPerPage: r.ItemsPerPage, + xPage: r.CurrentPage, + xNextPage: r.NextPage, + xPrevPage: r.PreviousPage, + } + for k, v := range wantPageHeaders { + if v != gotPageHeaders[k] { + t.Errorf("For %s, expected %d, got %d", k, v, gotPageHeaders[k]) + } + } +} diff --git a/invites_test.go b/invites_test.go index 56b27c6b6..d3178a274 100644 --- a/invites_test.go +++ b/invites_test.go @@ -16,7 +16,7 @@ func TestListGroupPendingInvites(t *testing.T) { }) opt := &ListPendingInvitationsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, } projects, _, err := client.Invites.ListPendingGroupInvitations("test", opt) @@ -85,7 +85,7 @@ func TestListProjectPendingInvites(t *testing.T) { }) opt := &ListPendingInvitationsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, } projects, _, err := client.Invites.ListPendingProjectInvitations("test", opt) diff --git a/notifications_test.go b/notifications_test.go index 1b971fcaf..a05651b1b 100644 --- a/notifications_test.go +++ b/notifications_test.go @@ -86,7 +86,7 @@ func TestGetProjectSettings(t *testing.T) { } want := &NotificationSettings{ - Level: 5, //custom + Level: 5, // custom Events: &NotificationEvents{ NewEpic: true, NewNote: true, diff --git a/project_vulnerabilities_test.go b/project_vulnerabilities_test.go index 7f21bc4b0..5bf0b8dd5 100644 --- a/project_vulnerabilities_test.go +++ b/project_vulnerabilities_test.go @@ -32,7 +32,7 @@ func TestListProjectVulnerabilities(t *testing.T) { }) opt := &ListProjectVulnerabilitiesOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, } projectVulnerabilities, _, err := client.ProjectVulnerabilities.ListProjectVulnerabilities(1, opt) diff --git a/projects_test.go b/projects_test.go index cf4965a75..c3e9b9a19 100644 --- a/projects_test.go +++ b/projects_test.go @@ -39,7 +39,7 @@ func TestListProjects(t *testing.T) { }) opt := &ListProjectsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Archived: Bool(true), OrderBy: String("name"), Sort: String("asc"), @@ -68,7 +68,7 @@ func TestListUserProjects(t *testing.T) { }) opt := &ListProjectsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Archived: Bool(true), OrderBy: String("name"), Sort: String("asc"), @@ -97,7 +97,7 @@ func TestListUserContributedProjects(t *testing.T) { }) opt := &ListProjectsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Archived: Bool(true), OrderBy: String("name"), Sort: String("asc"), @@ -126,7 +126,7 @@ func TestListUserStarredProjects(t *testing.T) { }) opt := &ListProjectsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Archived: Bool(true), OrderBy: String("name"), Sort: String("asc"), @@ -156,7 +156,7 @@ func TestListProjectsUsersByID(t *testing.T) { }) opt := &ListProjectUserOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Search: String("query"), } @@ -181,7 +181,7 @@ func TestListProjectsUsersByName(t *testing.T) { }) opt := &ListProjectUserOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Search: String("query"), } @@ -206,7 +206,7 @@ func TestListProjectsGroupsByID(t *testing.T) { }) opt := &ListProjectGroupOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Search: String("query"), } @@ -231,7 +231,7 @@ func TestListProjectsGroupsByName(t *testing.T) { }) opt := &ListProjectGroupOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Search: String("query"), } @@ -255,7 +255,7 @@ func TestListOwnedProjects(t *testing.T) { }) opt := &ListProjectsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Archived: Bool(true), OrderBy: String("name"), Sort: String("asc"), @@ -285,7 +285,7 @@ func TestListStarredProjects(t *testing.T) { }) opt := &ListProjectsOptions{ - ListOptions: ListOptions{2, 3}, + ListOptions: ListOptions{Page: 2, PerPage: 3}, Archived: Bool(true), OrderBy: String("name"), Sort: String("asc"), @@ -572,7 +572,7 @@ func TestListProjectForks(t *testing.T) { }) opt := &ListProjectsOptions{} - opt.ListOptions = ListOptions{2, 3} + opt.ListOptions = ListOptions{Page: 2, PerPage: 3} opt.Archived = Bool(true) opt.OrderBy = String("name") opt.Sort = String("asc") diff --git a/request_options.go b/request_options.go index bc01b9180..13973ba56 100644 --- a/request_options.go +++ b/request_options.go @@ -18,6 +18,7 @@ package gitlab import ( "context" + "net/url" retryablehttp "github.com/hashicorp/go-retryablehttp" ) @@ -52,6 +53,30 @@ func WithHeaders(headers map[string]string) RequestOptionFunc { } } +// WithKeysetPaginationParameters takes a "next" link from the Link header of a +// response to a keyset-based paginated request, and modifies the values of +// each query parameter in the request with its corresponding parameter from +// the response +func WithKeysetPaginationParameters(rawNextLink string) RequestOptionFunc { + return func(req *retryablehttp.Request) error { + nextUrl, err := url.Parse(rawNextLink) + if err != nil { + return err + } + params := nextUrl.Query() + // must make a copy of the query inside the func's scope + q := req.URL.Query() + for k, values := range params { + q.Del(k) + for _, v := range values { + q.Add(k, v) + } + } + req.URL.RawQuery = q.Encode() + return nil + } +} + // WithSudo takes either a username or user ID and sets the SUDO request header. func WithSudo(uid interface{}) RequestOptionFunc { return func(req *retryablehttp.Request) error { diff --git a/request_options_test.go b/request_options_test.go index 9e6a3fdfa..88e0aef29 100644 --- a/request_options_test.go +++ b/request_options_test.go @@ -19,6 +19,7 @@ import ( "testing" "time" + "github.com/hashicorp/go-retryablehttp" "github.com/stretchr/testify/assert" ) @@ -161,3 +162,23 @@ func TestWithHeaders(t *testing.T) { _, err = client.Do(req, nil) assert.NoError(t, err) } + +func TestWithKeysetPaginationParameters(t *testing.T) { + req, err := retryablehttp.NewRequest("GET", "https://gitlab.example.com/api/v4/groups?pagination=keyset&per_page=50&order_by=name&sort=asc", nil) + assert.NoError(t, err) + + linkNext := "https://gitlab.example.com/api/v4/groups?pagination=keyset&per_page=50&order_by=name&sort=asc&cursor=eyJuYW1lIjoiRmxpZ2h0anMiLCJpZCI6IjI2IiwiX2tkIjoibiJ9" + + err = WithKeysetPaginationParameters(linkNext)(req) + assert.NoError(t, err) + + values := req.URL.Query() + // Ensure all original parameters remain + assert.Equal(t, "keyset", values.Get("pagination")) + assert.Equal(t, "50", values.Get("per_page")) + assert.Equal(t, "name", values.Get("order_by")) + assert.Equal(t, "asc", values.Get("sort")) + + // Ensure cursor gets properly pulled from "next link" header + assert.Equal(t, "eyJuYW1lIjoiRmxpZ2h0anMiLCJpZCI6IjI2IiwiX2tkIjoibiJ9", values.Get("cursor")) +} From 5bd3fc74a9ca04206b78329ccee0ffa2ba48b25e Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Mon, 13 Nov 2023 09:51:52 +0100 Subject: [PATCH 2/2] Small tweaks, nothing exiting :) --- gitlab.go | 77 +++++++++++++++++----------------------------- gitlab_test.go | 14 --------- request_options.go | 13 +++----- 3 files changed, 33 insertions(+), 71 deletions(-) diff --git a/gitlab.go b/gitlab.go index fa1a1e430..0fb72e68a 100644 --- a/gitlab.go +++ b/gitlab.go @@ -227,18 +227,15 @@ type Client struct { // ListOptions specifies the optional parameters to various List methods that // support pagination. type ListOptions struct { - // For keyset-based paginated result sets, name of the column by which to order - OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` - // For offset-based paginated result sets, page of results to retrieve. Page int `url:"page,omitempty" json:"page,omitempty"` - - // For keyset-based paginated result sets, the value must be `"keyset"` - Pagination string `url:"pagination,omitempty" json:"pagination,omitempty"` - // For offset-based and keyset-based paginated result sets, the number of results to include per page. PerPage int `url:"per_page,omitempty" json:"per_page,omitempty"` + // For keyset-based paginated result sets, name of the column by which to order + OrderBy string `url:"order_by,omitempty" json:"order_by,omitempty"` + // For keyset-based paginated result sets, the value must be `"keyset"` + Pagination string `url:"pagination,omitempty" json:"pagination,omitempty"` // For keyset-based paginated result sets, sort order (`"asc"`` or `"desc"`) Sort string `url:"sort,omitempty" json:"sort,omitempty"` } @@ -719,10 +716,7 @@ func (c *Client) UploadRequest(method, path string, content io.Reader, filename type Response struct { *http.Response - // These fields provide the page values for paginating through a set of - // results. Any or all of these may be set to the zero value for - // responses that are not part of a paginated set, or for which there - // are no additional pages. + // Fields used for offset-based pagination. TotalItems int TotalPages int ItemsPerPage int @@ -730,6 +724,7 @@ type Response struct { NextPage int PreviousPage int + // Fields used for keyset-based pagination. PreviousLink string NextLink string FirstLink string @@ -745,6 +740,7 @@ func newResponse(r *http.Response) *Response { } const ( + // Headers used for offset-based pagination. xTotal = "X-Total" xTotalPages = "X-Total-Pages" xPerPage = "X-Per-Page" @@ -752,6 +748,7 @@ const ( xNextPage = "X-Next-Page" xPrevPage = "X-Prev-Page" + // Headers used for keyset-based pagination. linkPrev = "prev" linkNext = "next" linkFirst = "first" @@ -783,11 +780,26 @@ func (r *Response) populatePageValues() { func (r *Response) populateLinkValues() { if link := r.Header.Get("Link"); link != "" { - linkHeaders, _ := parseLinkHeader(link) - r.PreviousLink = linkHeaders[linkPrev] - r.NextLink = linkHeaders[linkNext] - r.FirstLink = linkHeaders[linkFirst] - r.LastLink = linkHeaders[linkLast] + for _, link := range strings.Split(link, ",") { + parts := strings.Split(link, ";") + if len(parts) < 2 { + continue + } + + linkType := strings.Trim(strings.Split(parts[1], "=")[1], "\"") + linkValue := strings.Trim(parts[0], "< >") + + switch linkType { + case linkPrev: + r.PreviousLink = linkValue + case linkNext: + r.NextLink = linkValue + case linkFirst: + r.FirstLink = linkValue + case linkLast: + r.LastLink = linkValue + } + } } } @@ -913,39 +925,6 @@ func parseID(id interface{}) (string, error) { } } -// parseLinkHeader splits a "Link" HTTP header into its "link" and "rel" -// components, formatted in a comma-joined string of `<%s>; rel="%s"` -// -// GitLab API docs: -// https://docs.gitlab.com/ee/api/rest/#pagination-link-header -func parseLinkHeader(link string) (map[string]string, error) { - result := make(map[string]string) - - if len(link) == 0 { - return result, nil - } - - // each link will be of the form: <%s>; rel="%s" - for _, link := range strings.Split(link, ",") { - pieces := strings.Split(link, ";") - if len(pieces) < 2 { - return result, fmt.Errorf("invalid format for link header component: %s", link) - } - linkType := strings.Trim(strings.Split(pieces[1], "=")[1], "\"") - // remove surrounding angle brackets _and_ whitespace - linkValue := strings.Trim(pieces[0], "< >") - - switch linkType { - case linkPrev, linkNext, linkFirst, linkLast: - result[linkType] = linkValue - default: - return result, fmt.Errorf("invalid link type %s", linkType) - } - } - - return result, nil -} - // Helper function to escape a project identifier. func PathEscape(s string) string { return strings.ReplaceAll(url.PathEscape(s), ".", "%2E") diff --git a/gitlab_test.go b/gitlab_test.go index 7dfd63e68..ae08a4936 100644 --- a/gitlab_test.go +++ b/gitlab_test.go @@ -26,7 +26,6 @@ import ( "net/http" "net/http/httptest" "os" - "reflect" "strings" "testing" "time" @@ -239,19 +238,6 @@ func TestPathEscape(t *testing.T) { } } -func TestParseLinkHeaderEmpty(t *testing.T) { - h := `` - want := map[string]string{} - got, err := parseLinkHeader(h) - if err != nil { - t.Errorf("unexpected error: %s", err.Error()) - } - - if !reflect.DeepEqual(want, got) { - t.Errorf("Expected %s, got %s", want, got) - } -} - func TestPaginationPopulatePageValuesEmpty(t *testing.T) { wantPageHeaders := map[string]int{ xTotal: 0, diff --git a/request_options.go b/request_options.go index 13973ba56..d158047f6 100644 --- a/request_options.go +++ b/request_options.go @@ -54,19 +54,16 @@ func WithHeaders(headers map[string]string) RequestOptionFunc { } // WithKeysetPaginationParameters takes a "next" link from the Link header of a -// response to a keyset-based paginated request, and modifies the values of -// each query parameter in the request with its corresponding parameter from -// the response -func WithKeysetPaginationParameters(rawNextLink string) RequestOptionFunc { +// response to a keyset-based paginated request and modifies the values of each +// query parameter in the request with its corresponding response parameter. +func WithKeysetPaginationParameters(nextLink string) RequestOptionFunc { return func(req *retryablehttp.Request) error { - nextUrl, err := url.Parse(rawNextLink) + nextUrl, err := url.Parse(nextLink) if err != nil { return err } - params := nextUrl.Query() - // must make a copy of the query inside the func's scope q := req.URL.Query() - for k, values := range params { + for k, values := range nextUrl.Query() { q.Del(k) for _, v := range values { q.Add(k, v)