errorhandling.go (6488B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package util 19 20 import ( 21 "context" 22 "net/http" 23 24 "codeberg.org/gruf/go-kv" 25 "github.com/gin-gonic/gin" 26 apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" 27 "github.com/superseriousbusiness/gotosocial/internal/gtscontext" 28 "github.com/superseriousbusiness/gotosocial/internal/gtserror" 29 "github.com/superseriousbusiness/gotosocial/internal/log" 30 ) 31 32 // TODO: add more templated html pages here for different error types 33 34 // NotFoundHandler serves a 404 html page through the provided gin context, 35 // if accept is 'text/html', or just returns a json error if 'accept' is empty 36 // or application/json. 37 // 38 // When serving html, NotFoundHandler calls the provided InstanceGet function 39 // to fetch the apimodel representation of the instance, for serving in the 40 // 404 header and footer. 41 // 42 // If an error is returned by InstanceGet, the function will panic. 43 func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode), accept string) { 44 switch accept { 45 case string(TextHTML): 46 ctx := c.Request.Context() 47 instance, err := instanceGet(ctx) 48 if err != nil { 49 panic(err) 50 } 51 52 c.HTML(http.StatusNotFound, "404.tmpl", gin.H{ 53 "instance": instance, 54 "requestID": gtscontext.RequestID(ctx), 55 }) 56 default: 57 c.JSON(http.StatusNotFound, gin.H{ 58 "error": http.StatusText(http.StatusNotFound), 59 }) 60 } 61 } 62 63 // genericErrorHandler is a more general version of the NotFoundHandler, which can 64 // be used for serving either generic error pages with some rendered help text, 65 // or just some error json if the caller prefers (or has no preference). 66 func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) { 67 switch accept { 68 case string(TextHTML): 69 ctx := c.Request.Context() 70 instance, err := instanceGet(ctx) 71 if err != nil { 72 panic(err) 73 } 74 75 c.HTML(errWithCode.Code(), "error.tmpl", gin.H{ 76 "instance": instance, 77 "code": errWithCode.Code(), 78 "error": errWithCode.Safe(), 79 "requestID": gtscontext.RequestID(ctx), 80 }) 81 default: 82 c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) 83 } 84 } 85 86 // ErrorHandler takes the provided gin context and errWithCode 87 // and tries to serve a helpful error to the caller. 88 // 89 // It will do content negotiation to figure out if the caller prefers 90 // to see an html page with the error rendered there. If not, or if 91 // something goes wrong during the function, it will recover and just 92 // try to serve an appropriate application/json content-type error. 93 // To override the default response type, specify `offers`. 94 // 95 // If the requester already hung up on the request, ErrorHandler 96 // will overwrite the given errWithCode with a 499 error to indicate 97 // that the failure wasn't due to something we did, and will avoid 98 // trying to write extensive bytes to the caller by just aborting. 99 // 100 // See: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#nginx. 101 func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode), offers ...MIME) { 102 if c.Request.Context().Err() != nil { 103 // Context error means requester probably left already. 104 // Wrap original error with a less alarming one. Then 105 // we can return early, because it doesn't matter what 106 // we send to the client at this point; they're gone. 107 errWithCode = gtserror.NewErrorClientClosedRequest(errWithCode.Unwrap()) 108 c.AbortWithStatus(errWithCode.Code()) 109 return 110 } 111 112 // Set the error on the gin context so that it can be logged 113 // in the gin logger middleware (internal/middleware/logger.go). 114 c.Error(errWithCode) //nolint:errcheck 115 116 // Discover if we're allowed to serve a nice html error page, 117 // or if we should just use a json. Normally we would want to 118 // check for a returned error, but if an error occurs here we 119 // can just fall back to default behavior (serve json error). 120 // Prefer provided offers, fall back to JSON or HTML. 121 accept, _ := NegotiateAccept(c, append(offers, JSONOrHTMLAcceptHeaders...)...) 122 123 if errWithCode.Code() == http.StatusNotFound { 124 // Use our special not found handler with useful status text. 125 NotFoundHandler(c, instanceGet, accept) 126 } else { 127 genericErrorHandler(c, instanceGet, accept, errWithCode) 128 } 129 } 130 131 // WebErrorHandler is like ErrorHandler, but will display HTML over JSON by default. 132 func WebErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode)) { 133 ErrorHandler(c, errWithCode, instanceGet, TextHTML, AppJSON) 134 } 135 136 // OAuthErrorHandler is a lot like ErrorHandler, but it specifically returns errors 137 // that are compatible with https://datatracker.ietf.org/doc/html/rfc6749#section-5.2, 138 // but serializing errWithCode.Error() in the 'error' field, and putting any help text 139 // from the error in the 'error_description' field. This means you should be careful not 140 // to pass any detailed errors (that might contain sensitive information) into the 141 // errWithCode.Error() field, since the client will see this. Use your noggin! 142 func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) { 143 l := log.WithContext(c.Request.Context()). 144 WithFields(kv.Fields{ 145 {"path", c.Request.URL.Path}, 146 {"error", errWithCode.Error()}, 147 {"help", errWithCode.Safe()}, 148 }...) 149 150 statusCode := errWithCode.Code() 151 152 if statusCode == http.StatusInternalServerError { 153 l.Error("Internal Server Error") 154 } else { 155 l.Debug("handling OAuth error") 156 } 157 158 c.JSON(statusCode, gin.H{ 159 "error": errWithCode.Error(), 160 "error_description": errWithCode.Safe(), 161 }) 162 }