diff options
author | 2023-05-21 17:59:14 +0100 | |
---|---|---|
committer | 2023-05-21 18:59:14 +0200 | |
commit | 2063d01cdb02c7ef26dc6d917e3bca252db5d5a8 (patch) | |
tree | ae220bc4956beb8bc09e3786f4ba8a0fb99b521b /internal/gtserror | |
parent | [feature] Make client IP logging configurable (#1799) (diff) | |
download | gotosocial-2063d01cdb02c7ef26dc6d917e3bca252db5d5a8.tar.xz |
[bugfix] Add back removed ValidateRequest() before backoff-retry loop (#1805)v0.9.0-rc2
* add back removed ValidateRequest() before backoff-retry loop
Signed-off-by: kim <grufwub@gmail.com>
* include response body in error response log
Signed-off-by: kim <grufwub@gmail.com>
* improved error response body draining
Signed-off-by: kim <grufwub@gmail.com>
* add more code commenting
Signed-off-by: kim <grufwub@gmail.com>
* move new error response logic to gtserror, handle instead in transport.Transport{} impl
Signed-off-by: kim <grufwub@gmail.com>
* appease ye oh mighty linter
Signed-off-by: kim <grufwub@gmail.com>
* fix mockhttpclient not setting request in http response
Signed-off-by: kim <grufwub@gmail.com>
---------
Signed-off-by: kim <grufwub@gmail.com>
Diffstat (limited to 'internal/gtserror')
-rw-r--r-- | internal/gtserror/error.go | 4 | ||||
-rw-r--r-- | internal/gtserror/new.go | 66 | ||||
-rw-r--r-- | internal/gtserror/new_test.go | 91 | ||||
-rw-r--r-- | internal/gtserror/util.go | 42 |
4 files changed, 201 insertions, 2 deletions
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 <http://www.gnu.org/licenses/>. + +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 = "<empty>" + } 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 <http://www.gnu.org/licenses/>. + +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 "<empty>" + } + + return byteutil.B2S(buf[:n]) +} |