英文:
Consuming Paginated REST API
问题
我正在尝试使用一个API,每次成功请求后都会返回以下json键:
{
    "hasNextPage": true,
    "endCursor": "some_guid_present_here"
}
API的工作方式是检查是否有下一页,然后使用适当的游标继续分页。
然而,每次使用这个结构时,即使response.Paginations.HasNextPage为false,它有时仍会保持循环。
我正在尝试理解是否是我以这种方式消耗分页API的结构导致了这个问题。
此外,我有4-5个起始请求,我通过goroutines分别发送。我不确定这是否会引起问题,但我也在etl.go中附上了这个。
主要的请求结构在api.go中。
我已经确认我收到了响应,并且它们正确地解组,但我正在努力解决这种不确定的行为。
api.go
package models
import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"fmt"
)
type Request struct {
	Url    string
	ApiKey string
}
type Response struct {
	...一些字段...
	Paginations Pagination `json:"pagination"`
}
type Pagination struct {
	EndCursor   string `json:"endCursor"`
	HasNextPage bool   `json:"hasNextPage"`
}
func (request *Request) Get() ([]Response, error) {
	var responses []Response
	var response Response
	// 设置新请求
	req, err := http.NewRequest("GET", request.Url, nil)
	if err != nil {
		fmt.Println("创建请求时出错...")
		return responses, err
	}
	// 添加请求头
	req.Header = http.Header{
		"accept":        {"application/json"},
		"authorization": {"Bearer " + request.ApiKey},
	}
	// 从API获取初始响应并捕获状态码
	resp, _ := http.DefaultClient.Do(req)
	response.Status = resp.StatusCode
	// 读取响应体并解组为结构体
	respBody, err := ioutil.ReadAll(resp.Body)
	json.Unmarshal(respBody, &response)
	// 如果有解析错误,记录下来
	if err != nil {
		fmt.Println(err)
	}
	defer resp.Body.Close()
	// 这个字段将在响应有效负载中
	// 它已验证为bool类型(而不是字符串)
	fmt.Printf("是否有下一页?%t\n", response.Paginations.HasNextPage)
	// 将响应追加到响应列表中
	responses = append(responses, response)
	// 如果有分页,继续循环直到所有分页都用完
	for response.Paginations.HasNextPage == true {
		req, err := http.NewRequest("GET", request.Url, nil)
		if err != nil {
			return responses, err
		}
		// 在查询中添加“after”游标,以便重定向到分页响应
		qry := req.URL.Query()
		qry.Set("after", response.Paginations.EndCursor)
		req.URL.RawQuery = qry.Encode()
		fmt.Println("分页请求查询:", req.URL.String())
		// 发送请求
		resp, err := http.DefaultClient.Do(req)
		response.Status = resp.StatusCode
		fmt.Printf("状态码:%d\n", response.Status)
		// 读取响应并反序列化
		respBody, err := ioutil.ReadAll(resp.Body)
		json.Unmarshal(respBody, &response)
		fmt.Println("分页游标:", response.Paginations.EndCursor)
		fmt.Printf("是否有下一页?%t\n", response.Paginations.HasNextPage)
		// 如果有解析错误,记录下来
		if err != nil {
			fmt.Println(err)
		}
		defer resp.Body.Close()
		// 将响应追加到响应列表中
		responses = append(responses, response)
	}
	return responses, nil
}
etl.go
package models
import (
	"fmt"
	"sync"
)
type Etl struct {
	Requests []Request
}
func (etl *Etl) Extract() [][]Response {
	var wg sync.WaitGroup
	ch := make(chan []Response)
	defer close(ch)
	for _, req := range etl.Requests {
		wg.Add(1) // 增加计数
		fmt.Println("增加等待组")
		go func(i Request) {
			defer wg.Done() // 减少计数
			resp, err := req.Get()
			if err != nil {
				fmt.Println(err)
			}
			ch <- resp
			fmt.Println("减少等待组")
		}(req)
	}
	results := make([][]Response, len(etl.Requests))
	for i, _ := range results {
		results[i] = <-ch
		//fmt.Println(results[i])
	}
	wg.Wait()
	return nil
}
希望对你有所帮助!
英文:
I am trying to consume an API that on each successful request has a json key like the following:
{
    "hasNextPage": true,
    "endCursor": "some_guid_present_here"
}
The way the API works (I have used it many times in Python but am trying with Go for separate use case) is to basically check if there is a next page, then use the appropriate cursor to continue to paginate.
However, every time I use this structure, it will sometimes keep looping even though the response.Paginations.HasNextPage will be false.
I am trying to understand if it is my structure of consuming a paginated API like this that is causing this or else.
Also, I have say 4-5 requests to start off with, which I sent separately via goroutines. I am not sure if this is causing an issue but I've attached that as well in etl.go.
The main request making structs are in api.go.
I've confirmed that I do receive responses and they are unmarshaling properly, but am trying to wrangle this uncertain behavior.
api.go
package models
import (
	"encoding/json"
	"io/ioutil"
	"net/http"
    "fmt"
)
type Request struct {
    Url string
    ApiKey string
}
type Response struct {
    ...some fields...
	Paginations Pagination `json:"pagination"`
}
type Pagination struct {
	EndCursor   string `json:"endCursor"`
	HasNextPage bool   `json:"hasNextPage"`
}
func (request *Request) Get() ([]Response, error) {
    
    var responses []Response
    var response Response
	// Set up new request
	req, err := http.NewRequest("GET", request.Url, nil)
	if err != nil {
		fmt.Println("Error creating request...")
		return responses, err
	}
	// Add request headers
	req.Header = http.Header{
		"accept":        {"application/json"},
		"authorization": {"Bearer " + request.ApiKey},
	}
	// Get our initial response from the API and capture status code
	resp, _ := http.DefaultClient.Do(req)
	response.Status = resp.StatusCode
	// Read the response body and Unmarshal into struct
	respBody, err := ioutil.ReadAll(resp.Body)
	json.Unmarshal(respBody, &response)
	// If there was a parsing error, log it
	if err != nil {
		fmt.Println(err)
	}
	defer resp.Body.Close()
    
    // This field will be in the response payload
    // It is verified to be of type bool (not string)
	fmt.Printf("Has Next Page? %t\n", resp.Paginations.HasNextPage)
	// Append response to our slice of responses
	responses = append(responses, response)
	// If paginations are available, continue to loop through until all paginations are exhausted
	for response.Paginations.HasNextPage == true {
		req, err := http.NewRequest("GET", request.Url, nil)
		if err != nil {
			return responses, err
		}
		// Append "after" cursor to query in order to redirect to paginated response
		qry := req.URL.Query()
		qry.Set("after", response.Paginations.EndCursor)
		req.URL.RawQuery = qry.Encode()
		fmt.Println("Paginated request query: ", req.URL.String())
		// Make request
		resp, err := http.DefaultClient.Do(req)
		response.Status = resp.StatusCode
		fmt.Printf("Status Code: %d\n", response.Status)
		// Read response and deserialize it
		respBody, err := ioutil.ReadAll(resp.Body)
		json.Unmarshal(respBody, &response)
		fmt.Println("Pagination Cursor: ", response.Paginations.EndCursor)
		fmt.Printf("Has Next Page? %t\n", response.Paginations.HasNextPage)
		// If there was a parsing error, log it
		if err != nil {
			fmt.Println(err)
		}
		defer resp.Body.Close()
		// Append response to our slice of responses
		responses = append(responses, response)
	}
	return responses, nil
}
etl.go
package models
import (
    "fmt"
	"sync"
)
type Etl struct {
	Requests []Request
}
func (etl *Etl) Extract() [][]Response {
	var wg sync.WaitGroup
	ch := make(chan []Response)
	defer close(ch)
	for _, req := range etl.Requests {
		wg.Add(1) // Increment
		fmt.Println("Incremented wait group")
		go func(i Request) {
			defer wg.Done() // Decrement
			resp, err := req.Get()
			if err != nil {
				fmt.Println(err)
			}
			ch <- resp
			fmt.Println("Decremented wait group")
		}(req)
	}
	results := make([][]Response, len(etl.Requests))
	for i, _ := range results {
		results[i] = <-ch
		//fmt.Println(results[i])
	}
	wg.Wait()
	return nil
}
答案1
得分: 1
我相信我找到了问题所在。在我的分页循环中,for response.Paginations.HasNextPage == true,我在每次迭代中创建了一个新的请求对象(http.NewRequest),该对象没有添加前一个(初始请求)的标头。
这导致返回401未经授权错误并持续查询API,因为它没有接收到新的response.Paginations.HasNextPage值。
我的解决方案是稍微修改for循环,如下所示:
package models
import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "fmt"
)
type Request struct {
    Url    string
    ApiKey string
}
type Response struct {
    // ...一些字段...
    Paginations Pagination `json:"pagination"`
}
type Pagination struct {
    EndCursor   string `json:"endCursor"`
    HasNextPage bool   `json:"hasNextPage"`
}
func (request *Request) Get() ([]Response, error) {
    var responses []Response
    var response Response
    // 设置新的请求
    req, err := http.NewRequest("GET", request.Url, nil)
    if err != nil {
        fmt.Println("创建请求时出错...")
        return responses, err
    }
    // 添加请求标头
    req.Header = http.Header{
        "accept":        {"application/json"},
        "authorization": {"Bearer " + request.ApiKey},
    }
    // 从API获取初始响应并捕获状态码
    resp, _ := http.DefaultClient.Do(req)
    response.Status = resp.StatusCode
    // 读取响应正文并解组为结构体
    respBody, err := ioutil.ReadAll(resp.Body)
    json.Unmarshal(respBody, &response)
    // 如果有解析错误,记录下来
    if err != nil {
        fmt.Println(err)
    }
    defer resp.Body.Close()
    // 这个字段将在响应有效负载中
    // 它被验证为bool类型(而不是字符串)
    fmt.Printf("是否有下一页?%t\n", response.Paginations.HasNextPage)
    // 将响应附加到我们的响应切片中
    responses = append(responses, response)
    // 如果有分页可用,继续循环直到所有分页用尽
    for response.Paginations.HasNextPage == true {
        // 将“after”游标附加到查询中,以便重定向到分页响应
        qry := req.URL.Query()
        qry.Set("after", response.Paginations.EndCursor)
        req.URL.RawQuery = qry.Encode()
        fmt.Println("分页请求查询:", req.URL.String())
        // 发送请求
        resp, err := http.DefaultClient.Do(req)
        response.Status = resp.StatusCode
        fmt.Printf("状态码:%d\n", response.Status)
        // 读取响应并反序列化
        respBody, err := ioutil.ReadAll(resp.Body)
        json.Unmarshal(respBody, &response)
        fmt.Println("分页游标:", response.Paginations.EndCursor)
        fmt.Printf("是否有下一页?%t\n", response.Paginations.HasNextPage)
        // 如果有解析错误,记录下来
        if err != nil {
            fmt.Println(err)
        }
        defer resp.Body.Close()
        // 将响应附加到我们的响应切片中
        responses = append(responses, response)
    }
    return responses, nil
}
以上是翻译好的内容,请查阅。
英文:
I believe I found the issue. In my pagination loop for response.Paginations.HasNextPage == true I was creating a new request object (http.NewRequest) on each iteration which did not have headers added from the prior (initial request).
This caused a 401 unauthorized error to return and continuous querying of the API since it was not receiving a new response.Paginations.HasNextPage value.
My solution was to simply alter the for loop a bit like so:
package models
import (
"encoding/json"
"io/ioutil"
"net/http"
"fmt"
)
type Request struct {
Url string
ApiKey string
}
type Response struct {
...some fields...
Paginations Pagination `json:"pagination"`
}
type Pagination struct {
EndCursor   string `json:"endCursor"`
HasNextPage bool   `json:"hasNextPage"`
}
func (request *Request) Get() ([]Response, error) {
var responses []Response
var response Response
// Set up new request
req, err := http.NewRequest("GET", request.Url, nil)
if err != nil {
fmt.Println("Error creating request...")
return responses, err
}
// Add request headers
req.Header = http.Header{
"accept":        {"application/json"},
"authorization": {"Bearer " + request.ApiKey},
}
// Get our initial response from the API and capture status code
resp, _ := http.DefaultClient.Do(req)
response.Status = resp.StatusCode
// Read the response body and Unmarshal into struct
respBody, err := ioutil.ReadAll(resp.Body)
json.Unmarshal(respBody, &response)
// If there was a parsing error, log it
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
// This field will be in the response payload
// It is verified to be of type bool (not string)
fmt.Printf("Has Next Page? %t\n", resp.Paginations.HasNextPage)
// Append response to our slice of responses
responses = append(responses, response)
// If paginations are available, continue to loop through until all paginations are exhausted
for response.Paginations.HasNextPage == true {
// Append "after" cursor to query in order to redirect to paginated response
qry := req.URL.Query()
qry.Set("after", response.Paginations.EndCursor)
req.URL.RawQuery = qry.Encode()
fmt.Println("Paginated request query: ", req.URL.String())
// Make request
resp, err := http.DefaultClient.Do(req)
response.Status = resp.StatusCode
fmt.Printf("Status Code: %d\n", response.Status)
// Read response and deserialize it
respBody, err := ioutil.ReadAll(resp.Body)
json.Unmarshal(respBody, &response)
fmt.Println("Pagination Cursor: ", response.Paginations.EndCursor)
fmt.Printf("Has Next Page? %t\n", response.Paginations.HasNextPage)
// If there was a parsing error, log it
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
// Append response to our slice of responses
responses = append(responses, response)
}
return responses, nil
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。


评论