2023-07-04 12:12:59 +00:00
|
|
|
package putsvc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"crypto/sha256"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"hash"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/client"
|
2024-03-28 10:46:19 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
2023-07-04 12:12:59 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
2023-07-27 13:28:02 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/object"
|
2024-08-09 07:38:45 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/policy"
|
2023-07-04 12:12:59 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network"
|
2024-08-30 09:09:14 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/common/target"
|
|
|
|
objectwriter "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/common/writer"
|
2023-07-04 12:12:59 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/internal"
|
|
|
|
svcutil "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/util"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object_manager/placement"
|
2023-09-27 08:02:06 +00:00
|
|
|
tracingPkg "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/tracing"
|
2023-07-04 12:12:59 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
|
2024-11-07 14:32:10 +00:00
|
|
|
objectAPI "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/rpc"
|
|
|
|
rawclient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/rpc/client"
|
|
|
|
sessionV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/signature"
|
2023-07-04 12:12:59 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
2024-03-28 10:46:19 +00:00
|
|
|
containerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
2023-07-04 12:12:59 +00:00
|
|
|
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
|
|
|
"git.frostfs.info/TrueCloudLab/tzhash/tz"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
2024-05-16 07:05:17 +00:00
|
|
|
var errInvalidPayloadChecksum = errors.New("incorrect payload checksum")
|
2024-03-11 14:55:50 +00:00
|
|
|
|
2023-07-04 12:12:59 +00:00
|
|
|
type putSingleRequestSigner struct {
|
|
|
|
req *objectAPI.PutSingleRequest
|
|
|
|
keyStorage *svcutil.KeyStorage
|
|
|
|
signer *sync.Once
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *putSingleRequestSigner) GetRequestWithSignedHeader() (*objectAPI.PutSingleRequest, error) {
|
|
|
|
var resErr error
|
|
|
|
s.signer.Do(func() {
|
|
|
|
metaHdr := new(sessionV2.RequestMetaHeader)
|
|
|
|
meta := s.req.GetMetaHeader()
|
|
|
|
|
|
|
|
metaHdr.SetTTL(meta.GetTTL() - 1)
|
|
|
|
metaHdr.SetOrigin(meta)
|
|
|
|
s.req.SetMetaHeader(metaHdr)
|
|
|
|
|
|
|
|
privateKey, err := s.keyStorage.GetKey(nil)
|
|
|
|
if err != nil {
|
|
|
|
resErr = err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
resErr = signature.SignServiceMessage(privateKey, s.req)
|
|
|
|
})
|
|
|
|
return s.req, resErr
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) PutSingle(ctx context.Context, req *objectAPI.PutSingleRequest) (*objectAPI.PutSingleResponse, error) {
|
|
|
|
ctx, span := tracing.StartSpanFromContext(ctx, "putsvc.PutSingle")
|
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
obj := objectSDK.NewFromV2(req.GetBody().GetObject())
|
|
|
|
|
2023-07-27 13:28:02 +00:00
|
|
|
meta, err := s.validatePutSingle(ctx, obj)
|
|
|
|
if err != nil {
|
2023-07-04 12:12:59 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-07-27 13:28:02 +00:00
|
|
|
if err := s.saveToNodes(ctx, obj, req, meta); err != nil {
|
2023-07-04 12:12:59 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resp := &objectAPI.PutSingleResponse{}
|
|
|
|
resp.SetBody(&objectAPI.PutSingleResponseBody{})
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2023-07-27 13:28:02 +00:00
|
|
|
func (s *Service) validatePutSingle(ctx context.Context, obj *objectSDK.Object) (object.ContentMeta, error) {
|
2023-07-04 12:12:59 +00:00
|
|
|
if err := s.validarePutSingleSize(obj); err != nil {
|
2023-07-27 13:28:02 +00:00
|
|
|
return object.ContentMeta{}, err
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.validatePutSingleChecksum(obj); err != nil {
|
2023-07-27 13:28:02 +00:00
|
|
|
return object.ContentMeta{}, err
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return s.validatePutSingleObject(ctx, obj)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) validarePutSingleSize(obj *objectSDK.Object) error {
|
|
|
|
if uint64(len(obj.Payload())) != obj.PayloadSize() {
|
2024-08-30 09:09:14 +00:00
|
|
|
return target.ErrWrongPayloadSize
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
maxAllowedSize := s.Config.MaxSizeSrc.MaxObjectSize()
|
2023-07-04 12:12:59 +00:00
|
|
|
if obj.PayloadSize() > maxAllowedSize {
|
2024-08-30 09:09:14 +00:00
|
|
|
return target.ErrExceedingMaxSize
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) validatePutSingleChecksum(obj *objectSDK.Object) error {
|
|
|
|
cs, csSet := obj.PayloadChecksum()
|
|
|
|
if !csSet {
|
|
|
|
return errors.New("missing payload checksum")
|
|
|
|
}
|
|
|
|
|
|
|
|
var hash hash.Hash
|
|
|
|
|
|
|
|
switch typ := cs.Type(); typ {
|
|
|
|
default:
|
|
|
|
return fmt.Errorf("unsupported payload checksum type %v", typ)
|
|
|
|
case checksum.SHA256:
|
|
|
|
hash = sha256.New()
|
|
|
|
case checksum.TZ:
|
|
|
|
hash = tz.New()
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := hash.Write(obj.Payload()); err != nil {
|
|
|
|
return fmt.Errorf("could not compute payload hash: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !bytes.Equal(hash.Sum(nil), cs.Value()) {
|
2024-03-11 14:55:50 +00:00
|
|
|
return errInvalidPayloadChecksum
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-07-27 13:28:02 +00:00
|
|
|
func (s *Service) validatePutSingleObject(ctx context.Context, obj *objectSDK.Object) (object.ContentMeta, error) {
|
2024-08-30 09:09:14 +00:00
|
|
|
if err := s.FormatValidator.Validate(ctx, obj, false); err != nil {
|
2023-07-27 13:28:02 +00:00
|
|
|
return object.ContentMeta{}, fmt.Errorf("coud not validate object format: %w", err)
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
meta, err := s.FormatValidator.ValidateContent(obj)
|
2023-07-04 12:12:59 +00:00
|
|
|
if err != nil {
|
2023-07-27 13:28:02 +00:00
|
|
|
return object.ContentMeta{}, fmt.Errorf("could not validate payload content: %w", err)
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 13:28:02 +00:00
|
|
|
return meta, nil
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
2023-07-27 13:28:02 +00:00
|
|
|
func (s *Service) saveToNodes(ctx context.Context, obj *objectSDK.Object, req *objectAPI.PutSingleRequest, meta object.ContentMeta) error {
|
2023-07-04 12:12:59 +00:00
|
|
|
localOnly := req.GetMetaHeader().GetTTL() <= 1
|
2024-03-28 10:46:19 +00:00
|
|
|
placement, err := s.getPutSinglePlacementOptions(obj, req.GetBody().GetCopiesNumber(), localOnly)
|
2023-07-04 12:12:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-08-23 17:31:03 +00:00
|
|
|
|
2024-03-28 10:46:19 +00:00
|
|
|
if placement.isEC {
|
|
|
|
return s.saveToECReplicas(ctx, placement, obj, req, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.saveToREPReplicas(ctx, placement, obj, localOnly, req, meta)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) saveToREPReplicas(ctx context.Context, placement putSinglePlacement, obj *objectSDK.Object, localOnly bool, req *objectAPI.PutSingleRequest, meta object.ContentMeta) error {
|
2024-08-30 09:09:14 +00:00
|
|
|
iter := s.Config.NewNodeIterator(placement.placementOptions)
|
|
|
|
iter.ExtraBroadcastEnabled = objectwriter.NeedAdditionalBroadcast(obj, localOnly)
|
|
|
|
iter.ResetSuccessAfterOnBroadcast = placement.resetSuccessAfterOnBroadcast
|
2023-08-23 17:31:03 +00:00
|
|
|
|
2023-07-04 12:12:59 +00:00
|
|
|
signer := &putSingleRequestSigner{
|
|
|
|
req: req,
|
2024-08-30 09:09:14 +00:00
|
|
|
keyStorage: s.Config.KeyStorage,
|
2023-07-04 12:12:59 +00:00
|
|
|
signer: &sync.Once{},
|
|
|
|
}
|
2023-08-23 17:31:03 +00:00
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
return iter.ForEachNode(ctx, func(ctx context.Context, nd objectwriter.NodeDescriptor) error {
|
2024-10-01 12:27:06 +00:00
|
|
|
return s.saveToPlacementNode(ctx, &nd, obj, signer, meta, placement.container)
|
2023-08-23 17:31:03 +00:00
|
|
|
})
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
2024-03-28 10:46:19 +00:00
|
|
|
func (s *Service) saveToECReplicas(ctx context.Context, placement putSinglePlacement, obj *objectSDK.Object, req *objectAPI.PutSingleRequest, meta object.ContentMeta) error {
|
|
|
|
commonPrm, err := svcutil.CommonPrmFromV2(req)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-08-30 09:09:14 +00:00
|
|
|
key, err := s.Config.KeyStorage.GetKey(nil)
|
2024-03-28 10:46:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
signer := &putSingleRequestSigner{
|
|
|
|
req: req,
|
2024-08-30 09:09:14 +00:00
|
|
|
keyStorage: s.Config.KeyStorage,
|
2024-03-28 10:46:19 +00:00
|
|
|
signer: &sync.Once{},
|
|
|
|
}
|
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
w := objectwriter.ECWriter{
|
|
|
|
Config: s.Config,
|
|
|
|
PlacementOpts: placement.placementOptions,
|
|
|
|
ObjectMeta: meta,
|
|
|
|
ObjectMetaValid: true,
|
|
|
|
CommonPrm: commonPrm,
|
|
|
|
Container: placement.container,
|
|
|
|
Key: key,
|
|
|
|
Relay: func(ctx context.Context, ni client.NodeInfo, mac client.MultiAddressClient) error {
|
2024-03-28 10:46:19 +00:00
|
|
|
return s.redirectPutSingleRequest(ctx, signer, obj, ni, mac)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return w.WriteObject(ctx, obj)
|
|
|
|
}
|
|
|
|
|
|
|
|
type putSinglePlacement struct {
|
2024-08-23 09:28:09 +00:00
|
|
|
placementOptions []placement.Option
|
|
|
|
isEC bool
|
|
|
|
container containerSDK.Container
|
|
|
|
resetSuccessAfterOnBroadcast bool
|
2024-03-28 10:46:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Service) getPutSinglePlacementOptions(obj *objectSDK.Object, copiesNumber []uint32, localOnly bool) (putSinglePlacement, error) {
|
|
|
|
var result putSinglePlacement
|
|
|
|
|
2023-07-04 12:12:59 +00:00
|
|
|
cnrID, ok := obj.ContainerID()
|
|
|
|
if !ok {
|
2024-03-28 10:46:19 +00:00
|
|
|
return result, errors.New("missing container ID")
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
2024-08-30 09:09:14 +00:00
|
|
|
cnrInfo, err := s.Config.ContainerSource.Get(cnrID)
|
2023-07-04 12:12:59 +00:00
|
|
|
if err != nil {
|
2024-03-28 10:46:19 +00:00
|
|
|
return result, fmt.Errorf("could not get container by ID: %w", err)
|
|
|
|
}
|
|
|
|
result.container = cnrInfo.Value
|
|
|
|
result.isEC = container.IsECContainer(cnrInfo.Value) && object.IsECSupported(obj)
|
|
|
|
if len(copiesNumber) > 0 && !result.isEC {
|
|
|
|
result.placementOptions = append(result.placementOptions, placement.WithCopyNumbers(copiesNumber))
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
2024-08-09 07:38:45 +00:00
|
|
|
if container.IsECContainer(cnrInfo.Value) && !object.IsECSupported(obj) && !localOnly {
|
|
|
|
result.placementOptions = append(result.placementOptions, placement.SuccessAfter(uint32(policy.ECParityCount(cnrInfo.Value.PlacementPolicy())+1)))
|
2024-08-23 09:28:09 +00:00
|
|
|
result.resetSuccessAfterOnBroadcast = true
|
2024-08-09 07:38:45 +00:00
|
|
|
}
|
2024-03-28 10:46:19 +00:00
|
|
|
result.placementOptions = append(result.placementOptions, placement.ForContainer(cnrInfo.Value))
|
2023-07-04 12:12:59 +00:00
|
|
|
|
|
|
|
objID, ok := obj.ID()
|
|
|
|
if !ok {
|
2024-03-28 10:46:19 +00:00
|
|
|
return result, errors.New("missing object ID")
|
|
|
|
}
|
|
|
|
if obj.ECHeader() != nil {
|
|
|
|
objID = obj.ECHeader().Parent()
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
2024-03-28 10:46:19 +00:00
|
|
|
result.placementOptions = append(result.placementOptions, placement.ForObject(objID))
|
2023-07-04 12:12:59 +00:00
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
latestNetmap, err := netmap.GetLatestNetworkMap(s.Config.NetmapSource)
|
2023-07-04 12:12:59 +00:00
|
|
|
if err != nil {
|
2024-03-28 10:46:19 +00:00
|
|
|
return result, fmt.Errorf("could not get latest network map: %w", err)
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
builder := placement.NewNetworkMapBuilder(latestNetmap)
|
|
|
|
if localOnly {
|
2024-03-28 10:46:19 +00:00
|
|
|
result.placementOptions = append(result.placementOptions, placement.SuccessAfter(1))
|
2024-08-30 09:09:14 +00:00
|
|
|
builder = svcutil.NewLocalPlacement(builder, s.Config.NetmapKeys)
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
2024-03-28 10:46:19 +00:00
|
|
|
result.placementOptions = append(result.placementOptions, placement.UseBuilder(builder))
|
2023-07-04 12:12:59 +00:00
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
func (s *Service) saveToPlacementNode(ctx context.Context, nodeDesc *objectwriter.NodeDescriptor, obj *objectSDK.Object,
|
2024-10-01 12:27:06 +00:00
|
|
|
signer *putSingleRequestSigner, meta object.ContentMeta, container containerSDK.Container,
|
2023-10-31 11:56:55 +00:00
|
|
|
) error {
|
2024-08-30 09:09:14 +00:00
|
|
|
if nodeDesc.Local {
|
2024-10-01 12:27:06 +00:00
|
|
|
return s.saveLocal(ctx, obj, meta, container)
|
2023-07-04 12:12:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var info client.NodeInfo
|
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
client.NodeInfoFromNetmapElement(&info, nodeDesc.Info)
|
2023-07-04 12:12:59 +00:00
|
|
|
|
2024-08-30 09:09:14 +00:00
|
|
|
c, err := s.Config.ClientConstructor.Get(info)
|
2023-07-04 12:12:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not create SDK client %s: %w", info.AddressGroup(), err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return s.redirectPutSingleRequest(ctx, signer, obj, info, c)
|
|
|
|
}
|
|
|
|
|
2024-10-01 12:27:06 +00:00
|
|
|
func (s *Service) saveLocal(ctx context.Context, obj *objectSDK.Object, meta object.ContentMeta, container containerSDK.Container) error {
|
2024-08-30 09:09:14 +00:00
|
|
|
localTarget := &objectwriter.LocalTarget{
|
2024-10-01 12:27:06 +00:00
|
|
|
Storage: s.Config.LocalStore,
|
|
|
|
Container: container,
|
2023-07-27 13:28:02 +00:00
|
|
|
}
|
|
|
|
return localTarget.WriteObject(ctx, obj, meta)
|
|
|
|
}
|
|
|
|
|
2023-07-04 12:12:59 +00:00
|
|
|
func (s *Service) redirectPutSingleRequest(ctx context.Context,
|
|
|
|
signer *putSingleRequestSigner,
|
|
|
|
obj *objectSDK.Object,
|
|
|
|
info client.NodeInfo,
|
2023-10-31 11:56:55 +00:00
|
|
|
c client.MultiAddressClient,
|
|
|
|
) error {
|
2023-07-04 12:12:59 +00:00
|
|
|
ctx, span := tracing.StartSpanFromContext(ctx, "putService.redirectPutSingleRequest")
|
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
var req *objectAPI.PutSingleRequest
|
|
|
|
var firstErr error
|
|
|
|
req, firstErr = signer.GetRequestWithSignedHeader()
|
|
|
|
if firstErr != nil {
|
|
|
|
return firstErr
|
|
|
|
}
|
|
|
|
|
|
|
|
info.AddressGroup().IterateAddresses(func(addr network.Address) (stop bool) {
|
|
|
|
ctx, span := tracing.StartSpanFromContext(ctx, "putService.redirectPutSingleRequest.IterateAddresses",
|
|
|
|
trace.WithAttributes(
|
2023-09-27 08:02:06 +00:00
|
|
|
attribute.String("address", addr.String())))
|
2023-07-04 12:12:59 +00:00
|
|
|
defer span.End()
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
if err != nil {
|
|
|
|
objID, _ := obj.ID()
|
|
|
|
cnrID, _ := obj.ContainerID()
|
2024-10-21 07:22:54 +00:00
|
|
|
s.Config.Logger.Warn(ctx, logs.PutSingleRedirectFailure,
|
2023-07-04 12:12:59 +00:00
|
|
|
zap.Error(err),
|
|
|
|
zap.Stringer("address", addr),
|
|
|
|
zap.Stringer("object_id", objID),
|
|
|
|
zap.Stringer("container_id", cnrID),
|
2023-09-27 08:02:06 +00:00
|
|
|
zap.String("trace_id", tracingPkg.GetTraceID(ctx)),
|
2023-07-04 12:12:59 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
stop = err == nil
|
|
|
|
if stop || firstErr == nil {
|
|
|
|
firstErr = err
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
var resp *objectAPI.PutSingleResponse
|
|
|
|
|
|
|
|
err = c.RawForAddress(ctx, addr, func(cli *rawclient.Client) error {
|
|
|
|
var e error
|
|
|
|
resp, e = rpc.PutSingleObject(cli, req, rawclient.WithContext(ctx))
|
|
|
|
return e
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("failed to execute request: %w", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = internal.VerifyResponseKeyV2(info.PublicKey(), resp); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = signature.VerifyServiceMessage(resp)
|
|
|
|
if err != nil {
|
|
|
|
err = fmt.Errorf("response verification failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
})
|
|
|
|
|
|
|
|
return firstErr
|
|
|
|
}
|