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 }