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 <[email protected]>

* Optimize errorCode extraction

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

---------

Co-authored-by: Daniel Olshansky <[email protected]>
  • Loading branch information
ricantar and Olshansk committed May 16, 2024
1 parent e33f6c8 commit cce6c96
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 55 deletions.
115 changes: 106 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 (
// DefaultStatusCode means the response was accepted but we don't know if it actually succeeded
DefaultStatusCode = http.StatusAccepted
// 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,100 @@ func (p *Provider) DispatchWithCtx(ctx context.Context, appPublicKey, chain stri
}

// Relay does request to be relayed to a target blockchain
// Will always return with an output that includes the status code from the request
func (p *Provider) Relay(rpcURL string, input *RelayInput, options *RelayRequestOptions) (*RelayOutput, error) {
return p.RelayWithCtx(context.Background(), rpcURL, input, options)
}

// RelayWithCtx does request to be relayed to a target blockchain
// Will always return with an output that includes the status code from the request
func (p *Provider) RelayWithCtx(ctx context.Context, rpcURL string, input *RelayInput, options *RelayRequestOptions) (*RelayOutput, error) {
rawOutput, reqErr := p.doPostRequest(ctx, rpcURL, input, ClientRelayRoute, http.Header{})

defer closeOrLog(rawOutput)

statusCode := extractStatusFromRequest(rawOutput, reqErr)

defaultOutput := &RelayOutput{
StatusCode: statusCode,
}

if reqErr != nil && !errors.Is(reqErr, errOnRelayRequest) {
return nil, reqErr
return defaultOutput, reqErr
}

bodyBytes, err := ioutil.ReadAll(rawOutput.Body)
bodyBytes, err := io.ReadAll(rawOutput.Body)
if err != nil {
return nil, err
return defaultOutput, err
}

if errors.Is(reqErr, errOnRelayRequest) {
return nil, parseRelayErrorOutput(bodyBytes, input.Proof.ServicerPubKey)
return defaultOutput, parseRelayErrorOutput(bodyBytes, input.Proof.ServicerPubKey)
}

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

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 parseRelaySuccesfulOutput(bodyBytes)
return statusCode
}

func parseRelaySuccesfulOutput(bodyBytes []byte) (*RelayOutput, error) {
output := RelayOutput{}
// 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, requestStatusCode int) (*RelayOutput, error) {
output := RelayOutput{
StatusCode: requestStatusCode,
}

err := json.Unmarshal(bodyBytes, &output)
if err != nil {
return nil, err
return &output, 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 &output, ErrNonJSONResponse
}

return &output, nil
Expand Down
20 changes: 12 additions & 8 deletions provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,28 +769,30 @@ 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.Equal(http.StatusInternalServerError, relay.StatusCode)
c.False(IsErrorCode(EmptyPayloadDataError, err))
c.Empty(relay)
c.Empty(relay.Response)

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.Empty(relay)
c.Equal(http.StatusInternalServerError, relay.StatusCode)
c.Empty(relay.Response)

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.Empty(relay)
c.Empty(relay.Response)
}

func TestProvider_RelayWithCtx(t *testing.T) {
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.Equal(http.StatusInternalServerError, relay.StatusCode)
c.False(IsErrorCode(EmptyPayloadDataError, err))
c.Empty(relay)
c.Empty(relay.Response)

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.Empty(relay)
c.Equal(http.StatusInternalServerError, relay.StatusCode)
c.Empty(relay.Response)

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.Empty(relay)
c.Empty(relay.Response)
}
5 changes: 3 additions & 2 deletions provider/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ 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"`
}

// RelayMeta represents metadata of a relay
Expand Down
21 changes: 15 additions & 6 deletions relayer/relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,30 +172,39 @@ func (r *Relayer) buildRelay(
}

// Relay does relay request with given input
// Will always return with an output that includes the status code from the request
func (r *Relayer) Relay(input *Input, options *provider.RelayRequestOptions) (*Output, error) {
return r.RelayWithCtx(context.Background(), input, options)
}

// RelayWithCtx does relay request with given input
// Will always return with an output that includes the status code from the request
func (r *Relayer) RelayWithCtx(ctx context.Context, input *Input, options *provider.RelayRequestOptions) (*Output, error) {
defaultOutput := &Output{
RelayOutput: &provider.RelayOutput{
StatusCode: provider.DefaultStatusCode,
},
}

err := r.validateRelayRequest(input)
if err != nil {
return nil, err
return defaultOutput, err
}

node, err := getNode(input)
if err != nil {
return nil, err
return defaultOutput, err
}

relayInput, err := r.buildRelay(node, input, options)
if err != nil {
return nil, err
return defaultOutput, 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 {
defaultOutput.RelayOutput = relayOutput
return defaultOutput, relayErr
}

return &Output{
Expand Down
Loading

0 comments on commit cce6c96

Please sign in to comment.