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