Skip to content

Commit

Permalink
fix: add status code if the body contain a code (#93)
Browse files Browse the repository at this point in the history
* fix: add status code if the body contain a code

* feat: Include relayOutputErr
- Add relayOutputErr type
- Update test to handle new error type

* fix: pre calculate regex

Co-authored-by: Daniel Olshansky <olshansky.daniel@gmail.com>

* Optimize errorCode extraction

* update test to include the new status code behavior with 500 as default

---------

Co-authored-by: Daniel Olshansky <olshansky.daniel@gmail.com>
  • Loading branch information
ricantar and Olshansk committed May 7, 2024
1 parent e33f6c8 commit 8eba803
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 60 deletions.
107 changes: 98 additions & 9 deletions provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"io/ioutil"
"math/big"
"net/http"
"regexp"
"strconv"
"strings"
"time"

"github.com/pokt-foundation/pocket-go/utils"
Expand All @@ -30,6 +33,30 @@ var (
ErrNonJSONResponse = errors.New("non JSON response")

errOnRelayRequest = errors.New("error on relay request")

regexPatterns = []*regexp.Regexp{}

errorStatusCodesMap = map[string]int{
"context deadline exceeded (Client.Timeout exceeded while awaiting headers)": 408, // Request Timeout
"connection reset by peer": 503, // Service Unavailable
"no such host": 404, // Not Found
"network is unreachable": 503, // Service Unavailable
"connection refused": 502, // Bad Gateway
"http: server closed idle connection": 499, // Client Closed Request (non-standard)
"tls: handshake failure": 525, // SSL Handshake Failed (Cloudflare specific, non-standard)
"i/o timeout": 504, // Gateway Timeout
"bad gateway": 502, // Bad Gateway
"service unavailable": 503, // Service Unavailable
"gateway timeout": 504, // Gateway Timeout
}
)

const (
// 202 means the response was accepted but we don't know if it actually succeeded
defaultStatusCode = 202
// Use this contante to avoid the use of the hardcoded string result
// result is the field present in a successful response
resultText = "result"
)

// Provider struct handler por JSON RPC provider
Expand All @@ -48,6 +75,18 @@ func NewProvider(rpcURL string, dispatchers []string) *Provider {
}
}

func init() {
regexPatterns = []*regexp.Regexp{
regexp.MustCompile(`"code"\s*:\s*(\d+)`), // Matches and captures any numeric status code after `"code":`
regexp.MustCompile(`(\d+)\s+Not Found`), // Matches and captures the status code from strings like `404 Not Found`
regexp.MustCompile(`(\d+)\s+page not found`), // Matches and captures the status code from strings like `404 page not found`
regexp.MustCompile(`HTTP\/\d\.\d\s+(\d+)`), // Matches and captures the status code from HTTP status lines like `HTTP/1.1 200`
regexp.MustCompile(`"statusCode"\s*:\s*(\d+)`), // Matches and captures any numeric status code after `"statusCode":`
regexp.MustCompile(`(\d+)\s+OK`), // Matches and captures `200` in a response like `200 OK`
regexp.MustCompile(`"statusCode"\s*:\s*(\d+)`), // Matches and captures any numeric status code after `"statusCode":`, added redundantly for clarity in different contexts
}
}

// RequestConfigOpts are the optional values for request config
type RequestConfigOpts struct {
Retries int
Expand Down Expand Up @@ -785,42 +824,92 @@ func (p *Provider) DispatchWithCtx(ctx context.Context, appPublicKey, chain stri
}

// Relay does request to be relayed to a target blockchain
func (p *Provider) Relay(rpcURL string, input *RelayInput, options *RelayRequestOptions) (*RelayOutput, error) {
func (p *Provider) Relay(rpcURL string, input *RelayInput, options *RelayRequestOptions) (*RelayOutput, *RelayOutputErr) {
return p.RelayWithCtx(context.Background(), rpcURL, input, options)
}

// RelayWithCtx does request to be relayed to a target blockchain
func (p *Provider) RelayWithCtx(ctx context.Context, rpcURL string, input *RelayInput, options *RelayRequestOptions) (*RelayOutput, error) {
func (p *Provider) RelayWithCtx(ctx context.Context, rpcURL string, input *RelayInput, options *RelayRequestOptions) (*RelayOutput, *RelayOutputErr) {
rawOutput, reqErr := p.doPostRequest(ctx, rpcURL, input, ClientRelayRoute, http.Header{})

defer closeOrLog(rawOutput)

statusCode := extractStatusFromRequest(rawOutput, reqErr)

if reqErr != nil && !errors.Is(reqErr, errOnRelayRequest) {
return nil, reqErr
return nil, &RelayOutputErr{Error: reqErr, StatusCode: statusCode}
}

bodyBytes, err := ioutil.ReadAll(rawOutput.Body)
bodyBytes, err := io.ReadAll(rawOutput.Body)
if err != nil {
return nil, err
return nil, &RelayOutputErr{Error: err, StatusCode: statusCode}
}

if errors.Is(reqErr, errOnRelayRequest) {
return nil, parseRelayErrorOutput(bodyBytes, input.Proof.ServicerPubKey)
return nil, &RelayOutputErr{Error: parseRelayErrorOutput(bodyBytes, input.Proof.ServicerPubKey), StatusCode: statusCode}
}

// The statusCode will be overwritten based on the response
return parseRelaySuccesfulOutput(bodyBytes)
}

func parseRelaySuccesfulOutput(bodyBytes []byte) (*RelayOutput, error) {
func extractStatusFromRequest(rawOutput *http.Response, reqErr error) int {
statusCode := defaultStatusCode

if reqErr != nil {
for key, status := range errorStatusCodesMap {
if strings.Contains(reqErr.Error(), key) { // This checks if the actual error contains the key string
return status
}
}

// If we got an error and we can't identify it as a known error, will be mark as if the server failed
return http.StatusInternalServerError
}

if rawOutput.StatusCode != http.StatusOK {
// If there's a response we'll use that as the status
// NOTE: We know that nodes are manipulating the output, for this reason we'll ignore the status if it's ok
statusCode = rawOutput.StatusCode
}

return statusCode
}

// TODO: Remove this function after the node responds back to us with a statusCode alongside with the response and the signature.
// Returns 202 if none of the pre-defined internal regexes matches any return values.
func extractStatusFromResponse(response string) int {
for _, pattern := range regexPatterns {
matches := pattern.FindStringSubmatch(response)
if len(matches) > 1 {
code, err := strconv.Atoi(matches[1])
if err != nil || http.StatusText(code) == "" {
continue
}
return code
}
}
return defaultStatusCode
}

func parseRelaySuccesfulOutput(bodyBytes []byte) (*RelayOutput, *RelayOutputErr) {
output := RelayOutput{}

err := json.Unmarshal(bodyBytes, &output)
if err != nil {
return nil, err
return nil, &RelayOutputErr{Error: err}
}

// Check if there's explicitly a result field, if there's on mark it as success, otherwise check what's the potential status.
// for REST chain that doesn't return result in any of the call will be defaulted to 202 in extractStatusFromResponse
if strings.Contains(output.Response, resultText) {
output.StatusCode = http.StatusOK
} else {
output.StatusCode = extractStatusFromResponse(output.Response)
}

if !json.Valid([]byte(output.Response)) {
return nil, ErrNonJSONResponse
return nil, &RelayOutputErr{Error: ErrNonJSONResponse, StatusCode: output.StatusCode}
}

return &output, nil
Expand Down
28 changes: 16 additions & 12 deletions provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,27 +769,29 @@ func TestProvider_Relay(t *testing.T) {
mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusOK, "samples/client_relay.json")

relay, err := provider.Relay("https://dummy.com", &RelayInput{}, nil)
c.NoError(err)
c.Nil(err)
c.NotEmpty(relay)

mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusInternalServerError, "samples/client_relay.json")

relay, err = provider.Relay("https://dummy.com", &RelayInput{}, nil)
c.Equal(Err5xxOnConnection, err)
c.False(IsErrorCode(EmptyPayloadDataError, err))
c.Equal(Err5xxOnConnection, err.Error)
c.Equal(http.StatusInternalServerError, err.StatusCode)
c.False(IsErrorCode(EmptyPayloadDataError, err.Error))
c.Empty(relay)

mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusBadRequest, "samples/client_relay_error.json")

relay, err = provider.Relay("https://dummy.com", &RelayInput{Proof: &RelayProof{ServicerPubKey: "PJOG"}}, nil)
c.Equal("Request failed with code: 25, codespace: pocketcore and message: the payload data of the relay request is empty\nWith ServicerPubKey: PJOG", err.Error())
c.True(IsErrorCode(EmptyPayloadDataError, err))
c.Equal("Request failed with code: 25, codespace: pocketcore and message: the payload data of the relay request is empty\nWith ServicerPubKey: PJOG", err.Error.Error())
c.True(IsErrorCode(EmptyPayloadDataError, err.Error))
c.Equal(http.StatusInternalServerError, err.StatusCode)
c.Empty(relay)

mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusOK, "samples/client_relay_non_json.json")

relay, err = provider.Relay("https://dummy.com", &RelayInput{}, nil)
c.Equal(ErrNonJSONResponse, err)
c.Equal(ErrNonJSONResponse, err.Error)
c.Empty(relay)
}

Expand All @@ -804,26 +806,28 @@ func TestProvider_RelayWithCtx(t *testing.T) {
mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusOK, "samples/client_relay.json")

relay, err := provider.RelayWithCtx(context.Background(), "https://dummy.com", &RelayInput{}, nil)
c.NoError(err)
c.Nil(err)
c.NotEmpty(relay)

mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusInternalServerError, "samples/client_relay.json")

relay, err = provider.RelayWithCtx(context.Background(), "https://dummy.com", &RelayInput{}, nil)
c.Equal(Err5xxOnConnection, err)
c.False(IsErrorCode(EmptyPayloadDataError, err))
c.Equal(Err5xxOnConnection, err.Error)
c.Equal(http.StatusInternalServerError, err.StatusCode)
c.False(IsErrorCode(EmptyPayloadDataError, err.Error))
c.Empty(relay)

mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusBadRequest, "samples/client_relay_error.json")

relay, err = provider.RelayWithCtx(context.Background(), "https://dummy.com", &RelayInput{Proof: &RelayProof{ServicerPubKey: "PJOG"}}, nil)
c.Equal("Request failed with code: 25, codespace: pocketcore and message: the payload data of the relay request is empty\nWith ServicerPubKey: PJOG", err.Error())
c.True(IsErrorCode(EmptyPayloadDataError, err))
c.Equal("Request failed with code: 25, codespace: pocketcore and message: the payload data of the relay request is empty\nWith ServicerPubKey: PJOG", err.Error.Error())
c.True(IsErrorCode(EmptyPayloadDataError, err.Error))
c.Equal(http.StatusInternalServerError, err.StatusCode)
c.Empty(relay)

mock.AddMockedResponseFromFile(http.MethodPost, fmt.Sprintf("%s%s", "https://dummy.com", ClientRelayRoute), http.StatusOK, "samples/client_relay_non_json.json")

relay, err = provider.RelayWithCtx(context.Background(), "https://dummy.com", &RelayInput{}, nil)
c.Equal(ErrNonJSONResponse, err)
c.Equal(ErrNonJSONResponse, err.Error)
c.Empty(relay)
}
11 changes: 9 additions & 2 deletions provider/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,15 @@ type RelayInput struct {

// RelayOutput represents the Relay RPC output
type RelayOutput struct {
Response string `json:"response"`
Signature string `json:"signature"`
Response string `json:"response"`
Signature string `json:"signature"`
StatusCode int `json:"statusCode"`
}

// RelayOutputErr represents the RPC output error
type RelayOutputErr struct {
Error error `json:"error"`
StatusCode int `json:"statusCode"`
}

// RelayMeta represents metadata of a relay
Expand Down
18 changes: 9 additions & 9 deletions relayer/relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var (

// Provider interface representing provider functions necessary for Relayer Package
type Provider interface {
RelayWithCtx(ctx context.Context, rpcURL string, input *provider.RelayInput, options *provider.RelayRequestOptions) (*provider.RelayOutput, error)
RelayWithCtx(ctx context.Context, rpcURL string, input *provider.RelayInput, options *provider.RelayRequestOptions) (*provider.RelayOutput, *provider.RelayOutputErr)
}

// Signer interface representing signer functions necessary for Relayer Package
Expand Down Expand Up @@ -172,30 +172,30 @@ func (r *Relayer) buildRelay(
}

// Relay does relay request with given input
func (r *Relayer) Relay(input *Input, options *provider.RelayRequestOptions) (*Output, error) {
func (r *Relayer) Relay(input *Input, options *provider.RelayRequestOptions) (*Output, *provider.RelayOutputErr) {
return r.RelayWithCtx(context.Background(), input, options)
}

// RelayWithCtx does relay request with given input
func (r *Relayer) RelayWithCtx(ctx context.Context, input *Input, options *provider.RelayRequestOptions) (*Output, error) {
func (r *Relayer) RelayWithCtx(ctx context.Context, input *Input, options *provider.RelayRequestOptions) (*Output, *provider.RelayOutputErr) {
err := r.validateRelayRequest(input)
if err != nil {
return nil, err
return nil, &provider.RelayOutputErr{Error: err}
}

node, err := getNode(input)
if err != nil {
return nil, err
return nil, &provider.RelayOutputErr{Error: err}
}

relayInput, err := r.buildRelay(node, input, options)
if err != nil {
return nil, err
return nil, &provider.RelayOutputErr{Error: err}
}

relayOutput, err := r.provider.RelayWithCtx(ctx, node.ServiceURL, relayInput, options)
if err != nil {
return nil, err
relayOutput, relayErr := r.provider.RelayWithCtx(ctx, node.ServiceURL, relayInput, options)
if relayErr != nil {
return nil, relayErr
}

return &Output{
Expand Down
Loading

0 comments on commit 8eba803

Please sign in to comment.