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 }