Skip to content

Commit 6deba90

Browse files
authored
Don't omit trailing error response for fake pages (Azure#21849)
Fetching the next page, or an error, is predicated on the page response's next link field being populated. Always set the next link field even if the next "page" is an error response.
1 parent 98163d2 commit 6deba90

File tree

5 files changed

+124
-27
lines changed

5 files changed

+124
-27
lines changed

sdk/azcore/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* Block key and SAS authentication for non TLS protected endpoints.
1717
* Passing a `nil` credential value will no longer cause a panic. Instead, the authentication is skipped.
1818
* Calling `Error` on a zero-value `azcore.ResponseError` will no longer panic.
19+
* Fixed an issue in `fake.PagerResponder[T]` that would cause a trailing error to be omitted when iterating over pages.
1920

2021
### Other Changes
2122

sdk/azcore/fake/fake.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ func (e *ErrorResponder) SetResponseError(httpStatus int, errorCode string) {
7474
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
7575

7676
// PagerResponder represents a sequence of paged responses.
77-
// Responses are replayed in the order in which they were added.
77+
// Responses are consumed in the order in which they were added.
78+
// If no pages or errors have been added, calls to Pager[T].NextPage
79+
// will return an error.
7880
type PagerResponder[T any] exported.PagerResponder[T]
7981

8082
// AddPage adds a page to the sequence of respones.
@@ -102,8 +104,10 @@ type AddPageOptions = exported.AddPageOptions
102104
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
103105

104106
// PollerResponder represents a sequence of responses for a long-running operation.
105-
// Any non-terminal responses are replayed in the order in which they were added.
107+
// Any non-terminal responses are consumed in the order in which they were added.
106108
// The terminal response, success or error, is always the final response.
109+
// If no responses or errors have been added, the following method calls on Poller[T]
110+
// will return an error: PollUntilDone, Poll, Result.
107111
type PollerResponder[T any] exported.PollerResponder[T]
108112

109113
// AddNonTerminalResponse adds a non-terminal response to the sequence of responses.

sdk/azcore/fake/fake_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func TestPagerResponder(t *testing.T) {
161161
require.NotNil(t, resp)
162162
page, err := unmarshal[widgets](resp)
163163
require.NoError(t, err)
164-
require.Nil(t, page.NextPage)
164+
require.NotNil(t, page.NextPage)
165165
require.Equal(t, []widget{{Name: "baz"}}, page.Widgets)
166166
case 4:
167167
require.Error(t, err)

sdk/azcore/fake/internal/exported/fake.go

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ type AddPageOptions struct {
133133
// This function is called by the fake server internals.
134134
func (p *PagerResponder[T]) Next(req *http.Request) (*http.Response, error) {
135135
if len(p.pages) == 0 {
136-
return nil, shared.NonRetriableError(errors.New("paged response has no pages"))
136+
return nil, shared.NonRetriableError(errors.New("fake paged response is empty"))
137137
}
138138

139139
page := p.pages[0]
@@ -175,48 +175,42 @@ func (p *PagerResponder[T]) More() bool {
175175
return len(p.pages) > 0
176176
}
177177

178-
type pageindex[T any] struct {
179-
i int
180-
page pageResp[T]
181-
}
182-
183178
// nextLinkURLSuffix is the URL path suffix for a faked next page followed by one or more digits.
184179
const nextLinkURLSuffix = "/fake_page_"
185180

186181
// InjectNextLinks is used to populate the nextLink field.
187182
// The inject callback is executed for every T in the sequence except for the last one.
188183
// This function is called by the fake server internals.
189184
func (p *PagerResponder[T]) InjectNextLinks(req *http.Request, inject func(page *T, createLink func() string)) {
190-
// first find all the actual pages in the list
191-
pages := make([]pageindex[T], 0, len(p.pages))
185+
// populate the next links, including pageResp[T] where the next
186+
// "page" is an error response. this allows an error response to
187+
// be returned when there are no subsequent pages.
188+
pageNum := 1
192189
for i := range p.pages {
193-
if pageT, ok := p.pages[i].(pageResp[T]); ok {
194-
pages = append(pages, pageindex[T]{
195-
i: i,
196-
page: pageT,
197-
})
198-
}
199-
}
200-
201-
// now populate the next links
202-
for i := range pages {
203-
if i+1 == len(pages) {
190+
if i+1 == len(p.pages) {
204191
// no nextLink for last page
205192
break
206193
}
207194

195+
pageT, ok := p.pages[i].(pageResp[T])
196+
if !ok {
197+
// error entry, no next link
198+
continue
199+
}
200+
208201
qp := ""
209202
if req.URL.RawQuery != "" {
210203
qp = "?" + req.URL.RawQuery
211204
}
212205

213-
inject(&pages[i].page.entry, func() string {
206+
inject(&pageT.entry, func() string {
214207
// NOTE: any changes to this path format MUST be reflected in SanitizePagerPath()
215-
return fmt.Sprintf("%s://%s%s%s%d%s", req.URL.Scheme, req.URL.Host, req.URL.Path, nextLinkURLSuffix, i+1, qp)
208+
return fmt.Sprintf("%s://%s%s%s%d%s", req.URL.Scheme, req.URL.Host, req.URL.Path, nextLinkURLSuffix, pageNum, qp)
216209
})
210+
pageNum++
217211

218212
// update the original slice with the modified page
219-
p.pages[pages[i].i] = pages[i].page
213+
p.pages[i] = pageT
220214
}
221215
}
222216

@@ -326,7 +320,7 @@ func (p *PollerResponder[T]) Next(req *http.Request) (*http.Response, error) {
326320
httpResp.Header.Set(shared.HeaderFakePollerStatus, "Succeeded")
327321
return httpResp, nil
328322
} else {
329-
return nil, shared.NonRetriableError(fmt.Errorf("%T has no terminal response", p))
323+
return nil, shared.NonRetriableError(errors.New("fake poller response is emtpy"))
330324
}
331325
}
332326

sdk/azcore/fake/internal/exported/fake_test.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func TestPagerResponder(t *testing.T) {
134134
require.NotNil(t, resp)
135135
page, err := unmarshal[widgets](resp)
136136
require.NoError(t, err)
137-
require.Nil(t, page.NextPage)
137+
require.NotNil(t, page.NextPage)
138138
require.Equal(t, []widget{{Name: "baz"}}, page.Widgets)
139139
case 4:
140140
require.Error(t, err)
@@ -149,6 +149,104 @@ func TestPagerResponder(t *testing.T) {
149149
iterations++
150150
}
151151
require.Equal(t, 5, iterations)
152+
153+
// single page with subsequent error
154+
pagerResp = PagerResponder[widgets]{}
155+
156+
pagerResp.AddPage(http.StatusOK, widgets{
157+
Widgets: []widget{
158+
{Name: "foo"},
159+
{Name: "bar"},
160+
},
161+
}, nil)
162+
pagerResp.AddError(errors.New("two"))
163+
164+
pagerResp.InjectNextLinks(req, func(p *widgets, create func() string) {
165+
p.NextPage = to.Ptr(create())
166+
})
167+
168+
iterations = 0
169+
for pagerResp.More() {
170+
resp, err := pagerResp.Next(req)
171+
switch iterations {
172+
case 0:
173+
require.NoError(t, err)
174+
require.NotNil(t, resp)
175+
page, err := unmarshal[widgets](resp)
176+
require.NoError(t, err)
177+
require.NotNil(t, page.NextPage)
178+
require.Equal(t, []widget{{Name: "foo"}, {Name: "bar"}}, page.Widgets)
179+
case 1:
180+
require.Error(t, err)
181+
require.Nil(t, resp)
182+
}
183+
iterations++
184+
}
185+
require.EqualValues(t, 2, iterations)
186+
187+
// single page with subsequent response error
188+
pagerResp = PagerResponder[widgets]{}
189+
190+
pagerResp.AddPage(http.StatusOK, widgets{
191+
Widgets: []widget{
192+
{Name: "foo"},
193+
{Name: "bar"},
194+
},
195+
}, nil)
196+
pagerResp.AddResponseError(http.StatusBadRequest, "BadRequest")
197+
198+
pagerResp.InjectNextLinks(req, func(p *widgets, create func() string) {
199+
p.NextPage = to.Ptr(create())
200+
})
201+
202+
iterations = 0
203+
for pagerResp.More() {
204+
resp, err := pagerResp.Next(req)
205+
switch iterations {
206+
case 0:
207+
require.NoError(t, err)
208+
require.NotNil(t, resp)
209+
page, err := unmarshal[widgets](resp)
210+
require.NoError(t, err)
211+
require.NotNil(t, page.NextPage)
212+
require.Equal(t, []widget{{Name: "foo"}, {Name: "bar"}}, page.Widgets)
213+
case 1:
214+
require.Error(t, err)
215+
require.Nil(t, resp)
216+
}
217+
iterations++
218+
}
219+
require.EqualValues(t, 2, iterations)
220+
221+
// single page
222+
pagerResp = PagerResponder[widgets]{}
223+
224+
pagerResp.AddPage(http.StatusOK, widgets{
225+
Widgets: []widget{
226+
{Name: "foo"},
227+
{Name: "bar"},
228+
},
229+
}, nil)
230+
231+
pagerResp.InjectNextLinks(req, func(p *widgets, create func() string) {
232+
p.NextPage = to.Ptr(create())
233+
})
234+
235+
iterations = 0
236+
for pagerResp.More() {
237+
resp, err := pagerResp.Next(req)
238+
switch iterations {
239+
case 0:
240+
require.NoError(t, err)
241+
require.NotNil(t, resp)
242+
page, err := unmarshal[widgets](resp)
243+
require.NoError(t, err)
244+
require.Nil(t, page.NextPage)
245+
require.Equal(t, []widget{{Name: "foo"}, {Name: "bar"}}, page.Widgets)
246+
}
247+
iterations++
248+
}
249+
require.EqualValues(t, 1, iterations)
152250
}
153251

154252
func TestPollerResponder(t *testing.T) {

0 commit comments

Comments
 (0)