goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/response.go (about)

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