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 }