goyave.dev/goyave/v4@v4.4.11/response.go (about)

     1  package goyave
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	htmltemplate "html/template"
    10  	"io"
    11  	"net"
    12  	"net/http"
    13  	"os"
    14  	"runtime/debug"
    15  	"strconv"
    16  	"text/template"
    17  
    18  	"gorm.io/gorm"
    19  	"goyave.dev/goyave/v4/config"
    20  	"goyave.dev/goyave/v4/util/fsutil"
    21  )
    22  
    23  var (
    24  	// ErrNotHijackable returned by response.Hijack() if the underlying
    25  	// http.ResponseWriter doesn't implement http.Hijacker. This can
    26  	// happen with HTTP/2 connections.
    27  	ErrNotHijackable = errors.New("Underlying http.ResponseWriter doesn't implement http.Hijacker")
    28  )
    29  
    30  // PreWriter is a writter that needs to alter the response headers or status
    31  // before they are written.
    32  // If implemented, PreWrite will be called right before the Write operation.
    33  type PreWriter interface {
    34  	PreWrite(b []byte)
    35  }
    36  
    37  // Response represents a controller response.
    38  type Response struct {
    39  	writer         io.Writer
    40  	responseWriter http.ResponseWriter
    41  	err            interface{}
    42  	httpRequest    *http.Request
    43  	stacktrace     string
    44  	status         int
    45  
    46  	// Used to check if controller didn't write anything so
    47  	// core can write default 204 No Content.
    48  	// See RFC 7231, 6.3.5
    49  	empty       bool
    50  	wroteHeader bool
    51  	hijacked    bool
    52  }
    53  
    54  // newResponse create a new Response using the given http.ResponseWriter and raw request.
    55  func newResponse(writer http.ResponseWriter, rawRequest *http.Request) *Response {
    56  	return &Response{
    57  		responseWriter: writer,
    58  		writer:         writer,
    59  		httpRequest:    rawRequest,
    60  		empty:          true,
    61  		status:         0,
    62  		wroteHeader:    false,
    63  		err:            nil,
    64  	}
    65  }
    66  
    67  // --------------------------------------
    68  // PreWriter implementation
    69  
    70  // PreWrite writes the response header after calling PreWrite on the
    71  // child writer if it implements PreWriter.
    72  func (r *Response) PreWrite(b []byte) {
    73  	r.empty = false
    74  	if pr, ok := r.writer.(PreWriter); ok {
    75  		pr.PreWrite(b)
    76  	}
    77  	if !r.wroteHeader {
    78  		if r.status == 0 {
    79  			r.status = http.StatusOK
    80  		}
    81  		r.WriteHeader(r.status)
    82  	}
    83  }
    84  
    85  // --------------------------------------
    86  // http.ResponseWriter implementation
    87  
    88  // Write writes the data as a response.
    89  // See http.ResponseWriter.Write
    90  func (r *Response) Write(data []byte) (int, error) {
    91  	r.PreWrite(data)
    92  	return r.writer.Write(data)
    93  }
    94  
    95  // WriteHeader sends an HTTP response header with the provided
    96  // status code.
    97  // Prefer using "Status()" method instead.
    98  // Calling this method a second time will have no effect.
    99  func (r *Response) WriteHeader(status int) {
   100  	if !r.wroteHeader {
   101  		r.status = status
   102  		r.wroteHeader = true
   103  		r.responseWriter.WriteHeader(status)
   104  	}
   105  }
   106  
   107  // Header returns the header map that will be sent.
   108  func (r *Response) Header() http.Header {
   109  	return r.responseWriter.Header()
   110  }
   111  
   112  // --------------------------------------
   113  // http.Hijacker implementation
   114  
   115  // Hijack implements the Hijacker.Hijack method.
   116  // For more details, check http.Hijacker.
   117  //
   118  // Returns ErrNotHijackable if the underlying http.ResponseWriter doesn't
   119  // implement http.Hijacker. This can happen with HTTP/2 connections.
   120  //
   121  // Middleware executed after controller handlers, as well as status handlers,
   122  // keep working as usual after a connection has been hijacked.
   123  // Callers should properly set the response status to ensure middleware and
   124  // status handler execute correctly. Usually, callers of the Hijack method
   125  // set the HTTP status to http.StatusSwitchingProtocols.
   126  // If no status is set, the regular behavior will be kept and `204 No Content`
   127  // will be set as the response status.
   128  func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
   129  	hijacker, ok := r.responseWriter.(http.Hijacker)
   130  	if !ok {
   131  		return nil, nil, ErrNotHijackable
   132  	}
   133  	c, b, e := hijacker.Hijack()
   134  	if e == nil {
   135  		r.hijacked = true
   136  	}
   137  	return c, b, e
   138  }
   139  
   140  // Hijacked returns true if the underlying connection has been successfully hijacked
   141  // via the Hijack method.
   142  func (r *Response) Hijacked() bool {
   143  	return r.hijacked
   144  }
   145  
   146  // --------------------------------------
   147  // Chained writers
   148  
   149  // Writer return the current writer used to write the response.
   150  // Note that the returned writer is not necessarily a http.ResponseWriter, as
   151  // it can be replaced using SetWriter.
   152  func (r *Response) Writer() io.Writer {
   153  	return r.writer
   154  }
   155  
   156  // SetWriter set the writer used to write the response.
   157  // This can be used to chain writers, for example to enable
   158  // gzip compression, or for logging.
   159  //
   160  // The original http.ResponseWriter is always kept.
   161  func (r *Response) SetWriter(writer io.Writer) {
   162  	r.writer = writer
   163  }
   164  
   165  func (r *Response) close() error {
   166  	if wr, ok := r.writer.(io.Closer); ok {
   167  		return wr.Close()
   168  	}
   169  	return nil
   170  }
   171  
   172  // --------------------------------------
   173  // Accessors
   174  
   175  // GetStatus return the response code for this request or 0 if not yet set.
   176  func (r *Response) GetStatus() int {
   177  	return r.status
   178  }
   179  
   180  // GetError return the value which caused a panic in the request's handling, or nil.
   181  func (r *Response) GetError() interface{} {
   182  	return r.err
   183  }
   184  
   185  // GetStacktrace return the stacktrace of when the error occurred, or an empty string.
   186  // The stacktrace is captured by the recovery middleware.
   187  func (r *Response) GetStacktrace() string {
   188  	return r.stacktrace
   189  }
   190  
   191  // IsEmpty return true if nothing has been written to the response body yet.
   192  func (r *Response) IsEmpty() bool {
   193  	return r.empty
   194  }
   195  
   196  // IsHeaderWritten return true if the response header has been written.
   197  // Once the response header is written, you cannot change the response status
   198  // and headers anymore.
   199  func (r *Response) IsHeaderWritten() bool {
   200  	return r.wroteHeader
   201  }
   202  
   203  // --------------------------------------
   204  // Write methods
   205  
   206  // Status set the response status code.
   207  // Calling this method a second time will have no effect.
   208  func (r *Response) Status(status int) {
   209  	if r.status == 0 {
   210  		r.status = status
   211  	}
   212  }
   213  
   214  // JSON write json data as a response.
   215  // Also sets the "Content-Type" header automatically.
   216  func (r *Response) JSON(responseCode int, data interface{}) error {
   217  	r.responseWriter.Header().Set("Content-Type", "application/json; charset=utf-8")
   218  	r.status = responseCode
   219  	return json.NewEncoder(r).Encode(data)
   220  }
   221  
   222  // String write a string as a response
   223  func (r *Response) String(responseCode int, message string) error {
   224  	r.status = responseCode
   225  	_, err := r.Write([]byte(message))
   226  	return err
   227  }
   228  
   229  func (r *Response) writeFile(file string, disposition string) (int64, error) {
   230  	if !fsutil.FileExists(file) {
   231  		r.Status(http.StatusNotFound)
   232  		return 0, &os.PathError{Op: "open", Path: file, Err: fmt.Errorf("no such file or directory")}
   233  	}
   234  	r.empty = false
   235  	r.status = http.StatusOK
   236  	mime, size := fsutil.GetMIMEType(file)
   237  	header := r.responseWriter.Header()
   238  	header.Set("Content-Disposition", disposition)
   239  
   240  	if header.Get("Content-Type") == "" {
   241  		header.Set("Content-Type", mime)
   242  	}
   243  
   244  	header.Set("Content-Length", strconv.FormatInt(size, 10))
   245  
   246  	f, _ := os.Open(file)
   247  	// No need to check for errors, fsutil.FileExists(file) and
   248  	// fsutil.GetMIMEType(file) already handled that.
   249  	defer f.Close()
   250  	return io.Copy(r, f)
   251  }
   252  
   253  // File write a file as an inline element.
   254  // Automatically detects the file MIME type and sets the "Content-Type" header accordingly.
   255  // If the file doesn't exist, respond with status 404 Not Found.
   256  // The given path can be relative or absolute.
   257  //
   258  // If you want the file to be sent as a download ("Content-Disposition: attachment"), use the "Download" function instead.
   259  func (r *Response) File(file string) error {
   260  	_, err := r.writeFile(file, "inline")
   261  	return err
   262  }
   263  
   264  // Download write a file as an attachment element.
   265  // Automatically detects the file MIME type and sets the "Content-Type" header accordingly.
   266  // If the file doesn't exist, respond with status 404 Not Found.
   267  // The given path can be relative or absolute.
   268  //
   269  // The "fileName" parameter defines the name the client will see. In other words, it sets the header "Content-Disposition" to
   270  // "attachment; filename="${fileName}""
   271  //
   272  // If you want the file to be sent as an inline element ("Content-Disposition: inline"), use the "File" function instead.
   273  func (r *Response) Download(file string, fileName string) error {
   274  	_, err := r.writeFile(file, fmt.Sprintf("attachment; filename=\"%s\"", fileName))
   275  	return err
   276  }
   277  
   278  // Error print the error in the console and return it with an error code 500 (or previously defined
   279  // status code using `response.Status()`).
   280  // If debugging is enabled in the config, the error is also written in the response
   281  // and the stacktrace is printed in the console.
   282  // If debugging is not enabled, only the status code is set, which means you can still
   283  // write to the response, or use your error status handler.
   284  func (r *Response) Error(err interface{}) error {
   285  	ErrLogger.Println(err)
   286  	return r.error(err)
   287  }
   288  
   289  func (r *Response) error(err interface{}) error {
   290  	r.err = err
   291  	if config.GetBool("app.debug") {
   292  		stacktrace := r.stacktrace
   293  		if stacktrace == "" {
   294  			stacktrace = string(debug.Stack())
   295  		}
   296  		ErrLogger.Print(stacktrace)
   297  		if !r.Hijacked() {
   298  			var message interface{}
   299  			if e, ok := err.(error); ok {
   300  				message = e.Error()
   301  			} else {
   302  				message = err
   303  			}
   304  			status := http.StatusInternalServerError
   305  			if r.status != 0 {
   306  				status = r.status
   307  			}
   308  			return r.JSON(status, map[string]interface{}{"error": message})
   309  		}
   310  	}
   311  
   312  	// Don't set r.empty to false to let error status handler process the error
   313  	r.Status(http.StatusInternalServerError)
   314  	return nil
   315  }
   316  
   317  // Cookie add a Set-Cookie header to the response.
   318  // The provided cookie must have a valid Name. Invalid cookies may be
   319  // silently dropped.
   320  func (r *Response) Cookie(cookie *http.Cookie) {
   321  	http.SetCookie(r.responseWriter, cookie)
   322  }
   323  
   324  // Redirect send a permanent redirect response
   325  func (r *Response) Redirect(url string) {
   326  	http.Redirect(r, r.httpRequest, url, http.StatusPermanentRedirect)
   327  }
   328  
   329  // TemporaryRedirect send a temporary redirect response
   330  func (r *Response) TemporaryRedirect(url string) {
   331  	http.Redirect(r, r.httpRequest, url, http.StatusTemporaryRedirect)
   332  }
   333  
   334  // Render a text template with the given data.
   335  // The template path is relative to the "resources/template" directory.
   336  func (r *Response) Render(responseCode int, templatePath string, data interface{}) error {
   337  	tmplt, err := template.ParseFiles(r.getTemplateDirectory() + templatePath)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	var b bytes.Buffer
   343  	if err := tmplt.Execute(&b, data); err != nil {
   344  		return err
   345  	}
   346  
   347  	return r.String(responseCode, b.String())
   348  }
   349  
   350  // RenderHTML an HTML template with the given data.
   351  // The template path is relative to the "resources/template" directory.
   352  func (r *Response) RenderHTML(responseCode int, templatePath string, data interface{}) error {
   353  	tmplt, err := htmltemplate.ParseFiles(r.getTemplateDirectory() + templatePath)
   354  	if err != nil {
   355  		return err
   356  	}
   357  
   358  	var b bytes.Buffer
   359  	if err := tmplt.Execute(&b, data); err != nil {
   360  		return err
   361  	}
   362  
   363  	return r.String(responseCode, b.String())
   364  }
   365  
   366  func (r *Response) getTemplateDirectory() string {
   367  	sep := string(os.PathSeparator)
   368  	workingDir, err := os.Getwd()
   369  	if err != nil {
   370  		panic(err)
   371  	}
   372  	return workingDir + sep + "resources" + sep + "template" + sep
   373  }
   374  
   375  // HandleDatabaseError takes a database query result and checks if any error has occurred.
   376  //
   377  // Automatically writes HTTP status code 404 Not Found if the error is a "Not found" error.
   378  // Calls "Response.Error()" if there is another type of error.
   379  //
   380  // Returns true if there is no error.
   381  func (r *Response) HandleDatabaseError(db *gorm.DB) bool {
   382  	if db.Error != nil {
   383  		if errors.Is(db.Error, gorm.ErrRecordNotFound) {
   384  			r.Status(http.StatusNotFound)
   385  		} else {
   386  			r.Error(db.Error)
   387  		}
   388  		return false
   389  	}
   390  	return true
   391  }