From 83d40cdd361633b2e003258623f9c2080fcbb8dd Mon Sep 17 00:00:00 2001 From: AH-dark Date: Sun, 7 Jan 2024 15:07:22 +0800 Subject: [PATCH] fix: error when param `d` is "404" --- server/controllers/avatar/get_avatar.go | 4 +- services/avatar/get_gravatar.go | 73 ++++++++++++++++--------- services/avatar/get_qq_avatar.go | 47 ++++++++++++++-- services/avatar/req_tracing.go | 42 ++++++++++++++ services/avatar/service.go | 43 +++++---------- 5 files changed, 145 insertions(+), 64 deletions(-) create mode 100644 services/avatar/req_tracing.go diff --git a/server/controllers/avatar/get_avatar.go b/server/controllers/avatar/get_avatar.go index 1cd648a..f6f2346 100644 --- a/server/controllers/avatar/get_avatar.go +++ b/server/controllers/avatar/get_avatar.go @@ -21,7 +21,7 @@ type GetAvatarRequest struct { Size int64 `query:"s"` Default string `query:"d"` Rating string `query:"r"` - Force bool `query:"f"` + Force string `query:"f"` } func (h *handlers) GetAvatar(ctx context.Context, c *app.RequestContext) { @@ -38,7 +38,7 @@ func (h *handlers) GetAvatar(ctx context.Context, c *app.RequestContext) { args := avatar.GetAvatarArgs{ Size: req.Size, Default: req.Default, - ForceDefault: req.Force, + ForceDefault: req.Force == "y", Rating: req.Rating, EnableWebp: lo.SomeBy(strings.Split(bytestring.BytesToString(c.GetHeader("Accept")), ","), func(item string) bool { return strings.HasPrefix(item, "image/webp") diff --git a/services/avatar/get_gravatar.go b/services/avatar/get_gravatar.go index 44e03e7..42cd26f 100644 --- a/services/avatar/get_gravatar.go +++ b/services/avatar/get_gravatar.go @@ -3,9 +3,10 @@ package avatar import ( "context" "fmt" + "github.com/imroc/req/v3" + "github.com/samber/lo" "image" "image/jpeg" - "net/url" "strconv" "time" @@ -18,44 +19,64 @@ type GetGravatarResult struct { LastModified time.Time } -func (s *service) getGravatar(ctx context.Context, hash string, args GetAvatarArgs) (GetGravatarResult, error) { - ctx, span := tracer.Start(ctx, "service.AvatarService.getGravatar") - defer span.End() +func initGravatarClient() *req.Client { + c := req.C(). + EnableDumpEachRequest(). + SetBaseURL("https://gravatar.com/avatar/"). + SetCommonHeader("Accept", "image/jpeg"). + SetCommonQueryParams(map[string]string{ + "s": "80", + }). + OnAfterResponse(func(client *req.Client, resp *req.Response) error { + if resp.Err != nil { // There is an underlying error, e.g. network error or unmarshal error (SetSuccessResult or SetErrorResult was invoked before). + if dump := resp.Dump(); dump != "" { // Append dump content to original underlying error to help troubleshoot. + resp.Err = fmt.Errorf("error: %s\nraw content:\n%s", resp.Err.Error(), resp.Dump()) + } + return nil // Skip the following logic if there is an underlying error. + } - values := url.Values{} - values.Set("d", args.Default) - values.Set("s", strconv.FormatInt(args.Size, 10)) - values.Set("r", args.Rating) + if !resp.IsSuccessState() { + resp.Err = fmt.Errorf("bad response, raw content:\n%s", resp.Dump()) + } - // force default - if args.ForceDefault { - hash = "" - } + return nil + }). + WrapRoundTripFunc(WithTracer) - // get gravatar - u, err := url.Parse(fmt.Sprintf("https://www.gravatar.com/avatar/%s", hash)) - if err != nil { - otelzap.L().Ctx(ctx).Error("parse gravatar url failed", zap.Error(err)) - return GetGravatarResult{}, err - } - u.RawQuery = values.Encode() + return c +} - stream, resp, err := s.downloadAvatar(ctx, u.String()) - if err != nil { +func (s *service) getGravatar(ctx context.Context, hash string, args GetAvatarArgs) (GetGravatarResult, error) { + ctx, span := tracer.Start(ctx, "service.AvatarService.getGravatar") + defer span.End() + + resp, err := s.gravatarClient.R(). + SetContext(ctx). + SetPathParam("hash", lo.If(args.ForceDefault, "").Else(hash)). + SetQueryParams(map[string]string{ + "d": args.Default, + "s": strconv.FormatInt(args.Size, 10), + "r": args.Rating, + }). + Get("{hash}") + if resp.GetStatusCode() == 404 { + return GetGravatarResult{}, nil + } else if err != nil { otelzap.L().Ctx(ctx).Error("download gravatar failed", zap.Error(err)) return GetGravatarResult{}, err + } else if resp.IsErrorState() { + otelzap.L().Ctx(ctx).Error("download gravatar failed", zap.Error(resp.Err)) + return GetGravatarResult{}, fmt.Errorf("download gravatar failed: %v", resp.ErrorResult()) } - img, err := jpeg.Decode(stream) + defer resp.Body.Close() + + img, err := jpeg.Decode(resp.Body) if err != nil { otelzap.L().Ctx(ctx).Error("failed to decode gravatar", zap.Error(err)) return GetGravatarResult{}, err } - if err := stream.Close(); err != nil { - otelzap.L().Ctx(ctx).Warn("close stream failed", zap.Error(err)) - } - lastModified, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")) if err != nil { otelzap.L().Ctx(ctx).Warn("parse last modified failed", zap.Error(err)) diff --git a/services/avatar/get_qq_avatar.go b/services/avatar/get_qq_avatar.go index d848598..318a475 100644 --- a/services/avatar/get_qq_avatar.go +++ b/services/avatar/get_qq_avatar.go @@ -3,14 +3,45 @@ package avatar import ( "context" "fmt" + "github.com/imroc/req/v3" "go.uber.org/zap" "image" "image/jpeg" + "strconv" "github.com/nfnt/resize" "github.com/uptrace/opentelemetry-go-extra/otelzap" ) +func initQQClient() *req.Client { + c := req.C(). + EnableDumpEachRequest(). + SetBaseURL("https://q.qlogo.cn/"). + SetCommonHeader("Accept", "image/jpeg"). + SetCommonQueryParams(map[string]string{ + "dst_uin": "0", + "spec": "640", + "img_type": "jpg", + }). + OnAfterResponse(func(client *req.Client, resp *req.Response) error { + if resp.Err != nil { // There is an underlying error, e.g. network error or unmarshal error (SetSuccessResult or SetErrorResult was invoked before). + if dump := resp.Dump(); dump != "" { // Append dump content to original underlying error to help troubleshoot. + resp.Err = fmt.Errorf("error: %s\nraw content:\n%s", resp.Err.Error(), resp.Dump()) + } + return nil // Skip the following logic if there is an underlying error. + } + + if !resp.IsSuccessState() { + resp.Err = fmt.Errorf("bad response, raw content:\n%s", resp.Dump()) + } + + return nil + }). + WrapRoundTripFunc(WithTracer) + + return c +} + func (s *service) getQQAvatar(ctx context.Context, hash string, args GetAvatarArgs) (image.Image, error) { ctx, span := tracer.Start(ctx, "service.AvatarService.getQQAvatar") defer span.End() @@ -22,22 +53,26 @@ func (s *service) getQQAvatar(ctx context.Context, hash string, args GetAvatarAr return nil, err } - stream, _, err := s.downloadAvatar(ctx, fmt.Sprintf("https://q.qlogo.cn/headimg_dl?dst_uin=%d&spec=640&img_type=jpg", qqid)) + resp, err := s.qqAvatarClient.R(). + SetContext(ctx). + SetQueryParam("dst_uin", strconv.FormatInt(qqid, 10)). + Get("headimg_dl") if err != nil { otelzap.L().Ctx(ctx).Error("download qq avatar failed", zap.Error(err)) return nil, err + } else if resp.IsErrorState() { + otelzap.L().Ctx(ctx).Error("download qq avatar failed", zap.Error(err)) + return nil, fmt.Errorf("download qq avatar failed: %v", resp.ErrorResult()) } - img, err := jpeg.Decode(stream) + defer resp.Body.Close() + + img, err := jpeg.Decode(resp.Body) if err != nil { otelzap.L().Ctx(ctx).Error("decode qq avatar failed", zap.Error(err)) return nil, err } - if err := stream.Close(); err != nil { - otelzap.L().Ctx(ctx).Warn("close stream failed", zap.Error(err)) - } - img = resize.Resize(uint(args.Size), uint(args.Size), img, resize.Lanczos3) return img, nil diff --git a/services/avatar/req_tracing.go b/services/avatar/req_tracing.go new file mode 100644 index 0000000..9c3b2aa --- /dev/null +++ b/services/avatar/req_tracing.go @@ -0,0 +1,42 @@ +package avatar + +import ( + "github.com/imroc/req/v3" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +func WithTracer(rt req.RoundTripper) req.RoundTripFunc { + return func(req *req.Request) (resp *req.Response, err error) { + ctx := req.Context() + + ctx, span := tracer.Start(ctx, "req.httpRequest", trace.WithAttributes( + attribute.String("http.url", req.URL.String()), + attribute.String("http.method", req.Method), + attribute.String("http.req.header", req.HeaderToString()), + )) + defer span.End() + + if len(req.Body) > 0 { + span.SetAttributes( + attribute.String("http.req.body", string(req.Body)), + ) + } + + resp, err = rt.RoundTrip(req) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + + if resp.Response != nil { + span.SetAttributes( + attribute.Int("http.status_code", resp.StatusCode), + attribute.String("http.resp.header", resp.HeaderToString()), + ) + } + + return + } +} diff --git a/services/avatar/service.go b/services/avatar/service.go index 66758a9..40f6193 100644 --- a/services/avatar/service.go +++ b/services/avatar/service.go @@ -2,11 +2,8 @@ package avatar import ( "context" - "errors" - "fmt" + "github.com/samber/lo" "image/png" - "io" - "net/http" "time" "github.com/cloudwego/hertz/pkg/common/bytebufferpool" @@ -36,34 +33,18 @@ type Service interface { } type service struct { - fx.In + fx.In `ignore-unexported:"true"` MD5QQMappingRepo dal.MD5QQMappingRepo -} -func NewService(s service) Service { - return &s + qqAvatarClient *req.Client + gravatarClient *req.Client } -func (s *service) downloadAvatar(ctx context.Context, url string) (io.ReadCloser, *req.Response, error) { - ctx, span := tracer.Start(ctx, "service.AvatarService.downloadAvatar") - defer span.End() - - resp, err := req.NewRequest().SetContext(ctx).Get(url) - if err != nil { - otelzap.L().Ctx(ctx).Error("download avatar failed", zap.Error(err)) - return nil, resp, err - } - - if resp.StatusCode != 200 || resp.IsErrorState() { - otelzap.L().Ctx(ctx).Error("download avatar failed") - return nil, resp, errors.New(http.StatusText(resp.StatusCode)) - } - - if resp.IsSuccessState() { - return resp.Body, resp, nil - } +func NewService(s service) Service { + s.qqAvatarClient = initQQClient() + s.gravatarClient = initGravatarClient() - return nil, resp, fmt.Errorf("download avatar failed, status code: %d", resp.StatusCode) + return &s } func (s *service) GetAvatar(ctx context.Context, hash string, args GetAvatarArgs) ([]byte, time.Time, error) { @@ -76,13 +57,15 @@ func (s *service) GetAvatar(ctx context.Context, hash string, args GetAvatarArgs img, err := s.getQQAvatar(ctx, hash, args) if err != nil { res, err := s.getGravatar(ctx, hash, args) - if errors.Is(err, errors.New("404 Not Found")) { - return nil, time.Time{}, nil - } else if err != nil { + if err != nil { otelzap.L().Ctx(ctx).Warn("get gravatar failed", zap.Error(err)) return nil, time.Time{}, err } + if lo.IsEmpty(res) { + return nil, time.Time{}, nil + } + img = res.Avatar lastModified = res.LastModified }