Skip to content

Commit

Permalink
Add LimitExceededError and try to detect limit exceed
Browse files Browse the repository at this point in the history
  • Loading branch information
printesoi committed Mar 25, 2024
1 parent a086541 commit 65ad3a6
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 43 deletions.
25 changes: 20 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,19 @@ func (c *Client) doApiUnmarshalXML(req *http.Request, response any) error {
}

if !responseBodyIsXML(resp.Header) {
if responseBodyIsPlainText(resp.Header) {
return newErrorResponseDetectType(resp)
}
return newErrorResponse(resp,
fmt.Errorf("expected application/xml, got %s", responseMediaType(resp.Header)))
fmt.Errorf("expected %s, got %s", mediaTypeApplicationXML, responseMediaType(resp.Header)))
}
return xmlUnmarshalReader(resp.Body, response)
if err := xmlUnmarshalReader(resp.Body, response); err != nil {
return newErrorResponseParse(resp, err, false)
}
return nil
}

func (c *Client) doApiUnmarshalJSON(req *http.Request, response any) error {
func (c *Client) doApiUnmarshalJSON(req *http.Request, destResponse any, cb func(*http.Response, any) error) error {
resp, err := c.do(req)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
Expand All @@ -189,10 +195,19 @@ func (c *Client) doApiUnmarshalJSON(req *http.Request, response any) error {
}

if !responseBodyIsJSON(resp.Header) {
if responseBodyIsPlainText(resp.Header) {
return newErrorResponseDetectType(resp)
}
return newErrorResponse(resp,
fmt.Errorf("expected application/json, got %s", responseMediaType(resp.Header)))
fmt.Errorf("expected %s, got %s", mediaTypeApplicationJSON, responseMediaType(resp.Header)))
}
if err := jsonUnmarshalReader(resp.Body, destResponse); err != nil {
return newErrorResponseParse(resp, err, false)
}
if cb != nil {
return cb(resp, destResponse)
}
return jsonUnmarshalReader(resp.Body, response)
return nil
}

// RequestOption represents an option that can modify an http.Request.
Expand Down
2 changes: 1 addition & 1 deletion client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ func TestClientAuth(t *testing.T) {
token.RefreshToken = buildRefreshToken(seq)
// 12 seconds = 2 seconds of validity (oauth2.defaultExpiryDelta = 10s)
token.ExpiresIn = 12
w.Header().Add("Content-Type", "application/json")
w.Header().Add("Content-Type", mediaTypeApplicationJSON)

tokenBytes, err := json.Marshal(token)
if err != nil {
Expand Down
98 changes: 94 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
)

// ErrorResponse is an error returns if the HTTP requests was finished (we got
Expand All @@ -35,7 +37,10 @@ type ErrorResponse struct {
Message *string
}

func newErrorResponse(resp *http.Response, err error) *ErrorResponse {
// newErrorResponseParse creates a new *ErrorResponse from the given
// http.Response and error. If parse is true, we try to unmarshal the body as
// JSON to extract some additional info about the error.
func newErrorResponseParse(resp *http.Response, err error, parse bool) *ErrorResponse {
errResp := &ErrorResponse{
StatusCode: resp.StatusCode,
Status: resp.Status,
Expand All @@ -46,20 +51,45 @@ func newErrorResponse(resp *http.Response, err error) *ErrorResponse {

data, err := peekResponseBody(resp)
errResp.ResponseBody = data
if err == nil && len(data) > 0 && responseBodyIsJSON(resp.Header) {
if !parse {
return errResp
}

responseSeemsJSON := func() bool {
if responseBodyIsJSON(resp.Header) {
return true
}
if responseBodyIsPlainText(resp.Header) {
if len(data) > 0 && data[0] == '{' {
return true
}
}
return false
}

if err == nil && len(data) > 0 && responseSeemsJSON() {
var b struct {
TraceID *string `json:"trace_id,omitempty"`
Message *string `json:"message,omitempty"`
Error *string `json:"eroare,omitempty"`
}
// Don't care if we get an error here.
json.Unmarshal(data, &b)
_ = json.Unmarshal(data, &b)
errResp.TraceID = b.TraceID
errResp.Message = b.Message
if b.Message == nil && b.Error != nil {
errResp.Message = b.Error
}
}

return errResp
}

// newErrorResponse is synonym to newErrorResponseParse(resp, err, true)
func newErrorResponse(resp *http.Response, err error) *ErrorResponse {
return newErrorResponseParse(resp, err, true)
}

func (r *ErrorResponse) Error() string {
m := fmt.Sprintf("ANAF API call error: %v %v: %s", r.Method, r.Url, r.Status)
if r.TraceID != nil {
Expand All @@ -81,6 +111,8 @@ type BuilderError struct {
Term *string
}

// newBuilderNameErrorf creates a new Builder error for the given builder name,
// term and format string and args.
func newBuilderNameErrorf(builder, term string, format string, a ...any) *BuilderError {
return &BuilderError{
error: fmt.Errorf(format, a...),
Expand All @@ -89,6 +121,8 @@ func newBuilderNameErrorf(builder, term string, format string, a ...any) *Builde
}
}

// newBuilderErrorf same as newBuilderNameErrorf but the builder name is taken
// from the type name of builder.
func newBuilderErrorf(builder any, term string, format string, a ...any) *BuilderError {
return newBuilderNameErrorf(typeNameAddrPtr(builder), term, format, a...)
}
Expand All @@ -108,6 +142,62 @@ type ValidateSignatureError struct {
error
}

func NewValidateSignatureError(err error) *ValidateSignatureError {
func newValidateSignatureError(err error) *ValidateSignatureError {
return &ValidateSignatureError{error: err}
}

// LimitExceededError is an error returned if we hit an API limit.
type LimitExceededError struct {
*ErrorResponse
Limit int64
}

func newLimitExceededError(r *http.Response, limit int64, err error) *LimitExceededError {
return &LimitExceededError{
ErrorResponse: newErrorResponseParse(r, err, false),
Limit: limit,
}
}

func (e *LimitExceededError) Error() string {
return e.ErrorResponse.Error()
}

var (
regexLimitExceededMsg = regexp.MustCompile("S-au facut deja (\\d+) .* in cursul zilei")
regexDownloadID = regexp.MustCompile("id_descarcare=(\\d+)")
regexCUI = regexp.MustCompile("CUI=\\s*(\\d+)")

// regexDownloadLimitExceededMsg = regexp.MustCompile("S-au facut deja (\\d+) descarcari la mesajul cu id_descarcare=(\\d+)\\s* in cursul zilei")
// regexGetMessagesLimitExceededMsg = regexp.MustCompile("S-au facut deja (\\d+) de interogari de tip lista mesaje .* de catre CUI=\\s*(\\d+)\\s* in cursul zilei")
)

func errorMessageMatchLimitExceeded(err string) (limit int64, match bool) {
if m, ok := matchFirstSubmatch(err, regexLimitExceededMsg); ok {
limit, _ := strconv.ParseInt(m, 10, 64)
return limit, true
}
return
}

func newErrorResponseDetectType(resp *http.Response) error {
data, err := peekResponseBody(resp)
if err == nil && len(data) > 0 {
var b struct {
Title string `json:"titlu"`
Error string `json:"eroare"`
}
// Don't care if we get an error here.
_ = json.Unmarshal(data, &b)
if b.Error != "" {
rerr := fmt.Errorf("%s: %s", b.Title, b.Error)
if limit, ok := errorMessageMatchLimitExceeded(b.Error); ok {
return newLimitExceededError(resp, limit, rerr)
}

return newErrorResponseParse(resp, rerr, false)
}
}

return newErrorResponse(resp, nil)
}
15 changes: 14 additions & 1 deletion helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ import (
"github.com/printesoi/xml-go"
)

const (
mediaTypeApplicationJSON = "application/json"
mediaTypeApplicationXML = "application/xml"
mediaTypeApplicationPDF = "application/pdf"
mediaTypeApplicationZIP = "application/zip"
mediaTypeTextXML = "text/xml"
mediaTypeTextPlain = "text/plain"
)

// This is a copy of the drainBody from src/net/http/httputil/dump.go
func drainBody(b io.ReadCloser) (body []byte, r2 io.ReadCloser, err error) {
if b == nil || b == http.NoBody {
Expand Down Expand Up @@ -59,7 +68,11 @@ func responseMediaType(headers http.Header) (mediaType string) {
}

func responseBodyIsJSON(headers http.Header) bool {
return responseMediaType(headers) == "application/json"
return responseMediaType(headers) == mediaTypeApplicationJSON
}

func responseBodyIsPlainText(headers http.Header) bool {
return responseMediaType(headers) == mediaTypeTextPlain
}

func responseBodyIsXML(headers http.Header) bool {
Expand Down
86 changes: 54 additions & 32 deletions rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,13 +432,13 @@ func (c *Client) ValidateXML(ctx context.Context, xml io.Reader, st ValidateStan
}
if !responseBodyIsJSON(resp.Header) {
return nil, newErrorResponse(resp,
fmt.Errorf("expected application/json, got %s", responseMediaType(resp.Header)))
fmt.Errorf("expected %s, got %s", mediaTypeApplicationJSON, responseMediaType(resp.Header)))
}

response = new(ValidateResponse)
if err := jsonUnmarshalReader(resp.Body, response); err != nil {
return nil, newErrorResponse(resp,
fmt.Errorf("failed to decode JSON body: %v", err))
return nil, newErrorResponseParse(resp,
fmt.Errorf("failed to decode JSON body: %v", err), false)
}

return response, nil
Expand Down Expand Up @@ -479,25 +479,25 @@ func (c *Client) XMLToPDF(ctx context.Context, xml io.Reader, st ValidateStandar
// If the response content type is application/json, then the validation
// failed, otherwise we got the PDF in response body
switch mediaType := responseMediaType(resp.Header); mediaType {
case "application/json":
response = &GeneratePDFResponse{
Error: &GeneratePDFResponseError{},
}
if err = jsonUnmarshalReader(resp.Body, response.Error); err != nil {
err = newErrorResponse(resp,
fmt.Errorf("failed to unmarshal response body: %v", err))
case mediaTypeApplicationJSON:
resError := new(GeneratePDFResponseError)
if err = jsonUnmarshalReader(resp.Body, resError); err != nil {
err = newErrorResponseParse(resp,
fmt.Errorf("failed to unmarshal response body: %v", err), false)
return
}
case "application/pdf":
response = &GeneratePDFResponse{Error: resError}
case mediaTypeApplicationPDF:
response = &GeneratePDFResponse{}
if response.PDF, err = io.ReadAll(resp.Body); err != nil {
err = newErrorResponse(resp,
fmt.Errorf("failed to read body: %v", err))
err = newErrorResponseParse(resp,
fmt.Errorf("failed to read body: %v", err), false)
return
}
default:
err = newErrorResponse(resp,
fmt.Errorf("expected application/json or application/pdf, got %s", mediaType))
fmt.Errorf("expected %s or %s, got %s", mediaTypeApplicationJSON,
mediaTypeApplicationPDF, mediaType))
}
return
}
Expand Down Expand Up @@ -542,7 +542,6 @@ func UploadOptionSelfBilled() uploadOption {
func (c *Client) UploadXML(
ctx context.Context, xml io.Reader, st UploadStandard, cif string, opts ...uploadOption,
) (response *UploadResponse, err error) {

uploadOptions := uploadOptions{}
for _, opt := range opts {
opt(&uploadOptions)
Expand All @@ -564,8 +563,10 @@ func (c *Client) UploadXML(
return
}

response = new(UploadResponse)
err = c.doApiUnmarshalXML(req, response)
res := new(UploadResponse)
if err = c.doApiUnmarshalXML(req, res); err == nil {
response = res
}
return
}

Expand Down Expand Up @@ -606,8 +607,10 @@ func (c *Client) GetMessageState(
return
}

response = new(GetMessageStateResponse)
err = c.doApiUnmarshalXML(req, response)
res := new(GetMessageStateResponse)
if err = c.doApiUnmarshalXML(req, res); err == nil {
response = res
}
return
}

Expand All @@ -629,8 +632,15 @@ func (c *Client) GetMessagesList(
return
}

response = new(MessagesListResponse)
err = c.doApiUnmarshalJSON(req, response)
res := new(MessagesListResponse)
if err = c.doApiUnmarshalJSON(req, res, func(r *http.Response, _ any) error {
if limit, ok := errorMessageMatchLimitExceeded(res.Error); ok {
return newLimitExceededError(r, limit, fmt.Errorf("%s: %s", res.Title, res.Error))
}
return nil
}); err == nil {
response = res
}
return
}

Expand All @@ -656,7 +666,15 @@ func (c *Client) GetMessagesListPagination(
return
}

err = c.doApiUnmarshalJSON(req, &response)
res := new(MessagesListPaginationResponse)
if err = c.doApiUnmarshalJSON(req, res, func(r *http.Response, _ any) error {
if limit, ok := errorMessageMatchLimitExceeded(res.Error); ok {
return newLimitExceededError(r, limit, fmt.Errorf("%s: %s", res.Title, res.Error))
}
return nil
}); err == nil {
response = res
}
return
}

Expand All @@ -683,25 +701,29 @@ func (c *Client) DownloadInvoice(
// If the response content type is application/json, then the download
// failed, otherwise we got the zip in response body
switch mediaType := responseMediaType(resp.Header); mediaType {
case "application/json":
response = &DownloadInvoiceResponse{
Error: &DownloadInvoiceResponseError{},
case mediaTypeApplicationJSON:
resError := new(DownloadInvoiceResponseError)
if err = jsonUnmarshalReader(resp.Body, resError); err != nil {
err = newErrorResponseParse(resp, err, false)
return
}
if err = jsonUnmarshalReader(resp.Body, response.Error); err != nil {
err = newErrorResponse(resp,
fmt.Errorf("failed to unmarshal response body: %v", err))
if limit, ok := errorMessageMatchLimitExceeded(resError.Error); ok {
err = newLimitExceededError(resp, limit, fmt.Errorf("%s: %s", resError.Title, resError.Error))
return
}
case "application/zip":
response = &DownloadInvoiceResponse{Error: resError}
case mediaTypeApplicationZIP:
response = &DownloadInvoiceResponse{}
if response.Zip, err = io.ReadAll(resp.Body); err != nil {
err = newErrorResponse(resp,
fmt.Errorf("failed to read body: %v", err))
err = newErrorResponseParse(resp, err, false)
return
}
case mediaTypeTextPlain:
err = newErrorResponseDetectType(resp)
default:
err = newErrorResponse(resp,
fmt.Errorf("expected application/json or application/pdf, got %s", mediaType))
fmt.Errorf("expected %s or %s, got %s", mediaTypeApplicationJSON,
mediaTypeApplicationPDF, mediaType))
}
return
}
Expand Down

0 comments on commit 65ad3a6

Please sign in to comment.