gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }