From 2063d01cdb02c7ef26dc6d917e3bca252db5d5a8 Mon Sep 17 00:00:00 2001 From: kim <89579420+NyaaaWhatsUpDoc@users.noreply.github.com> Date: Sun, 21 May 2023 17:59:14 +0100 Subject: [bugfix] Add back removed ValidateRequest() before backoff-retry loop (#1805) * add back removed ValidateRequest() before backoff-retry loop Signed-off-by: kim * include response body in error response log Signed-off-by: kim * improved error response body draining Signed-off-by: kim * add more code commenting Signed-off-by: kim * move new error response logic to gtserror, handle instead in transport.Transport{} impl Signed-off-by: kim * appease ye oh mighty linter Signed-off-by: kim * fix mockhttpclient not setting request in http response Signed-off-by: kim --------- Signed-off-by: kim --- internal/gtserror/error.go | 4 +- internal/gtserror/new.go | 66 +++++++++++++++++++++++++++++++ internal/gtserror/new_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++ internal/gtserror/util.go | 42 ++++++++++++++++++++ 4 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 internal/gtserror/new.go create mode 100644 internal/gtserror/new_test.go create mode 100644 internal/gtserror/util.go (limited to 'internal/gtserror') diff --git a/internal/gtserror/error.go b/internal/gtserror/error.go index 56e546cf1..e68ed7d3b 100644 --- a/internal/gtserror/error.go +++ b/internal/gtserror/error.go @@ -34,8 +34,8 @@ const ( notFoundKey errorTypeKey - // error types - TypeSMTP ErrorType = "smtp" // smtp (mail) error + // Types returnable from Type(...). + TypeSMTP ErrorType = "smtp" // smtp (mail) ) // StatusCode checks error for a stored status code value. For example diff --git a/internal/gtserror/new.go b/internal/gtserror/new.go new file mode 100644 index 000000000..ad20e5cac --- /dev/null +++ b/internal/gtserror/new.go @@ -0,0 +1,66 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtserror + +import ( + "errors" + "net/http" + + "codeberg.org/gruf/go-byteutil" +) + +// NewResponseError crafts an error from provided HTTP response +// including the method, status and body (if any provided). This +// will also wrap the returned error using WithStatusCode(). +func NewResponseError(rsp *http.Response) error { + var buf byteutil.Buffer + + // Get URL string ahead of time. + urlStr := rsp.Request.URL.String() + + // Alloc guesstimate of required buf size. + buf.Guarantee(0 + + len(rsp.Request.Method) + + 12 + // request to + len(urlStr) + + 17 + // failed: status=" + len(rsp.Status) + + 8 + // " body=" + 256 + // max body size + 1, // " + ) + + // Build error message string without + // using "fmt", as chances are this will + // be used in a hot code path and we + // know all the incoming types involved. + _, _ = buf.WriteString(rsp.Request.Method) + _, _ = buf.WriteString(" request to ") + _, _ = buf.WriteString(urlStr) + _, _ = buf.WriteString(" failed: status=\"") + _, _ = buf.WriteString(rsp.Status) + _, _ = buf.WriteString("\" body=\"") + _, _ = buf.WriteString(drainBody(rsp.Body, 256)) + _, _ = buf.WriteString("\"") + + // Create new error from msg. + err := errors.New(buf.String()) + + // Wrap error to provide status code. + return WithStatusCode(err, rsp.StatusCode) +} diff --git a/internal/gtserror/new_test.go b/internal/gtserror/new_test.go new file mode 100644 index 000000000..b0824b5a7 --- /dev/null +++ b/internal/gtserror/new_test.go @@ -0,0 +1,91 @@ +package gtserror_test + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +func TestResponseError(t *testing.T) { + testResponseError(t, http.Response{ + Body: toBody(`{"error": "user not found"}`), + Request: &http.Request{ + Method: "GET", + URL: toURL("https://google.com/users/sundar"), + }, + Status: "404 Not Found", + }) + testResponseError(t, http.Response{ + Body: toBody("Unauthorized"), + Request: &http.Request{ + Method: "POST", + URL: toURL("https://google.com/inbox"), + }, + Status: "401 Unauthorized", + }) + testResponseError(t, http.Response{ + Body: toBody(""), + Request: &http.Request{ + Method: "GET", + URL: toURL("https://google.com/users/sundar"), + }, + Status: "404 Not Found", + }) +} + +func testResponseError(t *testing.T, rsp http.Response) { + var body string + if rsp.Body == http.NoBody { + body = "" + } else { + var b []byte + rsp.Body, b = copyBody(rsp.Body) + trunc := len(b) + if trunc > 256 { + trunc = 256 + } + body = string(b[:trunc]) + } + expect := fmt.Sprintf( + "%s request to %s failed: status=\"%s\" body=\"%s\"", + rsp.Request.Method, + rsp.Request.URL.String(), + rsp.Status, + body, + ) + err := gtserror.NewResponseError(&rsp) + if str := err.Error(); str != expect { + t.Errorf("unexpected error string: recv=%q expct=%q", str, expect) + } +} + +func toURL(u string) *url.URL { + url, err := url.Parse(u) + if err != nil { + panic(err) + } + return url +} + +func toBody(s string) io.ReadCloser { + if s == "" { + return http.NoBody + } + r := strings.NewReader(s) + return io.NopCloser(r) +} + +func copyBody(rc io.ReadCloser) (io.ReadCloser, []byte) { + b, err := io.ReadAll(rc) + if err != nil { + panic(err) + } + r := bytes.NewReader(b) + return io.NopCloser(r), b +} diff --git a/internal/gtserror/util.go b/internal/gtserror/util.go new file mode 100644 index 000000000..635518b76 --- /dev/null +++ b/internal/gtserror/util.go @@ -0,0 +1,42 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package gtserror + +import ( + "io" + + "codeberg.org/gruf/go-byteutil" +) + +// drainBody will produce a truncated output of the content +// of given io.ReadCloser body, useful for logs / errors. +func drainBody(body io.ReadCloser, trunc int) string { + // Limit response to 'trunc' bytes. + buf := make([]byte, trunc) + + // Read body into err buffer. + n, _ := io.ReadFull(body, buf) + + if n == 0 { + // No error body, return + // reasonable error str. + return "" + } + + return byteutil.B2S(buf[:n]) +} -- cgit v1.2.3