diff --git a/catchup/universalFetcher.go b/catchup/universalFetcher.go index 0b1ab4abb0..6d9fcce8de 100644 --- a/catchup/universalFetcher.go +++ b/catchup/universalFetcher.go @@ -19,9 +19,9 @@ package catchup import ( "context" "encoding/binary" - "errors" "fmt" "net/http" + "strconv" "time" "github.com/algorand/go-deadlock" @@ -173,6 +173,9 @@ func (w *wsFetcherClient) requestBlock(ctx context.Context, round basics.Round) } if errMsg, found := resp.Topics.GetValue(network.ErrorKey); found { + if latest, lfound := resp.Topics.GetValue(rpcs.LatestRoundKey); lfound { + return nil, noBlockForRoundError{round: round, latest: basics.Round(binary.BigEndian.Uint64(latest))} + } return nil, makeErrWsFetcherRequestFailed(round, w.target.GetAddress(), string(errMsg)) } @@ -195,7 +198,11 @@ func (w *wsFetcherClient) requestBlock(ctx context.Context, round basics.Round) // set max fetcher size to 10MB, this is enough to fit the block and certificate const fetcherMaxBlockBytes = 10 << 20 -var errNoBlockForRound = errors.New("No block available for given round") +type noBlockForRoundError struct { + latest, round basics.Round +} + +func (noBlockForRoundError) Error() string { return "no block available for given round" } // HTTPFetcher implements FetcherClient doing an HTTP GET of the block type HTTPFetcher struct { @@ -239,7 +246,13 @@ func (hf *HTTPFetcher) getBlockBytes(ctx context.Context, r basics.Round) (data case http.StatusOK: case http.StatusNotFound: // server could not find a block with that round numbers. response.Body.Close() - return nil, errNoBlockForRound + noBlockErr := noBlockForRoundError{round: r} + if latestBytes := response.Header.Get(rpcs.BlockResponseLatestRoundHeader); latestBytes != "" { + if latest, pErr := strconv.ParseUint(latestBytes, 10, 64); pErr == nil { + noBlockErr.latest = basics.Round(latest) + } + } + return nil, noBlockErr default: bodyBytes, err := rpcs.ResponseBytes(response, hf.log, fetcherMaxBlockBytes) hf.log.Warnf("HTTPFetcher.getBlockBytes: response status code %d from '%s'. Response body '%s' ", response.StatusCode, blockURL, string(bodyBytes)) diff --git a/catchup/universalFetcher_test.go b/catchup/universalFetcher_test.go index 836360139f..c8dcbd9840 100644 --- a/catchup/universalFetcher_test.go +++ b/catchup/universalFetcher_test.go @@ -74,7 +74,9 @@ func TestUGetBlockWs(t *testing.T) { block, cert, duration, err = fetcher.fetchBlock(context.Background(), next+1, up) require.Error(t, err) - require.Contains(t, err.Error(), "requested block is not available") + require.Error(t, noBlockForRoundError{}, err) + require.Equal(t, next+1, err.(noBlockForRoundError).round) + require.Equal(t, next, err.(noBlockForRoundError).latest) require.Nil(t, block) require.Nil(t, cert) require.Equal(t, int64(duration), int64(0)) @@ -118,8 +120,10 @@ func TestUGetBlockHTTP(t *testing.T) { block, cert, duration, err = fetcher.fetchBlock(context.Background(), next+1, net.GetPeers()[0]) - require.Error(t, errNoBlockForRound, err) - require.Contains(t, err.Error(), "No block available for given round") + require.Error(t, noBlockForRoundError{}, err) + require.Equal(t, next+1, err.(noBlockForRoundError).round) + require.Equal(t, next, err.(noBlockForRoundError).latest) + require.Contains(t, err.Error(), "no block available for given round") require.Nil(t, block) require.Nil(t, cert) require.Equal(t, int64(duration), int64(0)) diff --git a/rpcs/blockService.go b/rpcs/blockService.go index a3bf886f2b..2d4a4b822e 100644 --- a/rpcs/blockService.go +++ b/rpcs/blockService.go @@ -54,6 +54,9 @@ const blockResponseRetryAfter = "3" const blockServerMaxBodyLength = 512 // we don't really pass meaningful content here, so 512 bytes should be a safe limit const blockServerCatchupRequestBufferSize = 10 +// BlockResponseLatestRoundHeader is returned in the response header when the requested block is not available +const BlockResponseLatestRoundHeader = "X-Latest-Round" + // BlockServiceBlockPath is the path to register BlockService as a handler for when using gorilla/mux // e.g. .Handle(BlockServiceBlockPath, &ls) const BlockServiceBlockPath = "/v{version:[0-9.]+}/{genesisID}/block/{round:[0-9a-z]+}" @@ -65,6 +68,7 @@ const ( BlockDataKey = "blockData" // Block-data topic-key in the response CertDataKey = "certData" // Cert-data topic-key in the response BlockAndCertValue = "blockAndCert" // block+cert request data (as the value of requestDataTypeKey) + LatestRoundKey = "latest" ) var errBlockServiceClosed = errors.New("block service is shutting down") @@ -239,12 +243,13 @@ func (bs *BlockService) ServeHTTP(response http.ResponseWriter, request *http.Re } encodedBlockCert, err := bs.rawBlockBytes(basics.Round(round)) if err != nil { - switch err.(type) { + switch lerr := err.(type) { case ledgercore.ErrNoEntry: // entry cound not be found. ok := bs.redirectRequest(round, response, request) if !ok { response.Header().Set("Cache-Control", blockResponseMissingBlockCacheControl) + response.Header().Set(BlockResponseLatestRoundHeader, fmt.Sprintf("%d", lerr.Latest)) response.WriteHeader(http.StatusNotFound) } return @@ -456,8 +461,12 @@ func (bs *BlockService) rawBlockBytes(round basics.Round) ([]byte, error) { func topicBlockBytes(log logging.Logger, dataLedger LedgerForBlockService, round basics.Round, requestType string) (network.Topics, uint64) { blk, cert, err := dataLedger.EncodedBlockCert(round) if err != nil { - switch err.(type) { + switch lerr := err.(type) { case ledgercore.ErrNoEntry: + return network.Topics{ + network.MakeTopic(network.ErrorKey, []byte(blockNotAvailableErrMsg)), + network.MakeTopic(LatestRoundKey, binary.BigEndian.AppendUint64([]byte{}, uint64(lerr.Latest))), + }, 0 default: log.Infof("BlockService topicBlockBytes: %s", err) }